Skip to content

Commit bd0ed37

Browse files
📝 Add docstrings to codex/implement-param-driven-3d-viewer
Docstrings generation was requested by @shayancoin. * #456 (comment) The following files were modified: * `backend/api/routes_catalog.py` * `backend/tests/test_routes_catalog.py` * `frontend/src/app/components/Viewer3D.tsx` * `frontend/src/app/configurator/page.tsx` * `scripts/generate_reference_glbs.py`
1 parent 934b2fe commit bd0ed37

File tree

5 files changed

+276
-10
lines changed

5 files changed

+276
-10
lines changed

backend/api/routes_catalog.py

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@
2828

2929
@lru_cache(maxsize=1)
3030
def _load_catalog(path: Path) -> Any:
31+
"""
32+
Load and parse a JSON catalog file from the given filesystem path.
33+
34+
Parameters:
35+
path (Path): Filesystem path to the catalog JSON file.
36+
37+
Returns:
38+
Any: The parsed JSON content (typically a dict or list).
39+
40+
Raises:
41+
HTTPException: With status 500 and detail "catalog file not found" if the file is missing.
42+
HTTPException: With status 500 and detail "catalog file is invalid" if the file contains invalid JSON.
43+
"""
3144
try:
3245
with path.open("r", encoding="utf-8") as fh:
3346
return json.load(fh)
@@ -39,6 +52,19 @@ def _load_catalog(path: Path) -> Any:
3952

4053
@lru_cache(maxsize=1)
4154
def _load_mesh_manifest(path: Path) -> Dict[str, Any]:
55+
"""
56+
Load and parse the mesh manifest JSON from the given filesystem path.
57+
58+
Parameters:
59+
path (Path): Filesystem path to the mesh manifest JSON file.
60+
61+
Returns:
62+
manifest (Dict[str, Any]): Parsed manifest JSON as a dictionary.
63+
64+
Raises:
65+
HTTPException: 500 with detail "mesh manifest not found" if the file is missing;
66+
500 with detail "mesh manifest is invalid" if the file contains invalid JSON.
67+
"""
4268
try:
4369
with path.open("r", encoding="utf-8") as fh:
4470
return json.load(fh)
@@ -49,6 +75,20 @@ def _load_mesh_manifest(path: Path) -> Dict[str, Any]:
4975

5076

