-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/assets gate #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| { | ||
| "models": [ | ||
| { | ||
| "file": "/models/base_600.glb", | ||
| "sha256": "45f4940517b77d748dc71d4efa6fc07ca4115ce92143fde4dfdcb424959b0b54", | ||
| "bytes": 4360 | ||
| }, | ||
| { | ||
| "file": "/models/base_600@lod1.glb", | ||
| "sha256": "c21ba9280c2a68a068b1282429c1c650303631d048492daf0e9bbc3c6c639d3e", | ||
| "bytes": 4052 | ||
| }, | ||
| { | ||
| "file": "/models/tall_600.glb", | ||
| "sha256": "31ec07b07df7f59c48369c96c723b8037e51f79605bf62f0ae9c1e0a53e8c99e", | ||
| "bytes": 4360 | ||
| }, | ||
| { | ||
| "file": "/models/tall_600@lod1.glb", | ||
| "sha256": "98a0063fc91dcbac054f5c8f88dc74f5e6f35525fc79d296c13755f6b77577e5", | ||
| "bytes": 4052 | ||
| }, | ||
| { | ||
| "file": "/models/wall_900.glb", | ||
| "sha256": "22c1c6d9269c079be53ca662f2583c033e57cea0b87d5d6ff80a62320e76e00d", | ||
| "bytes": 4364 | ||
| }, | ||
| { | ||
| "file": "/models/wall_900@lod1.glb", | ||
| "sha256": "de16699463f5a3eeda4e07ade0ca2e35ff34c3a6232fffde7c2178d844ca8304", | ||
| "bytes": 4052 | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the executable permission. The shebang is present but the file is not marked as executable. This prevents the script from being run directly. Apply this fix: chmod +x scripts/gen_glb_manifest.pyBased on static analysis hints. 🧰 Tools🪛 Ruff (0.14.0)1-1: Shebang is present but file is not executable (EXE001) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| """Generate manifest.json describing GLB assets (path, sha256, size).""" | ||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import glob | ||||||||||||||||||||||||||||||||||||
| import hashlib | ||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ROOT = Path(__file__).resolve().parents[1] | ||||||||||||||||||||||||||||||||||||
| MODELS_PATTERN = ROOT / "frontend" / "public" / "models" / "*.glb" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def manifest_entry(path: Path) -> dict: | ||||||||||||||||||||||||||||||||||||
| data = path.read_bytes() | ||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||
| "file": "/models/" + path.name, | ||||||||||||||||||||||||||||||||||||
| "sha256": hashlib.sha256(data).hexdigest(), | ||||||||||||||||||||||||||||||||||||
| "bytes": len(data), | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Add error handling for file operations. The function reads the entire file into memory and computes its hash without handling potential I/O errors (e.g., permission denied, file not found during read). Apply this diff to add basic error handling: def manifest_entry(path: Path) -> dict:
- data = path.read_bytes()
+ try:
+ data = path.read_bytes()
+ except OSError as e:
+ raise RuntimeError(f"Failed to read {path}: {e}") from e
return {
"file": "/models/" + path.name,
"sha256": hashlib.sha256(data).hexdigest(),
"bytes": len(data),
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def main() -> int: | ||||||||||||||||||||||||||||||||||||
| paths = sorted(MODELS_PATTERN.parent.glob(MODELS_PATTERN.name)) | ||||||||||||||||||||||||||||||||||||
| entries = [manifest_entry(path) for path in paths] | ||||||||||||||||||||||||||||||||||||
| manifest = {"models": entries} | ||||||||||||||||||||||||||||||||||||
| print(json.dumps(manifest, indent=2)) | ||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Simplify glob usage and add validation. Line 25 constructs the glob pattern in a convoluted way. Additionally, the function doesn't handle the case where no GLB files are found. Apply this diff to simplify and add validation: def main() -> int:
- paths = sorted(MODELS_PATTERN.parent.glob(MODELS_PATTERN.name))
+ paths = sorted(Path(MODELS_PATTERN).parent.glob("*.glb"))
+ if not paths:
+ print("Warning: No GLB files found", file=__import__("sys").stderr)
+ return 1
entries = [manifest_entry(path) for path in paths]
manifest = {"models": entries}
print(json.dumps(manifest, indent=2))
return 0📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||||||||||||||||||||||
| raise SystemExit(main()) | ||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,272 @@ | ||||||||
| #!/usr/bin/env python3 | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Shebang vs execution mode. Either make the script executable ( -#!/usr/bin/env python3📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.14.0)1-1: Shebang is present but file is not executable (EXE001) 🤖 Prompt for AI Agents |
||||||||
| """ | ||||||||
| Generate deterministic reference GLBs for the Paform viewer pipeline. | ||||||||
|
|
||||||||
| Emits base_600.glb, wall_900.glb, tall_600.glb into frontend/public/models/. | ||||||||
| Each GLB: | ||||||||
| - Uses millimetre scale with pivot/min bounds at the origin. | ||||||||
| - Declares Module metadata under extras (code, dimensions, materials). | ||||||||
| - Contains at least one mesh node with extras.panelType. | ||||||||
| - Includes simple base-color textures (4x4 PNG) so texture pipelines stay engaged. | ||||||||
| The resulting GLBs are intentionally minimal and are expected to be re-packed | ||||||||
| via scripts/pack_models.sh to add compression + LODs. | ||||||||
| """ | ||||||||
| from __future__ import annotations | ||||||||
|
|
||||||||
| import base64 | ||||||||
| import json | ||||||||
| import os | ||||||||
| import struct | ||||||||
| from pathlib import Path | ||||||||
| from typing import Dict, Iterable, List, Sequence, Tuple | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Modernize typing imports. Use built-in generics and collections.abc to address Ruff UP035. -from typing import Dict, Iterable, List, Sequence, Tuple
+from collections.abc import Iterable, Sequence
+from typing import Dict, List, Tuple # or prefer built-in: dict, list, tuple annotationsOptionally: replace 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.14.0)21-21: Import from Import from (UP035) 21-21: (UP035) 21-21: (UP035) 21-21: (UP035) 🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| ROOT = Path(__file__).resolve().parents[1] | ||||||||
| MODELS_DIR = ROOT / "frontend" / "public" / "models" | ||||||||
|
|
||||||||
| # 4x4 PNG pixels (white & grey) encoded to ensure textures exist pre-pack and can be BasisU compressed. | ||||||||
| PNG_WHITE = base64.b64decode( | ||||||||
| "iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAE0lEQVR4nGP8//8/AwwwwVl4OQCWbgMF7ZjH1AAAAABJRU5ErkJggg==" | ||||||||
| ) | ||||||||
| PNG_GREY = base64.b64decode( | ||||||||
| "iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAE0lEQVR4nGM8ceIEAwwwwVl4OQB2NAJgnoBkZwAAAABJRU5ErkJggg==" | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| def pack_f32(values: Iterable[float]) -> bytes: | ||||||||
| return b"".join(struct.pack("<f", float(v)) for v in values) | ||||||||
|
|
||||||||
|
|
||||||||
| def pack_u16(values: Iterable[int]) -> bytes: | ||||||||
| return b"".join(struct.pack("<H", int(v)) for v in values) | ||||||||
|
|
||||||||
|
|
||||||||
| def pad4(data: bytes, pad: bytes = b" ") -> bytes: | ||||||||
| remainder = (-len(data)) % 4 | ||||||||
| if remainder: | ||||||||
| data += pad * remainder | ||||||||
| return data | ||||||||
|
|
||||||||
|
|
||||||||
| def build_box(width: float, height: float, depth: float, subdivisions: int = 2) -> Tuple[List[float], List[float], List[int]]: | ||||||||
| """Create a box mesh with per-face subdivisions so LOD simplification has room to operate.""" | ||||||||
|
|
||||||||
| w, h, d = float(width), float(height), float(depth) | ||||||||
| positions: List[float] = [] | ||||||||
| texcoords: List[float] = [] | ||||||||
| indices: List[int] = [] | ||||||||
|
|
||||||||
| faces = [ | ||||||||
| # front (faces -Z) | ||||||||
| ((0.0, h, 0.0), (w, 0.0, 0.0), (0.0, -h, 0.0)), | ||||||||
| # right (+X) | ||||||||
| ((w, h, 0.0), (0.0, 0.0, d), (0.0, -h, 0.0)), | ||||||||
| # back (+Z) | ||||||||
| ((w, h, d), (-w, 0.0, 0.0), (0.0, -h, 0.0)), | ||||||||
| # left (-X) | ||||||||
| ((0.0, h, d), (0.0, 0.0, -d), (0.0, -h, 0.0)), | ||||||||
| # top (+Y) | ||||||||
| ((0.0, h, d), (w, 0.0, 0.0), (0.0, 0.0, -d)), | ||||||||
| # bottom (-Y) | ||||||||
| ((0.0, 0.0, 0.0), (w, 0.0, 0.0), (0.0, 0.0, d)), | ||||||||
| ] | ||||||||
|
|
||||||||
| for origin, u_vec, v_vec in faces: | ||||||||
| start_idx = len(positions) // 3 | ||||||||
| for v_idx in range(subdivisions + 1): | ||||||||
| v_ratio = v_idx / subdivisions if subdivisions else 0.0 | ||||||||
| for u_idx in range(subdivisions + 1): | ||||||||
| u_ratio = u_idx / subdivisions if subdivisions else 0.0 | ||||||||
| px = origin[0] + u_vec[0] * u_ratio + v_vec[0] * v_ratio | ||||||||
| py = origin[1] + u_vec[1] * u_ratio + v_vec[1] * v_ratio | ||||||||
| pz = origin[2] + u_vec[2] * u_ratio + v_vec[2] * v_ratio | ||||||||
| positions.extend([px, py, pz]) | ||||||||
| texcoords.extend([u_ratio, v_ratio]) | ||||||||
|
|
||||||||
| stride = subdivisions + 1 | ||||||||
| for v_idx in range(subdivisions): | ||||||||
| for u_idx in range(subdivisions): | ||||||||
| top_left = start_idx + v_idx * stride + u_idx | ||||||||
| top_right = top_left + 1 | ||||||||
| bottom_left = top_left + stride | ||||||||
| bottom_right = bottom_left + 1 | ||||||||
| indices.extend([top_left, bottom_left, bottom_right]) | ||||||||
| indices.extend([top_left, bottom_right, top_right]) | ||||||||
|
|
||||||||
| return positions, texcoords, indices | ||||||||
|
|
||||||||
|
|
||||||||
| def create_glb_document( | ||||||||
| code: str, width: int, height: int, depth: int | ||||||||
| ) -> Tuple[Dict, bytes]: | ||||||||
| positions, texcoords, indices = build_box(width, height, depth) | ||||||||
| position_blob = pack_f32(positions) | ||||||||
| texcoord_blob = pack_f32(texcoords) | ||||||||
| index_blob = pack_u16(indices) | ||||||||
|
|
||||||||
| def add_view(blob: bytes, *, target: int | None = None) -> Tuple[int, bytes]: | ||||||||
| nonlocal buffer_bytes, byte_offset | ||||||||
| view = {"buffer": 0, "byteOffset": byte_offset, "byteLength": len(blob)} | ||||||||
| if target is not None: | ||||||||
| view["target"] = target | ||||||||
| padded = pad4(blob, b"\x00") | ||||||||
| buffer_bytes += padded | ||||||||
| index = len(buffer_views) | ||||||||
| buffer_views.append(view) | ||||||||
| byte_offset += len(padded) | ||||||||
| return index, padded | ||||||||
|
|
||||||||
| buffer_views: List[Dict] = [] | ||||||||
| buffer_bytes = b"" | ||||||||
| byte_offset = 0 | ||||||||
|
|
||||||||
| pos_view, _ = add_view(position_blob, target=34962) # ARRAY_BUFFER | ||||||||
| tex_view, _ = add_view(texcoord_blob, target=34962) | ||||||||
| idx_view, _ = add_view(index_blob, target=34963) # ELEMENT_ARRAY_BUFFER | ||||||||
| white_view, _ = add_view(PNG_WHITE) | ||||||||
| grey_view, _ = add_view(PNG_GREY) | ||||||||
|
|
||||||||
| accessors = [ | ||||||||
| { | ||||||||
| "bufferView": pos_view, | ||||||||
| "componentType": 5126, | ||||||||
| "count": len(positions) // 3, | ||||||||
| "type": "VEC3", | ||||||||
| "min": [0.0, 0.0, 0.0], | ||||||||
| "max": [float(width), float(height), float(depth)], | ||||||||
| }, | ||||||||
| { | ||||||||
| "bufferView": tex_view, | ||||||||
| "componentType": 5126, | ||||||||
| "count": len(texcoords) // 2, | ||||||||
| "type": "VEC2", | ||||||||
| }, | ||||||||
| { | ||||||||
| "bufferView": idx_view, | ||||||||
| "componentType": 5123, | ||||||||
| "count": len(indices), | ||||||||
| "type": "SCALAR", | ||||||||
| }, | ||||||||
| ] | ||||||||
|
|
||||||||
| images = [ | ||||||||
| {"bufferView": white_view, "mimeType": "image/png"}, | ||||||||
| {"bufferView": grey_view, "mimeType": "image/png"}, | ||||||||
| ] | ||||||||
| textures = [{"source": 0}, {"source": 1}] | ||||||||
|
|
||||||||
| materials = [ | ||||||||
| { | ||||||||
| "name": "Mat::Carcass", | ||||||||
| "pbrMetallicRoughness": { | ||||||||
| "baseColorTexture": {"index": 0}, | ||||||||
| "metallicFactor": 0.0, | ||||||||
| "roughnessFactor": 0.9, | ||||||||
| }, | ||||||||
| }, | ||||||||
| { | ||||||||
| "name": "Mat::Front", | ||||||||
| "pbrMetallicRoughness": { | ||||||||
| "baseColorTexture": {"index": 1}, | ||||||||
| "metallicFactor": 0.0, | ||||||||
| "roughnessFactor": 0.9, | ||||||||
| }, | ||||||||
| }, | ||||||||
| ] | ||||||||
|
|
||||||||
| meshes = [ | ||||||||
| { | ||||||||
| "name": f"{code}_Body", | ||||||||
| "primitives": [ | ||||||||
| { | ||||||||
| "attributes": {"POSITION": 0, "TEXCOORD_0": 1}, | ||||||||
| "indices": 2, | ||||||||
| "mode": 4, | ||||||||
| "material": 0, | ||||||||
| } | ||||||||
| ], | ||||||||
| } | ||||||||
| ] | ||||||||
|
|
||||||||
| module_extras = { | ||||||||
| "paform_glb_schema": "1.0.0", | ||||||||
| "module": { | ||||||||
| "code": code, | ||||||||
| "type": "Cabinet", | ||||||||
| "system": "Prime", | ||||||||
| "family": "Reference", | ||||||||
| "width_mm": width, | ||||||||
| "height_mm": height, | ||||||||
| "depth_mm": depth, | ||||||||
| }, | ||||||||
| "dimensionsMm": {"width": width, "height": height, "depth": depth}, | ||||||||
| "materials": { | ||||||||
| "carcass": "Mat::Carcass", | ||||||||
| "front": "Mat::Front", | ||||||||
| }, | ||||||||
| "lod": {"level": 0, "triangleCount": len(indices) // 3}, | ||||||||
| "qa": {"validatorVersion": "0.0.0"}, | ||||||||
| } | ||||||||
|
|
||||||||
| nodes = [ | ||||||||
| { | ||||||||
| "name": f"Module::{code.upper()}", | ||||||||
| "mesh": 0, | ||||||||
| "extras": {**module_extras, "panelType": "carcass"}, | ||||||||
| } | ||||||||
| ] | ||||||||
|
|
||||||||
| document = { | ||||||||
| "asset": {"version": "2.0", "generator": "paform/reference-generator"}, | ||||||||
| "scenes": [{"nodes": [0]}], | ||||||||
| "scene": 0, | ||||||||
| "nodes": nodes, | ||||||||
| "meshes": meshes, | ||||||||
| "materials": materials, | ||||||||
| "textures": textures, | ||||||||
| "images": images, | ||||||||
| "buffers": [{"byteLength": len(buffer_bytes)}], | ||||||||
| "bufferViews": buffer_views, | ||||||||
| "accessors": accessors, | ||||||||
| "extensionsUsed": [], | ||||||||
| } | ||||||||
| return document, buffer_bytes | ||||||||
|
|
||||||||
|
|
||||||||
| def write_glb(path: Path, document: Dict, binary_blob: bytes) -> None: | ||||||||
| json_bytes = pad4(json.dumps(document, separators=(",", ":")).encode("utf-8")) | ||||||||
| bin_bytes = pad4(binary_blob, b"\x00") | ||||||||
|
|
||||||||
| total_length = 12 + 8 + len(json_bytes) + 8 + len(bin_bytes) | ||||||||
| header = struct.pack("<4sII", b"glTF", 2, total_length) | ||||||||
| json_chunk = struct.pack("<I4s", len(json_bytes), b"JSON") | ||||||||
| bin_chunk = struct.pack("<I4s", len(bin_bytes), b"BIN\x00") | ||||||||
|
|
||||||||
| path.parent.mkdir(parents=True, exist_ok=True) | ||||||||
| with path.open("wb") as fh: | ||||||||
| fh.write(header) | ||||||||
| fh.write(json_chunk) | ||||||||
| fh.write(json_bytes) | ||||||||
| fh.write(bin_chunk) | ||||||||
| fh.write(bin_bytes) | ||||||||
|
|
||||||||
|
|
||||||||
| def main() -> None: | ||||||||
| MODELS_DIR.mkdir(parents=True, exist_ok=True) | ||||||||
| legacy = MODELS_DIR / "BaseCabinet600.glb" | ||||||||
| if legacy.exists(): | ||||||||
| legacy.unlink() | ||||||||
|
|
||||||||
| configs = [ | ||||||||
| ("base_600", 600, 720, 580), | ||||||||
| ("wall_900", 900, 720, 360), | ||||||||
| ("tall_600", 600, 2100, 580), | ||||||||
| ] | ||||||||
| for code, width, height, depth in configs: | ||||||||
| path = MODELS_DIR / f"{code}.glb" | ||||||||
| doc, blob = create_glb_document(code, width, height, depth) | ||||||||
| write_glb(path, doc, blob) | ||||||||
| print(f"Wrote {path.relative_to(ROOT)}") | ||||||||
|
|
||||||||
|
|
||||||||
| if __name__ == "__main__": | ||||||||
| main() | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Standardize Python invocation; align with pack script.
Use python3 consistently to avoid env mismatches (some systems have no
python). Example:Note:
> public/models/manifest.jsonand the*.glbglob rely on a POSIX shell; if Windows support is needed, consider a node wrapper script.🏁 Script executed:
Length of output: 1752
Use python3 in package.json scripts
Replace
pythonwithpython3inassets:gen,assets:validate, andassets:manifestto match the scripts’ shebangs. These commands use POSIX globs and redirection; for Windows support, consider a Node-based wrapper.🤖 Prompt for AI Agents