diff --git a/doc/changelog.d/128.fixed.md b/doc/changelog.d/128.fixed.md new file mode 100644 index 0000000..bbebc70 --- /dev/null +++ b/doc/changelog.d/128.fixed.md @@ -0,0 +1 @@ +Web deprecation diff --git a/doc/changelog.d/81.removed.md b/doc/changelog.d/81.removed.md new file mode 100644 index 0000000..1f66425 --- /dev/null +++ b/doc/changelog.d/81.removed.md @@ -0,0 +1 @@ +Removed the ``ansys.tools.usdviewer.web`` subpackage. HTML export now lives in ``ansys-tools-visualization-interface`` — install with ``pip install 'ansys-tools-visualization-interface[usd]'`` and use ``from ansys.tools.visualization_interface import export_usd_to_html``. \ No newline at end of file diff --git a/examples/03-web-view.py b/examples/03-web-view.py deleted file mode 100644 index 74b7cdf..0000000 --- a/examples/03-web-view.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -""" -.. _ref_usd_web_viewer: - -============== -USD Web Viewer -============== - -This example shows how to generate a web-based USD viewer component. -""" -import webbrowser - -from ansys.tools.usdviewer.web.html_export import export_viewer_html - - -if __name__ == "__main__": - html_path = export_viewer_html("assets/display_color_vtk.usda") - print(f"Viewer page generated at: {html_path}") - print("Controls: left-drag orbit, right-drag pan, mouse wheel zoom.") - webbrowser.open(html_path.resolve().as_uri()) - diff --git a/examples/04-export-viewer-html.py b/examples/04-export-viewer-html.py deleted file mode 100644 index c6c438d..0000000 --- a/examples/04-export-viewer-html.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -""" -.. _ref_usd_web_viewer_integration: - -========================== -USD Web Viewer integration -========================== - -Example on how to export a USD viewer and embed it inside a standalone webpage via iframe. - -Running this script produces two files in the same directory as the source -asset: - -* ``display_color_vtk_viewer.html`` - the self-contained Three.js viewer -* ``display_color_vtk_page.html`` - a demo webpage that hosts the viewer - inside an `` - - -

- The viewer above is a self-contained {viewer_filename} - file generated by - export_viewer_html() and embedded here with a single - <iframe> tag. Replace the - src attribute with any - *_viewer.html file produced by the same function to - display a different model. -

- - - diff --git a/pyproject.toml b/pyproject.toml index 7ae0b13..f03afeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ dependencies = [ "pyopengl", # OpenUSD build dep "pyside6", # OpenUSD build dep "vtk", - "pygltflib", ] scripts.usd-setup = "ansys.tools.usdviewer.autosetup:main" @@ -70,7 +69,6 @@ tests = [ [tool.hatch.build.targets.wheel] packages = [ "src/ansys" ] -include = [ "src/ansys/tools/usdviewer/web/*.html" ] [tool.ruff] line-length = 120 diff --git a/src/ansys/tools/usdviewer/web/__init__.py b/src/ansys/tools/usdviewer/web/__init__.py deleted file mode 100644 index a0eb108..0000000 --- a/src/ansys/tools/usdviewer/web/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Web viewer package exports.""" - -from ansys.tools.usdviewer.web.html_export import export_viewer_html - -__all__ = ["export_viewer_html"] diff --git a/src/ansys/tools/usdviewer/web/glb.py b/src/ansys/tools/usdviewer/web/glb.py deleted file mode 100644 index 4220d30..0000000 --- a/src/ansys/tools/usdviewer/web/glb.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""GLB conversion entrypoint for USD web viewer export.""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any - -from pxr import Usd - -from .glb_builder import GLBBuilder, create_gltf_scene -from .glb_lights import append_usd_lights_to_scene -from .glb_mesh import convert_mesh_prim_to_gltf, iter_usd_mesh_prims - - -def _open_stage_for_glb(path: Path) -> Any: - """Open a USD stage for GLB conversion. - - Parameters - ---------- - path : Path - Path to the USD file to open. - - Returns - ------- - Any - The opened USD stage. - """ - stage = Usd.Stage.Open(str(path)) - if not stage: - raise RuntimeError(f"Failed to open USD stage: {path}") - return stage - - -def convert_usd_to_glb(path: Path) -> bytes: - """Convert a USD stage to a self-contained GLB binary. - - Parameters - ---------- - path : Path - Path to the USD file to convert. - - Returns - ------- - bytes - The GLB binary data. - """ - stage = _open_stage_for_glb(path) - gltf, scene = create_gltf_scene() - builder = GLBBuilder(gltf) - texture_cache: dict[tuple[str, str, str], int] = {} - image_cache: dict[str, int] = {} - sampler_cache: dict[tuple[int, int], int] = {} - - append_usd_lights_to_scene(stage, gltf, scene) - - for prim in iter_usd_mesh_prims(stage): - convert_mesh_prim_to_gltf( - prim, - gltf, - scene, - builder, - texture_cache, - image_cache, - sampler_cache, - ) - - return builder.finalize() diff --git a/src/ansys/tools/usdviewer/web/glb_builder.py b/src/ansys/tools/usdviewer/web/glb_builder.py deleted file mode 100644 index 82ede7c..0000000 --- a/src/ansys/tools/usdviewer/web/glb_builder.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Low-level helpers to build GLB buffers and base glTF scene state.""" - -from __future__ import annotations - -from typing import Any - -import pygltflib - - -class GLBBuilder: - """Utility for writing GLTF buffers and accessors. - - This class manages the binary buffer and associated glTF structures when - converting USD assets to GLB format. It provides methods to append data - to the shared buffer and create corresponding buffer views and accessors. - - Parameters - ---------- - gltf : Any - An initialized glTF document (e.g. pygltflib.GLTF2) to populate. - """ - - def __init__(self, gltf: Any): - """Initialize the GLBBuilder with a GLTF document.""" - self.gltf = gltf - self.binary_blob = bytearray() - - def _pad4(self) -> None: - """Pad the binary buffer to the next 4-byte boundary.""" - while len(self.binary_blob) % 4: - self.binary_blob.append(0) - - def add_buffer_view(self, data: bytes, target: int | None) -> int: - """Append binary data to the shared buffer and return its buffer-view index. - - Parameters - ---------- - data : bytes - The binary data to append to the buffer. - target : int | None - The target type for the buffer view (e.g., pygltflib.ARRAY_BUFFER or pygltflib.ELEMENT_ARRAY_BUFFER). - """ - self._pad4() - offset = len(self.binary_blob) - self.binary_blob.extend(data) - buffer_view = pygltflib.BufferView(buffer=0, byteOffset=offset, byteLength=len(data)) - if target is not None: - buffer_view.target = target - self.gltf.bufferViews.append(buffer_view) - return len(self.gltf.bufferViews) - 1 - - def add_accessor( - self, - buffer_view_idx: int, - component_type: int, - count: int, - accessor_type: str, - min_values: list[float] | None = None, - max_values: list[float] | None = None, - ) -> int: - """Create an accessor for an existing buffer view and return its index. - - Parameters - ---------- - buffer_view_idx : int - The index of the buffer view this accessor references. - component_type : int - The GLTF component type (e.g., pygltflib.FLOAT, pygltflib.UNSIGNED_SHORT). - count : int - The number of elements in the accessor. - accessor_type : str - The GLTF accessor type (e.g., "SCALAR", "VEC3", "MAT4"). - min_values : list[float] | None, optional - Minimum values for the accessor (used for bounding boxes), by default None. - max_values : list[float] | None, optional - Maximum values for the accessor (used for bounding boxes), by default None. - """ - accessor = pygltflib.Accessor( - bufferView=buffer_view_idx, - byteOffset=0, - componentType=component_type, - count=count, - type=accessor_type, - ) - if min_values is not None: - accessor.min = min_values - if max_values is not None: - accessor.max = max_values - self.gltf.accessors.append(accessor) - return len(self.gltf.accessors) - 1 - - def finalize(self) -> bytes: - """Finalize the GLTF document and return the serialized GLB bytes. - - Returns - ------- - bytes - The complete GLB file as a byte string. - """ - self.gltf.buffers.append(pygltflib.Buffer(byteLength=len(self.binary_blob))) - self.gltf.set_binary_blob(bytes(self.binary_blob)) - data = self.gltf.save_to_bytes() - return data if isinstance(data, bytes) else b"".join(data) - - -def create_gltf_scene() -> tuple[Any, Any]: - """Create an initialized glTF document and default scene. - - Returns - ------- - tuple[Any, Any] - A tuple containing the glTF document and the default scene object. - """ - gltf = pygltflib.GLTF2() - gltf.asset = pygltflib.Asset(version="2.0") - scene = pygltflib.Scene(nodes=[]) - gltf.scenes.append(scene) - gltf.scene = 0 - return gltf, scene diff --git a/src/ansys/tools/usdviewer/web/glb_lights.py b/src/ansys/tools/usdviewer/web/glb_lights.py deleted file mode 100644 index a93a55c..0000000 --- a/src/ansys/tools/usdviewer/web/glb_lights.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""USD-to-glTF light conversion helpers.""" - -from __future__ import annotations - -from typing import Any - -from pxr import Usd, UsdGeom, UsdLux -import pygltflib - - -def _ensure_lights_extension(gltf: Any) -> list[dict[str, Any]]: - """Ensure KHR_lights_punctual is declared and return its light list. - - Parameters - ---------- - gltf : Any - The glTF document to check and modify. - - Returns - ------- - list[dict[str, Any]] - The list of lights in the KHR_lights_punctual extension, which may be newly created. - """ - if gltf.extensionsUsed is None: - gltf.extensionsUsed = [] - if "KHR_lights_punctual" not in gltf.extensionsUsed: - gltf.extensionsUsed.append("KHR_lights_punctual") - - if gltf.extensions is None: - gltf.extensions = {} - - lights_ext = gltf.extensions.get("KHR_lights_punctual") - if lights_ext is None: - lights_ext = {"lights": []} - gltf.extensions["KHR_lights_punctual"] = lights_ext - - lights = lights_ext.get("lights") - if lights is None: - lights = [] - lights_ext["lights"] = lights - - return lights - - -def _get_light_schema_and_type(prim: Any) -> tuple[Any, str] | None: - """Map a USD light prim to a KHR_lights_punctual-compatible type. - - Parameters - ---------- - prim : Any - The USD light prim to map. - - Returns - ------- - tuple[Any, str] | None - A tuple containing the USD light schema and the corresponding glTF light type, - or None if the prim is not a supported light type. - """ - if prim.IsA(UsdLux.DistantLight): - return UsdLux.DistantLight(prim), "directional" - - if prim.IsA(UsdLux.SphereLight): - return UsdLux.SphereLight(prim), "point" - - if prim.IsA(UsdLux.DiskLight): - return UsdLux.DiskLight(prim), "point" - - return None - - -def _make_gltf_light(prim: Any) -> dict[str, Any] | None: - """Build one glTF punctual light description from a USD light prim. - - Parameters - ---------- - prim : Any - The USD light prim to convert. - - Returns - ------- - dict[str, Any] | None - A dictionary representing the glTF light, or None if the prim is not a supported light type. - """ - schema_and_type = _get_light_schema_and_type(prim) - if schema_and_type is None: - return None - - light_schema, light_type = schema_and_type - color_value = light_schema.GetColorAttr().Get() - intensity_value = light_schema.GetIntensityAttr().Get() - exposure_value = light_schema.GetExposureAttr().Get() - - if intensity_value is None: - intensity_value = 1.0 - intensity = float(intensity_value) - if exposure_value is not None: - intensity *= 2.0 ** float(exposure_value) - - # USD light intensity conventions vary across renderers and can be orders - # of magnitude larger than typical WebGL values. - if light_type == "directional": - intensity /= 50000.0 - else: - intensity /= 500.0 - intensity = max(0.0, min(intensity, 20.0)) - - if color_value is None: - color = [1.0, 1.0, 1.0] - else: - color = [float(color_value[0]), float(color_value[1]), float(color_value[2])] - - light: dict[str, Any] = { - "type": light_type, - "color": color, - "intensity": intensity, - "name": prim.GetName(), - } - return light - - -def append_usd_lights_to_scene(stage: Any, gltf: Any, scene: Any) -> None: - """Convert supported USD lights into glTF KHR_lights_punctual lights. - - Parameters - ---------- - stage : Any - The USD stage containing the lights. - gltf : Any - The glTF document to modify. - scene : Any - The glTF scene to which the lights will be added. - """ - lights = _ensure_lights_extension(gltf) - - for prim in stage.Traverse(): - gltf_light = _make_gltf_light(prim) - if gltf_light is None: - continue - - light_index = len(lights) - lights.append(gltf_light) - - node = pygltflib.Node( - name=prim.GetName(), - extensions={"KHR_lights_punctual": {"light": light_index}}, - ) - - xformable = UsdGeom.Xformable(prim) - if xformable: - world = xformable.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) - translation = world.ExtractTranslation() - node.translation = [float(translation[0]), float(translation[1]), float(translation[2])] - - gltf.nodes.append(node) - scene.nodes.append(len(gltf.nodes) - 1) diff --git a/src/ansys/tools/usdviewer/web/glb_materials.py b/src/ansys/tools/usdviewer/web/glb_materials.py deleted file mode 100644 index babb9f7..0000000 --- a/src/ansys/tools/usdviewer/web/glb_materials.py +++ /dev/null @@ -1,445 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Material and texture conversion helpers for GLB export.""" - -from __future__ import annotations - -import mimetypes -from pathlib import Path -from typing import Any - -from pxr import UsdGeom, UsdShade -import pygltflib - -from .glb_builder import GLBBuilder - - -def _append_material_and_get_index(gltf: Any, material: Any) -> int: - """Append a GLTF material and return its index. - - Parameters - ---------- - gltf : Any - The glTF document to modify. - material : Any - The material to append to the glTF document. - - Returns - ------- - int - The index of the newly appended material in the glTF document. - """ - gltf.materials.append(material) - return len(gltf.materials) - 1 - - -def _build_material_from_shader(shader: Any, prim_name: str) -> Any: - """Build a GLTF material from a UsdPreviewSurface shader. - - Parameters - ---------- - shader : Any - The UsdPreviewSurface shader to convert. - prim_name : str - The name of the primitive associated with the shader. - - Returns - ------- - Any - The resulting GLTF material. - """ - # Match UsdPreviewSurface defaults to avoid glTF defaults (metallic=1) - # when USD scalar inputs are texture-connected and return None. - pbr = pygltflib.PbrMetallicRoughness( - baseColorFactor=[1.0, 1.0, 1.0, 1.0], - metallicFactor=0.0, - roughnessFactor=0.5, - ) - - diffuse = shader.GetInput("diffuseColor") - if diffuse and diffuse.Get() is not None: - dc = diffuse.Get() - pbr.baseColorFactor = [float(dc[0]), float(dc[1]), float(dc[2]), 1.0] - - metallic = shader.GetInput("metallic") - if metallic and metallic.Get() is not None: - pbr.metallicFactor = float(metallic.Get()) - - roughness = shader.GetInput("roughness") - if roughness and roughness.Get() is not None: - pbr.roughnessFactor = float(roughness.Get()) - - mat = pygltflib.Material(pbrMetallicRoughness=pbr, name=prim_name, doubleSided=True) - - emissive = shader.GetInput("emissiveColor") - if emissive and emissive.Get() is not None: - ec = emissive.Get() - mat.emissiveFactor = [float(ec[0]), float(ec[1]), float(ec[2])] - - return mat - - -def _get_connected_shader_input(shader: Any, input_name: str) -> tuple[Any, str] | None: - """Return connected shader and output name for a shader input. - - Parameters - ---------- - shader : Any - The shader containing the input to check. - input_name : str - The name of the shader input to check for a connection. - - Returns - ------- - tuple[Any, str] | None - A tuple containing the connected shader and output name, or None if no connection exists. - """ - shader_input = shader.GetInput(input_name) - if not shader_input: - return None - - connected = shader_input.GetConnectedSource() - if not connected: - return None - - source_api, output_name, _ = connected - source_prim = source_api.GetPrim() - if not source_prim or not source_prim.IsValid(): - return None - - source_shader = UsdShade.Shader(source_prim) - if not source_shader or not source_shader.GetPrim().IsValid(): - return None - - return source_shader, output_name - - -def _map_wrap_mode(wrap_mode: Any) -> int: - """Map USD wrap token to glTF sampler wrap mode. - - Parameters - ---------- - wrap_mode : Any - The USD token representing the wrap mode (e.g., "repeat", "mirror", "clamp"). - - Returns - ------- - int - The corresponding glTF wrap mode constant (e.g., pygltflib.REPEAT). - """ - if wrap_mode == "repeat": - return pygltflib.REPEAT - if wrap_mode == "mirror": - return pygltflib.MIRRORED_REPEAT - return pygltflib.CLAMP_TO_EDGE - - -def _resolve_asset_file_path(asset_input: Any, shader_prim: Any) -> Path | None: - """Resolve a USD asset path from an input to a file path. - - Parameters - ---------- - asset_input : Any - The USD asset input to resolve. - shader_prim : Any - The shader primitive associated with the asset input. - - Returns - ------- - Path | None - The resolved file path, or None if it cannot be resolved. - """ - if not asset_input: - return None - - asset_value = asset_input.Get() - if asset_value is None: - return None - - resolved = getattr(asset_value, "resolvedPath", None) - if resolved: - resolved_path = Path(str(resolved)) - if resolved_path.exists(): - return resolved_path - - authored = getattr(asset_value, "path", None) - if not authored: - return None - - authored_path = Path(str(authored)) - if authored_path.is_absolute() and authored_path.exists(): - return authored_path - - layer = shader_prim.GetStage().GetRootLayer() - if not layer: - return None - layer_path = Path(layer.realPath) - candidate = (layer_path.parent / authored_path).resolve() - return candidate if candidate.exists() else None - - -def _get_texture_index_from_usd_uv_texture( - source_shader: Any, - gltf: Any, - builder: GLBBuilder, - texture_cache: dict[tuple[str, str, str], int], - image_cache: dict[str, int], - sampler_cache: dict[tuple[int, int], int], -) -> int | None: - """Create or reuse a glTF texture index from a UsdUVTexture shader. - - Parameters - ---------- - source_shader : Any - The UsdUVTexture shader to convert. - gltf : Any - The glTF document to modify. - builder : GLBBuilder - The GLBBuilder instance managing the binary buffer. - texture_cache : dict[tuple[str, str, str], int] - Cache mapping (file_path, wrap_s, wrap_t) to glTF texture indices. - image_cache : dict[str, int] - Cache mapping file paths to glTF image indices. - sampler_cache : dict[tuple[int, int], int] - Cache mapping (wrap_s, wrap_t) to glTF sampler indices. - - Returns - ------- - int | None - The index of the corresponding glTF texture, or None if the shader is not a - UsdUVTexture or lacks a valid file input. - """ - shader_id = source_shader.GetIdAttr().Get() - if shader_id != "UsdUVTexture": - return None - - file_path = _resolve_asset_file_path(source_shader.GetInput("file"), source_shader.GetPrim()) - if file_path is None: - return None - - wrap_s = _map_wrap_mode(source_shader.GetInput("wrapS").Get() if source_shader.GetInput("wrapS") else None) - wrap_t = _map_wrap_mode(source_shader.GetInput("wrapT").Get() if source_shader.GetInput("wrapT") else None) - - texture_key = (str(file_path), str(wrap_s), str(wrap_t)) - cached_texture = texture_cache.get(texture_key) - if cached_texture is not None: - return cached_texture - - image_key = str(file_path) - image_index = image_cache.get(image_key) - if image_index is None: - image_bytes = file_path.read_bytes() - image_buffer_view = builder.add_buffer_view(image_bytes, None) - mime_type, _ = mimetypes.guess_type(str(file_path)) - if mime_type is None: - mime_type = "image/png" - gltf.images.append(pygltflib.Image(bufferView=image_buffer_view, mimeType=mime_type, name=file_path.name)) - image_index = len(gltf.images) - 1 - image_cache[image_key] = image_index - - sampler_key = (wrap_s, wrap_t) - sampler_index = sampler_cache.get(sampler_key) - if sampler_index is None: - gltf.samplers.append(pygltflib.Sampler(wrapS=wrap_s, wrapT=wrap_t)) - sampler_index = len(gltf.samplers) - 1 - sampler_cache[sampler_key] = sampler_index - - gltf.textures.append(pygltflib.Texture(source=image_index, sampler=sampler_index)) - texture_index = len(gltf.textures) - 1 - texture_cache[texture_key] = texture_index - return texture_index - - -def _apply_preview_surface_textures( - shader: Any, - material: Any, - gltf: Any, - builder: GLBBuilder, - texture_cache: dict[tuple[str, str, str], int], - image_cache: dict[str, int], - sampler_cache: dict[tuple[int, int], int], -) -> None: - """Apply supported UsdPreviewSurface texture connections to a GLTF material. - - Parameters - ---------- - shader : Any - The UsdPreviewSurface shader to check for texture connections. - material : Any - The GLTF material to modify with texture references. - gltf : Any - The glTF document to modify with new textures, images, and samplers as needed. - builder : GLBBuilder - The GLBBuilder instance managing the binary buffer. - texture_cache : dict[tuple[str, str, str], int] - Cache mapping (file_path, wrap_s, wrap_t) to glTF texture indices. - image_cache : dict[str, int] - Cache mapping file paths to glTF image indices. - sampler_cache : dict[tuple[int, int], int] - Cache mapping (wrap_s, wrap_t) to glTF sampler indices. - """ - pbr = material.pbrMetallicRoughness - - diffuse_connection = _get_connected_shader_input(shader, "diffuseColor") - if diffuse_connection is not None: - diffuse_shader, _ = diffuse_connection - tex_index = _get_texture_index_from_usd_uv_texture( - diffuse_shader, - gltf, - builder, - texture_cache, - image_cache, - sampler_cache, - ) - if tex_index is not None: - pbr.baseColorTexture = pygltflib.TextureInfo(index=tex_index) - - normal_connection = _get_connected_shader_input(shader, "normal") - if normal_connection is not None: - normal_shader, _ = normal_connection - tex_index = _get_texture_index_from_usd_uv_texture( - normal_shader, - gltf, - builder, - texture_cache, - image_cache, - sampler_cache, - ) - if tex_index is not None: - material.normalTexture = pygltflib.NormalMaterialTexture(index=tex_index) - - emissive_connection = _get_connected_shader_input(shader, "emissiveColor") - if emissive_connection is not None: - emissive_shader, _ = emissive_connection - tex_index = _get_texture_index_from_usd_uv_texture( - emissive_shader, - gltf, - builder, - texture_cache, - image_cache, - sampler_cache, - ) - if tex_index is not None: - material.emissiveTexture = pygltflib.TextureInfo(index=tex_index) - - opacity_input = shader.GetInput("opacity") - opacity_threshold_input = shader.GetInput("opacityThreshold") - opacity_threshold = float(opacity_threshold_input.Get()) if opacity_threshold_input else 0.0 - has_connected_opacity = _get_connected_shader_input(shader, "opacity") is not None - opacity_value = float(opacity_input.Get()) if opacity_input and opacity_input.Get() is not None else 1.0 - - if has_connected_opacity or opacity_value < 1.0: - material.alphaMode = "MASK" if opacity_threshold > 0.0 else "BLEND" - if opacity_threshold > 0.0: - material.alphaCutoff = opacity_threshold - - -def _build_display_color_material(display_colors: Any, prim_name: str) -> Any: - """Build a fallback GLTF material from the first displayColor value. - - Parameters - ---------- - display_colors : Any - The displayColor values to use for the material. - prim_name : str - The name of the prim to use for the material name. - - Returns - ------- - Any - The constructed GLTF material. - """ - c = display_colors[0] - pbr = pygltflib.PbrMetallicRoughness(baseColorFactor=[float(c[0]), float(c[1]), float(c[2]), 1.0]) - return pygltflib.Material(pbrMetallicRoughness=pbr, name=f"{prim_name}_displayColor", doubleSided=True) - - -def get_material_index( - prim: Any, - display_colors: Any, - color_interpolation: Any, - gltf: Any, - builder: GLBBuilder, - texture_cache: dict[tuple[str, str, str], int], - image_cache: dict[str, int], - sampler_cache: dict[tuple[int, int], int], -) -> int | None: - """Resolve GLTF material index from USD bindings or displayColor fallback. - - Parameters - ---------- - prim : Any - The USD prim to get the material for. - display_colors : Any - The displayColor values to use for the material. - color_interpolation : Any - The interpolation method for the display colors. - gltf : Any - The glTF document to modify with new textures, images, and samplers as needed. - builder : GLBBuilder - The GLBBuilder instance managing the binary buffer. - texture_cache : dict[tuple[str, str, str], int] - Cache mapping (file_path, wrap_s, wrap_t) to glTF texture indices. - image_cache : dict[str, int] - Cache mapping file paths to glTF image indices. - sampler_cache : dict[tuple[int, int], int] - Cache mapping (wrap_s, wrap_t) to glTF sampler indices. - - Returns - ------- - int | None - The index of the resolved GLTF material, or None if no material is found. - """ - mat_index = None - binding = UsdShade.MaterialBindingAPI(prim).GetDirectBinding() - if binding: - material = binding.GetMaterial() - if material: - surface_source = material.ComputeSurfaceSource() - shader = surface_source[0] if surface_source else None - if shader and shader.GetPrim().IsValid(): - mat = _build_material_from_shader(shader, prim.GetName()) - _apply_preview_surface_textures( - shader, - mat, - gltf, - builder, - texture_cache, - image_cache, - sampler_cache, - ) - mat_index = _append_material_and_get_index(gltf, mat) - - vertex_interp = {UsdGeom.Tokens.vertex, UsdGeom.Tokens.varying} - if mat_index is None and display_colors: - if color_interpolation in vertex_interp: - # Create a white material to properly use vertex colors (COLOR_0) - pbr = pygltflib.PbrMetallicRoughness(baseColorFactor=[1.0, 1.0, 1.0, 1.0]) - mat = pygltflib.Material(pbrMetallicRoughness=pbr, name=f"{prim.GetName()}_vertexColor", doubleSided=True) - mat_index = _append_material_and_get_index(gltf, mat) - else: - # Use the first color value as a uniform material - mat = _build_display_color_material(display_colors, prim.GetName()) - mat_index = _append_material_and_get_index(gltf, mat) - - return mat_index diff --git a/src/ansys/tools/usdviewer/web/glb_mesh.py b/src/ansys/tools/usdviewer/web/glb_mesh.py deleted file mode 100644 index bfd1610..0000000 --- a/src/ansys/tools/usdviewer/web/glb_mesh.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Mesh geometry conversion helpers for GLB export.""" - -from __future__ import annotations - -import struct -from typing import Any - -from pxr import UsdGeom -import pygltflib - -from .glb_builder import GLBBuilder -from .glb_materials import get_material_index - - -def triangulate_faces(indices: list[int], counts: list[int]) -> list[int]: - """Convert polygon faces to triangles using fan triangulation. - - Parameters - ---------- - indices : list[int] - The list of vertex indices for all faces, as defined by USD's faceVertexIndices. - counts : list[int] - The list of vertex counts for each face, as defined by USD's faceVertexCounts. - - Returns - ------- - list[int] - The list of triangle indices. - """ - result = [] - idx = 0 - for count in counts: - face = indices[idx : idx + count] - for i in range(1, count - 1): - result.extend([face[0], face[i], face[i + 1]]) - idx += count - return result - - -def _add_position_attribute(points: list[Any], builder: GLBBuilder) -> int: - """Write position data and return the POSITION accessor index. - - Parameters - ---------- - points : list[Any] - The list of vertex positions. - builder : GLBBuilder - The GLBBuilder instance managing the binary buffer. - - Returns - ------- - int - The index of the POSITION accessor. - """ - pos_data = struct.pack(f"{len(points) * 3}f", *[c for p in points for c in (float(p[0]), float(p[1]), float(p[2]))]) - xs = [float(p[0]) for p in points] - ys = [float(p[1]) for p in points] - zs = [float(p[2]) for p in points] - pos_buffer_view = builder.add_buffer_view(pos_data, pygltflib.ARRAY_BUFFER) - return builder.add_accessor( - pos_buffer_view, - pygltflib.FLOAT, - len(points), - pygltflib.VEC3, - min_values=[min(xs), min(ys), min(zs)], - max_values=[max(xs), max(ys), max(zs)], - ) - - -def _add_index_accessor(triangle_indices: list[int], builder: GLBBuilder) -> int: - """Write triangle index data and return the index accessor. - - Parameters - ---------- - triangle_indices : list[int] - The list of triangle indices. - builder : GLBBuilder - The GLBBuilder instance managing the binary buffer. - - Returns - ------- - int - The index of the triangle index accessor. - """ - idx_data = struct.pack(f"{len(triangle_indices)}I", *triangle_indices) - idx_buffer_view = builder.add_buffer_view(idx_data, pygltflib.ELEMENT_ARRAY_BUFFER) - return builder.add_accessor(idx_buffer_view, pygltflib.UNSIGNED_INT, len(triangle_indices), pygltflib.SCALAR) - - -def _add_uv_attribute( - attributes: Any, - uv_values: list[tuple[float, float]] | None, - builder: GLBBuilder, -) -> None: - """Attach TEXCOORD_0 accessor when matching UV data exists. - - Parameters - ---------- - attributes : Any - The attributes object to modify. - uv_values : list[tuple[float, float]] | None - The list of UV coordinates. - builder : GLBBuilder - The GLBBuilder instance managing the binary buffer. - """ - if not uv_values: - return - - uv_data = struct.pack(f"{len(uv_values) * 2}f", *[c for uv in uv_values for c in (float(uv[0]), float(uv[1]))]) - uv_buffer_view = builder.add_buffer_view(uv_data, pygltflib.ARRAY_BUFFER) - attributes.TEXCOORD_0 = builder.add_accessor(uv_buffer_view, pygltflib.FLOAT, len(uv_values), pygltflib.VEC2) - - -def _get_display_colors(usd_mesh: Any) -> tuple[Any, Any]: - """Return display colors and interpolation token from USD mesh. - - Parameters - ---------- - usd_mesh : Any - The USD mesh prim to query for display color information. - - Returns - ------- - tuple[Any, Any] - A tuple containing the display colors and the interpolation token. - """ - display_color_primvar = usd_mesh.GetDisplayColorPrimvar() - colors = display_color_primvar.Get() if display_color_primvar else usd_mesh.GetDisplayColorAttr().Get() - interpolation = display_color_primvar.GetInterpolation() if display_color_primvar else None - return colors, interpolation - - -def _add_display_color_attribute( - points: list[Any], - source_point_indices: list[int], - display_colors: Any, - color_interpolation: Any, - attributes: Any, - builder: GLBBuilder, -) -> None: - """Attach COLOR_0 when displayColor is vertex-compatible. - - Parameters - ---------- - points : list[Any] - The list of points in the mesh. - source_point_indices : list[int] - The list of source point indices. - display_colors : Any - The displayColor values to use for the attribute. - color_interpolation : Any - The interpolation method for the display colors. - attributes : Any - The attributes object to modify. - builder : GLBBuilder - The GLBBuilder instance managing the binary buffer. - """ - if not (display_colors and color_interpolation in {UsdGeom.Tokens.vertex, UsdGeom.Tokens.varying}): - return - - if len(display_colors) == len(points): - expanded_colors = display_colors - elif source_point_indices and len(display_colors) > 0: - if any(index < 0 or index >= len(display_colors) for index in source_point_indices): - return - expanded_colors = [display_colors[index] for index in source_point_indices] - else: - return - - color_data = struct.pack( - f"{len(expanded_colors) * 3}f", - *[c for color in expanded_colors for c in (float(color[0]), float(color[1]), float(color[2]))], - ) - color_buffer_view = builder.add_buffer_view(color_data, pygltflib.ARRAY_BUFFER) - attributes.COLOR_0 = builder.add_accessor(color_buffer_view, pygltflib.FLOAT, len(expanded_colors), pygltflib.VEC3) - - -def _append_mesh_to_scene( - prim: Any, - attributes: Any, - idx_acc: int, - mat_index: int | None, - gltf: Any, - scene: Any, -) -> None: - """Append one mesh primitive and its node to the active scene. - - Parameters - ---------- - prim : Any - The USD mesh prim being converted. - attributes : Any - The attributes object for the mesh primitive. - idx_acc : int - The index accessor for the mesh primitive. - mat_index : int | None - The material index for the mesh primitive. - gltf : Any - The glTF object to which the mesh primitive will be added. - scene : Any - The scene object to which the mesh node will be added. - """ - primitive = pygltflib.Primitive(attributes=attributes, indices=idx_acc, material=mat_index) - gltf_mesh = pygltflib.Mesh(primitives=[primitive], name=prim.GetName()) - gltf.meshes.append(gltf_mesh) - - node = pygltflib.Node(mesh=len(gltf.meshes) - 1, name=prim.GetName()) - gltf.nodes.append(node) - scene.nodes.append(len(gltf.nodes) - 1) - - -def _build_attributes_for_mesh( - prim: Any, - usd_mesh: Any, - builder: GLBBuilder, -) -> tuple[Any, int, Any, Any]: - """Build geometric attributes and index accessor for one mesh. - - Parameters - ---------- - prim : Any - The USD mesh prim being converted. - usd_mesh : Any - The USD mesh object. - builder : GLBBuilder - The GLBBuilder instance managing the binary buffer. - - Returns - ------- - tuple[Any, int, Any, Any] - A tuple containing the attributes object, index accessor, display colors, and color interpolation. - """ - triangle_positions, triangle_indices, triangle_uvs, triangle_point_indices = _build_triangle_mesh_data( - prim, usd_mesh - ) - pos_acc = _add_position_attribute(triangle_positions, builder) - idx_acc = _add_index_accessor(triangle_indices, builder) - - attributes = pygltflib.Attributes(POSITION=pos_acc) - _add_uv_attribute(attributes, triangle_uvs, builder) - - display_colors, color_interpolation = _get_display_colors(usd_mesh) - _add_display_color_attribute( - triangle_positions, - triangle_point_indices, - display_colors, - color_interpolation, - attributes, - builder, - ) - return attributes, idx_acc, display_colors, color_interpolation - - -def _resolve_primvar_element_index( - interpolation: Any, - face_index: int, - corner_index: int, - point_index: int, -) -> int | None: - """Resolve primvar element index by interpolation for one face corner. - - Parameters - ---------- - interpolation : Any - The interpolation token for the primvar (e.g., vertex, faceVarying). - face_index : int - The index of the face being processed. - corner_index : int - The index of the corner being processed. - point_index : int - The index of the point being processed. - - Returns - ------- - int | None - The resolved element index for the primvar, or None if the interpolation type is unsupported. - """ - if interpolation in {UsdGeom.Tokens.vertex, UsdGeom.Tokens.varying}: - return point_index - if interpolation == UsdGeom.Tokens.faceVarying: - return corner_index - if interpolation == UsdGeom.Tokens.uniform: - return face_index - if interpolation == UsdGeom.Tokens.constant: - return 0 - return None - - -def _build_corner_uvs( - prim: Any, - face_vertex_indices: list[int], - face_vertex_counts: list[int], -) -> list[tuple[float, float]] | None: - """Build one UV coordinate per face corner, honoring indexed primvars. - - Parameters - ---------- - prim : Any - The USD mesh prim to query for UV information. - face_vertex_indices : list[int] - The indices of the vertices for each face. - face_vertex_counts : list[int] - The number of vertices for each face. - - Returns - ------- - list[tuple[float, float]] | None - A list of UV coordinates for each corner, or None if no UVs are defined. - """ - st_primvar = UsdGeom.PrimvarsAPI(prim).GetPrimvar("st") - if not st_primvar or not st_primvar.IsDefined(): - return None - - st_values = st_primvar.Get() - if not st_values: - return None - - st_indices = st_primvar.GetIndices() or [] - interpolation = st_primvar.GetInterpolation() or UsdGeom.Tokens.faceVarying - - corner_uvs: list[tuple[float, float]] = [] - corner_offset = 0 - for face_index, count in enumerate(face_vertex_counts): - for local_corner in range(count): - corner_index = corner_offset + local_corner - point_index = face_vertex_indices[corner_index] - element_index = _resolve_primvar_element_index(interpolation, face_index, corner_index, point_index) - if element_index is None: - corner_uvs.append((0.0, 0.0)) - continue - - if st_indices and element_index < len(st_indices): - element_index = st_indices[element_index] - - if element_index is None or element_index < 0 or element_index >= len(st_values): - corner_uvs.append((0.0, 0.0)) - continue - - uv = st_values[element_index] - corner_uvs.append((float(uv[0]), float(uv[1]))) - - corner_offset += count - - return corner_uvs - - -def _build_triangle_mesh_data( - prim: Any, - usd_mesh: Any, -) -> tuple[list[Any], list[int], list[tuple[float, float]] | None, list[int]]: - """Build triangle-ready vertex data, including expanded UVs for face-varying primvars. - - Parameters - ---------- - prim : Any - The USD mesh prim being converted. - usd_mesh : Any - The USD mesh object. - - Returns - ------- - tuple[list[Any], list[int], list[tuple[float, float]] | None, list[int]] - A tuple containing the triangle positions, triangle indices, triangle UVs, and triangle point indices. - """ - points = usd_mesh.GetPointsAttr().Get() or [] - face_vertex_indices = list(usd_mesh.GetFaceVertexIndicesAttr().Get() or []) - face_vertex_counts = list(usd_mesh.GetFaceVertexCountsAttr().Get() or []) - - if not points or not face_vertex_indices or not face_vertex_counts: - return points, list(range(len(points))), None, list(range(len(points))) - - corner_uvs = _build_corner_uvs(prim, face_vertex_indices, face_vertex_counts) - - triangle_positions: list[Any] = [] - triangle_indices: list[int] = [] - triangle_uvs: list[tuple[float, float]] | None = [] if corner_uvs is not None else None - triangle_point_indices: list[int] = [] - - corner_offset = 0 - for count in face_vertex_counts: - if count < 3: - corner_offset += count - continue - - for tri_step in range(1, count - 1): - tri_corners = (0, tri_step, tri_step + 1) - for local_corner in tri_corners: - corner_index = corner_offset + local_corner - point_index = face_vertex_indices[corner_index] - if point_index < 0 or point_index >= len(points): - continue - - point = points[point_index] - triangle_positions.append(point) - triangle_indices.append(len(triangle_positions) - 1) - triangle_point_indices.append(point_index) - - if triangle_uvs is not None and corner_uvs is not None: - triangle_uvs.append(corner_uvs[corner_index]) - - corner_offset += count - - return triangle_positions, triangle_indices, triangle_uvs, triangle_point_indices - - -def iter_usd_mesh_prims(stage: Any) -> list[Any]: - """Collect mesh prims from a USD stage. - - Parameters - ---------- - stage : Any - The USD stage to traverse for mesh prims. - - Returns - ------- - list[Any] - A list of USD mesh prims found in the stage. - """ - mesh_prims = [] - for prim in stage.Traverse(): - if prim.IsA(UsdGeom.Mesh): - mesh_prims.append(prim) - return mesh_prims - - -def convert_mesh_prim_to_gltf( - prim: Any, - gltf: Any, - scene: Any, - builder: GLBBuilder, - texture_cache: dict[tuple[str, str, str], int], - image_cache: dict[str, int], - sampler_cache: dict[tuple[int, int], int], -) -> None: - """Convert one USD mesh prim into GLTF mesh/node entries. - - Parameters - ---------- - prim : Any - The USD mesh prim to convert. - gltf : Any - The GLTF object to which the mesh will be added. - scene : Any - The GLTF scene to which the mesh node will be added. - builder : GLBBuilder - The GLBBuilder instance used for constructing the GLTF. - texture_cache : dict[tuple[str, str, str], int] - A cache mapping texture identifiers to GLTF texture indices. - image_cache : dict[str, int] - A cache mapping image file paths to GLTF image indices. - sampler_cache : dict[tuple[int, int], int] - A cache mapping sampler settings to GLTF sampler indices. - """ - usd_mesh = UsdGeom.Mesh(prim) - points = usd_mesh.GetPointsAttr().Get() - if not points: - return - - attributes, idx_acc, display_colors, color_interpolation = _build_attributes_for_mesh( - prim, - usd_mesh, - builder, - ) - mat_index = get_material_index( - prim, - display_colors, - color_interpolation, - gltf, - builder, - texture_cache, - image_cache, - sampler_cache, - ) - _append_mesh_to_scene(prim, attributes, idx_acc, mat_index, gltf, scene) diff --git a/src/ansys/tools/usdviewer/web/glb_template.html b/src/ansys/tools/usdviewer/web/glb_template.html deleted file mode 100644 index 940f4b4..0000000 --- a/src/ansys/tools/usdviewer/web/glb_template.html +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - USD Web Viewer - - - - -
Loading:
- - - - - diff --git a/src/ansys/tools/usdviewer/web/html_export.py b/src/ansys/tools/usdviewer/web/html_export.py deleted file mode 100644 index 7d5ad51..0000000 --- a/src/ansys/tools/usdviewer/web/html_export.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Utilities to export USD assets and launch a simple browser viewer.""" - -from __future__ import annotations - -import base64 -from pathlib import Path - -from ansys.tools.usdviewer.vtk_converter import VTKConverter - -from .glb import convert_usd_to_glb -from .templates import build_viewer_html_glb - -_SUPPORTED_USD_EXTENSIONS = {".usd", ".usda", ".usdc", ".usdz"} - - -def export_viewer_html( - input_path: str | Path, - output_path: str | Path | None = None, -) -> Path: - """Generate a self-contained HTML viewer file for embedding in other pages. - - The generated HTML embeds all mesh geometry as inline JSON and requires - only a CDN connection for Three.js. It is designed to be embedded in - other web pages via an `` - - Parameters - ---------- - input_path : str | Path - Source asset path. Supported formats are ``.usd``, ``.usda``, - ``.usdc``, and ``.usdz``. - output_path : str | Path | None, default: ``None`` - Full path for the generated HTML file. When ``None``, the file is - written alongside the source asset as ``{stem}_viewer.html``. - - Returns - ------- - Path - Absolute path to the generated HTML file. - """ - source_path = Path(input_path).expanduser().resolve() - if not source_path.exists(): - raise FileNotFoundError(f"Input file not found: {source_path}") - - extension = source_path.suffix.lower() - if extension not in _SUPPORTED_USD_EXTENSIONS: - supported = ", ".join(sorted(_SUPPORTED_USD_EXTENSIONS)) - raise ValueError(f"Unsupported input format '{extension}'. Supported formats: {supported}.") - - if output_path is None: - html_path = source_path.parent / f"{source_path.stem}_viewer.html" - else: - html_path = Path(output_path).expanduser().resolve() - - html_path.parent.mkdir(parents=True, exist_ok=True) - _generate_viewer_html(source_path, html_path) - return html_path - - -def _prepare_source_for_web(source_path: Path, export_root: Path) -> Path: - """Prepare a source USD file for web export. - - For files that reference VTK assets via custom ``Asset`` attributes, - convert those assets into USD mesh data before packaging. - - - Parameters - ---------- - source_path : Path - The source USD file to prepare. - export_root : Path - The directory where intermediate files can be written during preparation. - - Returns - ------- - Path - The path to the prepared USD file, which may be the original source if no preparation was needed. - """ - if source_path.suffix.lower() == ".usdz": - return source_path - - try: - from pxr import Usd - except ImportError as exc: - raise RuntimeError( - "OpenUSD Python bindings are required to prepare web export assets. " - "Run usd-setup and ensure pxr is importable." - ) from exc - - stage = Usd.Stage.Open(str(source_path)) - if not stage: - raise RuntimeError(f"Failed to open USD stage: {source_path}") - - vtk_asset_paths: list[Path] = [] - vtk_asset_attrs = [] - for prim in stage.Traverse(): - attr = prim.GetAttribute("Asset") - if not attr: - continue - value = attr.Get() - asset_path = getattr(value, "path", None) - if not asset_path: - continue - if Path(asset_path).suffix.lower() not in {".vtk", ".vtp", ".vtu", ".vts", ".obj", ".ply", ".stl"}: - continue - - resolved_asset = Path(asset_path) - if not resolved_asset.is_absolute(): - resolved_asset = (source_path.parent / resolved_asset).resolve() - vtk_asset_paths.append(resolved_asset) - vtk_asset_attrs.append(attr) - - if not vtk_asset_paths: - return source_path - - converter = VTKConverter() - for vtk_path in vtk_asset_paths: - loaded = converter.load_asset(str(vtk_path), stage) - if loaded is None: - raise RuntimeError(f"Failed to load VTK asset referenced by stage: {vtk_path}") - - # Clear custom Asset links after conversion so USDZ packaging doesn't retain - # unresolved non-USD references that web loaders cannot consume. - for attr in vtk_asset_attrs: - attr.Clear() - - prepared_source = export_root / f"{source_path.stem}_webprep.usda" - stage.Export(str(prepared_source)) - return prepared_source - - -def _generate_viewer_html(source_path: Path, html_path: Path) -> None: - """Prepare mesh data and write a viewer HTML file to *html_path*. - - Converts the USD asset to GLB via ``pygltflib`` for full PBR material - support. Intermediate files (e.g. ``_webprep.usda``) are written to - ``html_path.parent``. - - - Parameters - ---------- - source_path : Path - The source USD file to convert and embed in the viewer. - html_path : Path - The output path for the generated HTML file. - """ - prepared_path = _prepare_source_for_web(source_path, html_path.parent) - glb_bytes = convert_usd_to_glb(prepared_path) - glb_b64 = base64.b64encode(glb_bytes).decode("ascii") - html = build_viewer_html_glb(glb_b64, source_path.name) - html_path.write_text(html, encoding="utf-8") diff --git a/src/ansys/tools/usdviewer/web/templates.py b/src/ansys/tools/usdviewer/web/templates.py deleted file mode 100644 index ca58d37..0000000 --- a/src/ansys/tools/usdviewer/web/templates.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""HTML template rendering helpers for web viewer export.""" - -from __future__ import annotations - -import json -from pathlib import Path - -_GLB_TEMPLATE_FILE = "glb_template.html" - - -def build_viewer_html_glb(glb_b64: str, model_name: str) -> str: - """Build a self-contained HTML viewer that renders a base64-encoded GLB.""" - template = _load_template(_GLB_TEMPLATE_FILE) - return template.replace("__MODEL_NAME_JSON__", json.dumps(model_name)).replace( - "__GLB_B64_JSON__", json.dumps(glb_b64) - ) - - -def _load_template(template_name: str) -> str: - """Load a viewer HTML template from the package directory.""" - template_path = Path(__file__).with_name(template_name) - if not template_path.exists(): - raise RuntimeError(f"Viewer template not found: {template_path}") - return template_path.read_text(encoding="utf-8") diff --git a/tests/test_web_viewer.py b/tests/test_web_viewer.py deleted file mode 100644 index 067a740..0000000 --- a/tests/test_web_viewer.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) 2025 - 2026 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -"""Unit tests for web viewer utilities.""" - -from unittest.mock import patch - -import pytest - -from ansys.tools.usdviewer.web.html_export import export_viewer_html - - -@patch("ansys.tools.usdviewer.web.html_export._prepare_source_for_web") -def test_export_viewer_html_output_path(mock_prepare, tmp_path): - """Test that export_viewer_html respects an explicit output path.""" - usda_path = tmp_path / "scene.usda" - usda_path.write_text("#usda 1.0", encoding="utf-8") - out_path = tmp_path / "output" / "custom.html" - mock_prepare.return_value = usda_path - - html_path = export_viewer_html(usda_path, output_path=out_path) - - assert html_path == out_path - assert html_path.exists() - - -def test_export_viewer_html_missing_file_raises(tmp_path): - """Test that a missing input raises FileNotFoundError.""" - with pytest.raises(FileNotFoundError): - export_viewer_html(tmp_path / "missing.usda") - - -def test_export_viewer_html_unsupported_extension_raises(tmp_path): - """Test that unsupported file formats are rejected.""" - invalid_file = tmp_path / "model.obj" - invalid_file.write_text("data", encoding="utf-8") - - with pytest.raises(ValueError, match="Unsupported input format"): - export_viewer_html(invalid_file) diff --git a/uv.lock b/uv.lock index 975579b..b8faa17 100644 --- a/uv.lock +++ b/uv.lock @@ -62,7 +62,6 @@ source = { editable = "." } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pygltflib" }, { name = "pyopengl" }, { name = "pyside6" }, { name = "vtk" }, @@ -99,7 +98,6 @@ tests = [ [package.metadata] requires-dist = [ { name = "numpy" }, - { name = "pygltflib" }, { name = "pyopengl" }, { name = "pyside6" }, { name = "vtk" }, @@ -618,31 +616,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/8d/7f362c2fb8ef4decd2160bc24d4292c6ca658cc6d9a161b89ca5122bbdbf/cyclopts-4.16.1-py3-none-any.whl", hash = "sha256:617795392c4113a2c2cc7af716f20244900e87f23daa05442d1268d81472a592", size = 219020, upload-time = "2026-05-25T15:29:09.646Z" }, ] -[[package]] -name = "dataclasses-json" -version = "0.6.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "marshmallow" }, - { name = "typing-inspect" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, -] - -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -1045,18 +1018,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "marshmallow" -version = "3.26.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, -] - [[package]] name = "matplotlib" version = "3.10.9" @@ -1141,15 +1102,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "nodeenv" version = "1.10.0" @@ -1495,19 +1447,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/bc/2cb8c78300ce1ace4eeac3b3522218cea2c2053bfa6b4e32cc972a477f9a/pydata_sphinx_theme-0.17.1-py3-none-any.whl", hash = "sha256:320b022d7808bdf5920d9a28e573f27aace9b23e1af6ca103eecc752411df492", size = 6823346, upload-time = "2026-04-21T13:00:31.978Z" }, ] -[[package]] -name = "pygltflib" -version = "1.16.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dataclasses-json" }, - { name = "deprecated" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/e8/f8232abdf9c085333689b0a428dcd1d0f83edd1ecafa6ed878a633d8c9d5/pygltflib-1.16.5.tar.gz", hash = "sha256:1f15740d5a7aaf71a5083e285af6b361184958e255659132f4ba8fe4f3d21ea9", size = 43272, upload-time = "2025-07-24T06:35:38.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/d6/7eb8a0e4eb30add2b76c957a41107a5f2ba26472d656e2733728bec0476b/pygltflib-1.16.5-py3-none-any.whl", hash = "sha256:41d3349c59dcf1586faeaee29c967be07ac2bf7cecdb8ae2b527da1f25afdaac", size = 27557, upload-time = "2025-07-24T06:35:37.328Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -2157,19 +2096,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] -[[package]] -name = "typing-inspect" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, -] - [[package]] name = "urllib3" version = "2.7.0" @@ -2232,92 +2158,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/57/6410098435a3976cc749c151c4e09bff4bcf4af0ebeb13b3e47fbd5871e5/vtk-9.6.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b06725993112097f43daefca0f3637a73a63c67b61a9c124455d881bc05cc9a4", size = 135630292, upload-time = "2026-05-19T04:48:37.053Z" }, ] -[[package]] -name = "wrapt" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/8b/59781d0fe7b0adfbea37f600857de4be68921e454aeecf1a11bda35cdccc/wrapt-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:055e6fcfaa28e58c6a8c247d48b92be9d56f818b7068aa4f22b15b3343a09931", size = 80556, upload-time = "2026-06-20T23:47:28.473Z" }, - { url = "https://files.pythonhosted.org/packages/94/dc/66c61aca927230c9cf97a3cb005c803971a1076ff9f7d61085d035c20085/wrapt-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8374eb6b1a58809211e84ff835a182bb17ab2807a5bfef23204c8cff38178a00", size = 81648, upload-time = "2026-06-20T23:47:30.504Z" }, - { url = "https://files.pythonhosted.org/packages/23/1b/545eee1c18f3af4cf140bb5822b6ef81ebe569df0a63ac109973103a30a5/wrapt-2.2.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:656593bb3f5529f03d27af4136c4d7b11990e470bcbc6fefa5ef218695bece55", size = 152956, upload-time = "2026-06-20T23:47:31.867Z" }, - { url = "https://files.pythonhosted.org/packages/44/a7/6f42a3d03e44dc612a5dcff324e7366075a7857f0be2d49a8cb8a68279b8/wrapt-2.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb00cb7bb22099e2f64b7340fb96113639aa7260c0972af3797ace2297b936c", size = 154771, upload-time = "2026-06-20T23:47:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/bf/55/4d76175aaa97523c38f1d28f79d18ab41a1b116814158a818bc0eba00571/wrapt-2.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7f10ee0bd53673bfd52b67cbce83336fe6cad90d2377b03baf66491d2bbfb91", size = 149460, upload-time = "2026-06-20T23:47:34.712Z" }, - { url = "https://files.pythonhosted.org/packages/84/9b/12e23264d8f4735e8483262f95c5a6b03c3665fd2a84bdf99a45b6a2f4ec/wrapt-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4402f57c5f0d0579599858ffbdd9bf4e3f0972f51096f2bd6cc7dab6b76ee49e", size = 153648, upload-time = "2026-06-20T23:47:36.092Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a3/bcd5ec37289dcd85ecd4d15395a6a6063d60bc45ff94a9d77814e1e54d64/wrapt-2.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3a4eb7964ff4643d333c84f880bcf554652b2a1050aebc54ae696327f61acfaf", size = 148502, upload-time = "2026-06-20T23:47:37.623Z" }, - { url = "https://files.pythonhosted.org/packages/f2/be/716d708f607fa70f8a6eb47dff8ee945d5278dfc89ffeeff33039d052e63/wrapt-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e542b7c5af91e2123a8aabf19894319d5ec4268d2a9ffd2f239386133fc47746", size = 152238, upload-time = "2026-06-20T23:47:39.118Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c0/1a48e7e54501274f5d906f18372221b13183b0afbb5b8bb4c7ca0392c0b4/wrapt-2.2.2-cp310-cp310-win32.whl", hash = "sha256:6e7e45b43d3c774d244fe7264378f5a3f0f383bc55a54a9866434e524540110f", size = 77278, upload-time = "2026-06-20T23:47:40.476Z" }, - { url = "https://files.pythonhosted.org/packages/b0/82/9cd69a1af288fbdedf01a10e3c8a0b6890b08c7f3f96d36a213699dbcd94/wrapt-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:955f1d6e72a352e478de8d8b503abe301c5e139a141b62eb0923bd694995025f", size = 80131, upload-time = "2026-06-20T23:47:41.785Z" }, - { url = "https://files.pythonhosted.org/packages/7f/73/8db7e27daef37ae70a53ea62bef7fe80cc51a8b5e9e9181a8be6eb9a999c/wrapt-2.2.2-cp310-cp310-win_arm64.whl", hash = "sha256:b89d8d73c82db2bb7e6090b3afd7973f980d24e905cc34394eab60b884b3bf67", size = 79615, upload-time = "2026-06-20T23:47:43.109Z" }, - { url = "https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f1a2ff355ece6a111ca7a20dc86df6659c9205d3fcee674ca34f2a2854fd4e73", size = 80782, upload-time = "2026-06-20T23:47:44.367Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b9a899e6fff5444f229d30aa6e9ac92d2216d9d60f33c771b5d76a760d5f8e", size = 81678, upload-time = "2026-06-20T23:47:45.857Z" }, - { url = "https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a2d78c363f97d8bd718ee40432c66395685e9e98528ccaa423c3355d1715a26d", size = 159671, upload-time = "2026-06-20T23:47:47.345Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d619e1eed9bd4f6ed9f24cd61971aa086fa86505289628d464bcf8a2c2e3f328", size = 160785, upload-time = "2026-06-20T23:47:48.759Z" }, - { url = "https://files.pythonhosted.org/packages/45/04/aa5309beed5344b00220ae6b3b24055852192656194c27947bee1736306a/wrapt-2.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:518b0c5e323511ec56a38894802ddd5e1222626484e68efe63f201854ad788e5", size = 153699, upload-time = "2026-06-20T23:47:50.177Z" }, - { url = "https://files.pythonhosted.org/packages/01/df/2def7e99d1fe87eea413f95f671924cdddcb08823b1ffd212748dfa6d062/wrapt-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4bccea5cdecffa9dd70e343741f0e41e0a16619313d04b72f78bb525162ebcd0", size = 159695, upload-time = "2026-06-20T23:47:51.602Z" }, - { url = "https://files.pythonhosted.org/packages/c7/f6/a906d01a2ce12157bad2404957b3e2140da354b8a70b2fa48bbf282871c0/wrapt-2.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:209112cafd963710a05d199aae431d79a28bc76eb8e6d1bbbb8ad24340722cae", size = 152813, upload-time = "2026-06-20T23:47:53.03Z" }, - { url = "https://files.pythonhosted.org/packages/02/49/bc0086292d239575b4c08f4cf8a4079fa58abbad58ec23abf84833a283ed/wrapt-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5a5290e4bf2f332fc29ce72ffb9a2fff678aaac047e2e9f5f7165cd7792e099", size = 158809, upload-time = "2026-06-20T23:47:54.391Z" }, - { url = "https://files.pythonhosted.org/packages/55/83/8fbd034de1f3e907edaa18786d5dd8f6932874edee0826c7cecb5cab03a1/wrapt-2.2.2-cp311-cp311-win32.whl", hash = "sha256:5499236ad1dc116012e2a5dd943f3f31af12fce452128e2bbcbd55a7d3d4d14c", size = 77414, upload-time = "2026-06-20T23:47:55.882Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8636809939152be6ae20a6cef0fed9fe60f411b47847d0426a826884b469e971", size = 80368, upload-time = "2026-06-20T23:47:57.237Z" }, - { url = "https://files.pythonhosted.org/packages/08/49/40cefc342bf89b234a4490d741290fce781774b831aefb39c25471da96c9/wrapt-2.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:5d0a142f7af07caeb5e5da87493162a7b8efa19ba919e550a746f7446e13fb30", size = 79489, upload-time = "2026-06-20T23:47:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484, upload-time = "2026-06-20T23:47:59.924Z" }, - { url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151, upload-time = "2026-06-20T23:48:01.303Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828, upload-time = "2026-06-20T23:48:02.719Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544, upload-time = "2026-06-20T23:48:04.266Z" }, - { url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663, upload-time = "2026-06-20T23:48:05.708Z" }, - { url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387, upload-time = "2026-06-20T23:48:07.243Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849, upload-time = "2026-06-20T23:48:08.91Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147, upload-time = "2026-06-20T23:48:10.374Z" }, - { url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734, upload-time = "2026-06-20T23:48:11.769Z" }, - { url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585, upload-time = "2026-06-20T23:48:13.117Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553, upload-time = "2026-06-20T23:48:14.5Z" }, - { url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296, upload-time = "2026-06-20T23:48:15.881Z" }, - { url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841, upload-time = "2026-06-20T23:48:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882, upload-time = "2026-06-20T23:48:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411, upload-time = "2026-06-20T23:48:20.315Z" }, - { url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607, upload-time = "2026-06-20T23:48:21.728Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367, upload-time = "2026-06-20T23:48:23.177Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176, upload-time = "2026-06-20T23:48:24.711Z" }, - { url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025, upload-time = "2026-06-20T23:48:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605, upload-time = "2026-06-20T23:48:27.643Z" }, - { url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508, upload-time = "2026-06-20T23:48:29.013Z" }, - { url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565, upload-time = "2026-06-20T23:48:30.429Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264, upload-time = "2026-06-20T23:48:31.807Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791, upload-time = "2026-06-20T23:48:33.482Z" }, - { url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399, upload-time = "2026-06-20T23:48:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461, upload-time = "2026-06-20T23:48:36.569Z" }, - { url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313, upload-time = "2026-06-20T23:48:38.175Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116, upload-time = "2026-06-20T23:48:39.804Z" }, - { url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668, upload-time = "2026-06-20T23:48:41.346Z" }, - { url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891, upload-time = "2026-06-20T23:48:43.056Z" }, - { url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537, upload-time = "2026-06-20T23:48:44.509Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005, upload-time = "2026-06-20T23:48:46.391Z" }, - { url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762, upload-time = "2026-06-20T23:48:47.964Z" }, - { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" }, - { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" }, - { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" }, - { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" }, - { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" }, - { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" }, - { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" }, - { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" }, - { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" }, - { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, -] - [[package]] name = "zipp" version = "4.1.0"