5177
def _find_manifest_entry(manifest: Dict[str, Any], variant_id: str) -> Dict[str, Any]:
78+
"""
79+
Find the manifest entry that corresponds to the given variant identifier.
80+
81+
Parameters:
82+
manifest (Dict[str, Any]): Parsed mesh manifest containing a "models" list.
83+
variant_id (str): Variant identifier to locate. Can be an entry's `id`, a `variant`, or a composite of the form `"variant@lodX"`.
84+
85+
Returns:
86+
Dict[str, Any]: The matching model entry from the manifest.
87+
88+
Raises:
89+
HTTPException: 500 if the manifest is missing or malformed (no "models" list or invalid model entries).
90+
HTTPException: 404 if no matching model entry is found.
91+
"""
5292
models = manifest.get("models")
5393
if not isinstance(models, list):
5494
raise HTTPException(status_code=500, detail="mesh manifest missing models")
@@ -89,6 +129,18 @@ def _find_manifest_entry(manifest: Dict[str, Any], variant_id: str) -> Dict[str,
89129

90130

91131
def _decode_mesh_bytes(entry: Dict[str, Any]) -> bytes:
132+
"""
133+
Decode base64-encoded GLB mesh data from a manifest entry.
134+
135+
Parameters:
136+
entry (Dict[str, Any]): Manifest entry expected to contain a non-empty `meshBase64` string.
137+
138+
Returns:
139+
bytes: The decoded GLB binary data.
140+
141+
Raises:
142+
HTTPException: 500 if `meshBase64` is missing or empty, or if the base64 data is invalid.
143+
"""
92144
mesh_value = entry.get("meshBase64")
93145
if not isinstance(mesh_value, str) or not mesh_value:
94146
raise HTTPException(status_code=500, detail="mesh manifest missing mesh data")
@@ -100,6 +152,17 @@ def _decode_mesh_bytes(entry: Dict[str, Any]) -> bytes:
100152

101153

102154
def _parse_glb_document(data: bytes) -> Dict[str, Any]:
155+
"""
156+
Extracts and parses the JSON chunk from a GLB binary.
157+
158+
Returns:
159+
document (Dict[str, Any]): The parsed JSON object from the GLB's JSON chunk.
160+
161+
Raises:
162+
HTTPException: If the GLB header is invalid (500, "invalid GLB header").
163+
HTTPException: If the JSON chunk cannot be parsed (500, "invalid GLB metadata").
164+
HTTPException: If no JSON chunk is present in the GLB (500, "GLB missing JSON chunk").
165+
"""
103166
if len(data) < 12 or data[:4] != b"glTF":
104167
raise HTTPException(status_code=500, detail="invalid GLB header")
105168

@@ -120,6 +183,18 @@ def _parse_glb_document(data: bytes) -> Dict[str, Any]:
120183

121184

122185
def _extract_variant_extras(document: Dict[str, Any]) -> Dict[str, Any]:
186+
"""
187+
Locate and return a node's extras dictionary that contains the "paform:variant" key from a parsed GLB JSON document.
188+
189+
Parameters:
190+
document (Dict[str, Any]): Parsed GLB JSON document (typically the JSON chunk).
191+
192+
Returns:
193+
Dict[str, Any]: The node's `extras` dictionary that includes the `"paform:variant"` entry.
194+
195+
Raises:
196+
HTTPException: With status 500 if no node extras containing `"paform:variant"` are found.
197+
"""
123198
nodes = document.get("nodes")
124199
if isinstance(nodes, list):
125200
for node in nodes:
@@ -133,14 +208,32 @@ def _extract_variant_extras(document: Dict[str, Any]) -> Dict[str, Any]:
133208

134209
@router.get("/option-graph")
135210
def get_option_graph() -> Any:
136-
"""Return the option graph catalog used by CPQ pricing flows."""
211+
"""
212+
Retrieve the option graph catalog used by CPQ pricing flows.
213+
214+
Returns:
215+
catalog (Any): The parsed catalog data (typically a dict or list) representing the option graph.
216+
"""
137217

138218
return _load_catalog(CATALOG_PATH)
139219

140220

141221
@router_mesh.get("/{variant_id}/mesh")
142222
def get_catalog_mesh(variant_id: str) -> Dict[str, Any]:
143-
"""Return the GLB payload and variant metadata for the requested catalog entry."""
223+
"""
224+
Return the GLB payload and extracted variant metadata for the specified catalog entry.
225+
226+
Returns:
227+
payload (Dict[str, Any]): Dictionary with keys:
228+
- id (str | None): manifest entry id.
229+
- variant (str): variant code or resolved variant identifier.
230+
- lod (int | None): level-of-detail value from the manifest entry.
231+
- sha256 (str | None): SHA-256 checksum from the manifest entry.
232+
- bytes (int): size in bytes (from entry or inferred from decoded data).
233+
- mesh (str | None): Base64-encoded GLB data from the manifest entry.
234+
- metadata (Dict[str, Any]): `paform:variant` metadata extracted from the GLB.
235+
- extras (Dict[str, Any]): extras object from the GLB node that contained `paform:variant`.
236+
"""
144237

145238
manifest = _load_mesh_manifest(MESH_MANIFEST_PATH)
146239
entry = _find_manifest_entry(manifest, variant_id)
@@ -168,7 +261,13 @@ def get_catalog_mesh(variant_id: str) -> Dict[str, Any]:
168261

169262
@router_mesh.get("/{variant_id}/mesh.glb")
170263
def download_catalog_mesh(variant_id: str) -> Response:
171-
"""Return the raw GLB binary for the requested catalog entry."""
264+
"""
265+
Serve the raw GLB binary for a catalog variant as a downloadable file.
266+
267+
Returns:
268+
Response: HTTP response with the GLB bytes as media type `model/gltf-binary` and a
269+
`Content-Disposition` header prompting download using `<id|variant|variant_id>.glb` as the filename.
270+
"""
172271

173272
manifest = _load_mesh_manifest(MESH_MANIFEST_PATH)
174273
entry = _find_manifest_entry(manifest, variant_id)
@@ -177,4 +276,4 @@ def download_catalog_mesh(variant_id: str) -> Response:
177276
filename = f"{(entry.get('id') or entry.get('variant') or variant_id)}.glb"
178277
headers = {"Content-Disposition": f"attachment; filename=\"{filename}\""}
179278

180-
return Response(content=data, media_type="model/gltf-binary", headers=headers)
279+
return Response(content=data, media_type="model/gltf-binary", headers=headers)

backend/tests/test_routes_catalog.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ def test_get_catalog_mesh_glb_downloads_binary() -> None:
4242

4343

4444
def test_get_catalog_mesh_missing_variant_returns_404() -> None:
45+
"""
46+
Check that requesting a nonexistent catalog mesh variant returns HTTP 404.
47+
48+
Sends a GET request for a missing catalog variant and asserts the response status code is 404.
49+
"""
4550
client = TestClient(app)
4651

4752
response = client.get("/api/catalog/unknown_variant/mesh")
48-
assert response.status_code == 404
53+
assert response.status_code == 404

frontend/src/app/components/Viewer3D.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ const isObject3DLike = (
6464
return isVector3Like(candidate.scale) && isVector3Like(candidate.position);
6565
};
6666

67+
/**
68+
* Marks a mesh source as having its LOD0 become visible shortly after it is mounted or changed.
69+
*
70+
* When `src` is provided in a browser environment, schedules a two-frame animation-frame delay, then
71+
* records the current timestamp in a global Map at `window.__lod0VisibleAt` keyed by `src` and
72+
* dispatches a `CustomEvent` named `"lod0:visible"` with `{ src, ts }` in `event.detail`. Cancels
73+
* any pending animation frames when `src` changes or the component unmounts. No-op on the server.
74+
*
75+
* @param src - A string identifier or URL for the mesh whose LOD0 visibility should be recorded; if `null` nothing is scheduled
76+
*/
6777
function useLodVisibility(src: string | null) {
6878
useEffect(() => {
6979
if (typeof window === 'undefined' || !src) {
@@ -104,6 +114,15 @@ function useLodVisibility(src: string | null) {
104114
}, [src]);
105115
}
106116

117+
/**
118+
* Selects the primary manifest entry for a given variant id following priority rules.
119+
*
120+
* Searches the provided manifest for an entry whose `id` exactly matches `variantId`, then for an entry whose `variant` equals `variantId` and has `lod` undefined or `0`. If no exact or zero-LOD match is found, returns the first entry whose `variant` equals `variantId` (used as a fallback).
121+
*
122+
* @param manifest - Array of manifest entries to search
123+
* @param variantId - Variant identifier to select the primary entry for
124+
* @returns The chosen manifest entry for `variantId`, or `undefined` if none found
125+
*/
107126
function selectPrimaryEntry(manifest: MeshManifestItem[], variantId: string): MeshManifestItem | undefined {
108127
let fallback: MeshManifestItem | undefined;
109128
for (const entry of manifest) {
@@ -125,6 +144,15 @@ function selectPrimaryEntry(manifest: MeshManifestItem[], variantId: string): Me
125144
return fallback;
126145
}
127146

147+
/**
148+
* Create a blob object URL from a base64-encoded GLB payload for use with model loaders.
149+
*
150+
* When `meshBase64` is provided, the hook decodes it into a binary GLB Blob and returns a corresponding object URL.
151+
* The created object URL is revoked automatically when `meshBase64` changes or the component unmounts.
152+
*
153+
* @param meshBase64 - A base64-encoded GLB (binary glTF) payload; pass `null`/`undefined` to clear the URL.
154+
* @returns The object URL for the decoded GLB Blob, or `null` if no payload was provided or decoding failed.
155+
*/
128156
function useMeshObjectUrl(meshBase64: string | null | undefined): string | null {
129157
const [objectUrl, setObjectUrl] = useState<string | null>(null);
130158

@@ -170,6 +198,19 @@ type LoadedModelProps = {
170198
onWidthChange: (widthMm: number) => void;
171199
};
172200

201+
/**
202+
* Render a loaded GLTF scene with scale transform controls and report width updates.
203+
*
204+
* Applies the provided transform to the scene, exposes interactive scaling along the configured axis,
205+
* disables camera orbiting while the user is manipulating the model, and invokes `onWidthChange`
206+
* when the effective model width (in millimeters) changes due to a scale operation.
207+
*
208+
* @param modelUrl - Object URL or path to a GLTF/GLB model to load and render
209+
* @param meshId - Identifier used for LOD visibility tracking for the loaded model
210+
* @param transform - Optional transform state (scale, translation, axis, base dimensions) to apply to the model; defaults are used when omitted
211+
* @param onWidthChange - Callback invoked with the computed width in millimeters when the model's scale along the active axis changes
212+
* @returns The React element that renders the loaded model with TransformControls
213+
*/
173214
function LoadedModel({ modelUrl, meshId, transform, onWidthChange }: LoadedModelProps) {
174215
const gltf = useGLTF(modelUrl);
175216
const orbitControls = useThree((state) => state.controls) as { enabled?: boolean } | null;
@@ -234,6 +275,13 @@ function LoadedModel({ modelUrl, meshId, transform, onWidthChange }: LoadedModel
234275
);
235276
}
236277

278+
/**
279+
* Render a simple placeholder box mesh used when a real model is unavailable.
280+
*
281+
* The component marks a placeholder key as visible for LOD tracking and returns a compact box mesh with a blue standard material.
282+
*
283+
* @returns A React element containing the fallback box mesh
284+
*/
237285
function FallbackModel() {
238286
useLodVisibility('mock-model');
239287

@@ -245,6 +293,18 @@ function FallbackModel() {
245293
);
246294
}
247295

296+
/**
297+
* Render a 3D viewer that loads and displays a GLTF model for the specified variant.
298+
*
299+
* The component selects a primary manifest entry for `variantId`, ensures the mesh
300+
* manifest is loaded, decodes a base64 GLTF payload to an object URL when available,
301+
* and renders either the loaded model with transform controls or a simple fallback.
302+
* It also exposes the current mesh width in the DOM for testing and updates persisted
303+
* transform width via the configurator store when the model is scaled.
304+
*
305+
* @param variantId - The variant identifier used to select the manifest entry and transform key
306+
* @returns A React element containing the Three.js Canvas, model or fallback, environment (optional), and controls
307+
*/
248308
export default function Viewer3D({ variantId }: Viewer3DProps) {
249309
const disableEnvironment = process.env.NEXT_PUBLIC_DISABLE_ENVIRONMENT === 'true';
250310
const disableModelLoading = process.env.NEXT_PUBLIC_DISABLE_MODEL_LOADING === 'true';
@@ -303,4 +363,4 @@ export default function Viewer3D({ variantId }: Viewer3DProps) {
303363
)}
304364
</>
305365
);
306-
}
366+
}

frontend/src/app/configurator/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import { Box, Flex } from '@chakra-ui/react'
22
import Viewer3D from '../components/Viewer3D'
33
import ConfiguratorPanel from '../components/configurator-panel'
44

5+
/**
6+
* Renders the product configurator page with a responsive layout containing a 3D viewer and a configuration panel.
7+
*
8+
* The 3D viewer is passed the variant id `'base_600'`.
9+
*
10+
* @returns The React element representing the configurator page layout.
11+
*/
512
export default function ConfiguratorPage() {
613
const demoVariant = 'base_600'
714
return (
@@ -16,4 +23,3 @@ export default function ConfiguratorPage() {
1623
)
1724
}
1825

19-

0 commit comments

Comments
 (0)