Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add point cloud export #80

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions glue_ar/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from .scatter_gltf import add_scatter_layer_gltf # noqa: F401
from .scatter_stl import add_scatter_layer_stl # noqa: F401
from .scatter_usd import add_scatter_layer_usd # noqa: F401
from .points_gltf import add_points_layer_gltf # noqa: F401
from .points_usd import add_points_layer_usd # noqa: F401
from .voxels import add_voxel_layers_gltf, add_voxel_layers_usd # noqa: F401
from .scatter_export_options import ARVispyScatterExportOptions # noqa: F401
from .volume_export_options import ARIsosurfaceExportOptions, ARVoxelExportOptions # noqa: F401
154 changes: 154 additions & 0 deletions glue_ar/common/points_gltf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from collections import defaultdict
from gltflib import AccessorType, BufferTarget, ComponentType, PrimitiveMode
from glue_vispy_viewers.common.viewer_state import Vispy3DViewerState
from glue_vispy_viewers.scatter.layer_state import ScatterLayerState

from glue_ar.common.export_options import ar_layer_export
from glue_ar.common.scatter import Scatter3DLayerState, ScatterLayerState3D, scatter_layer_mask
from glue_ar.gltf_utils import add_points_to_bytearray, index_mins, index_maxes
from glue_ar.utils import Bounds, NoneType, Viewer3DState, color_identifier, hex_to_components, \
layer_color, unique_id, xyz_bounds, xyz_for_layer
from glue_ar.common.gltf_builder import GLTFBuilder
from glue_ar.common.scatter_export_options import ARPointExportOptions

try:
from glue_jupyter.common.state3d import ViewerState3D
except ImportError:
ViewerState3D = NoneType


def add_points_layer_gltf(builder: GLTFBuilder,
viewer_state: Viewer3DState,
layer_state: ScatterLayerState3D,
bounds: Bounds,
clip_to_bounds: bool = True):

if layer_state is None:
return

Check warning on line 27 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L26-L27

Added lines #L26 - L27 were not covered by tests

bounds = xyz_bounds(viewer_state, with_resolution=False)

Check warning on line 29 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L29

Added line #L29 was not covered by tests

vispy_layer_state = isinstance(layer_state, ScatterLayerState)
color_mode_attr = "color_mode" if vispy_layer_state else "cmap_mode"
fixed_color = getattr(layer_state, color_mode_attr, "Fixed") == "Fixed"

Check warning on line 33 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L31-L33

Added lines #L31 - L33 were not covered by tests

mask = scatter_layer_mask(viewer_state, layer_state, bounds, clip_to_bounds)
data = xyz_for_layer(viewer_state, layer_state,

Check warning on line 36 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L35-L36

Added lines #L35 - L36 were not covered by tests
preserve_aspect=viewer_state.native_aspect,
mask=mask,
scaled=True)
data = data[:, [1, 2, 0]]

Check warning on line 40 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L40

Added line #L40 was not covered by tests

uri = f"layer_{unique_id()}.bin"

Check warning on line 42 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L42

Added line #L42 was not covered by tests

if fixed_color:
color = layer_color(layer_state)
color_components = hex_to_components(color)
builder.add_material(color=color_components, opacity=layer_state.alpha)

Check warning on line 47 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L44-L47

Added lines #L44 - L47 were not covered by tests

barr = bytearray()
add_points_to_bytearray(barr, data)

Check warning on line 50 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L49-L50

Added lines #L49 - L50 were not covered by tests

data_mins = index_mins(data)
data_maxes = index_maxes(data)

Check warning on line 53 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L52-L53

Added lines #L52 - L53 were not covered by tests

builder.add_buffer(byte_length=len(barr), uri=uri)
builder.add_buffer_view(

Check warning on line 56 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L55-L56

Added lines #L55 - L56 were not covered by tests
buffer=builder.buffer_count-1,
byte_length=len(barr),
byte_offset=0,
target=BufferTarget.ARRAY_BUFFER
)
builder.add_accessor(

Check warning on line 62 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L62

Added line #L62 was not covered by tests
buffer_view=builder.buffer_view_count-1,
component_type=ComponentType.FLOAT,
count=len(data),
type=AccessorType.VEC3,
mins=data_mins,
maxes=data_maxes,
)
builder.add_mesh(

Check warning on line 70 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L70

Added line #L70 was not covered by tests
position_accessor=builder.accessor_count-1,
material=builder.material_count-1,
mode=PrimitiveMode.POINTS
)
builder.add_file_resource(uri, data=barr)

Check warning on line 75 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L75

Added line #L75 was not covered by tests
else:
# If we don't have fixed colors, the idea is to make a different "mesh" for each different color used
# So first we need to run through the points and determine which color they have, and group ones with
# the same color together
points_by_color = defaultdict(list)
cmap = layer_state.cmap
cmap_attr = "cmap_attribute" if vispy_layer_state else "cmap_att"
cmap_att = getattr(layer_state, cmap_attr)
cmap_vals = layer_state.layer[cmap_att][mask]
crange = layer_state.cmap_vmax - layer_state.cmap_vmin
opacity = layer_state.alpha

Check warning on line 86 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L80-L86

Added lines #L80 - L86 were not covered by tests

for i, point in enumerate(data):
cval = cmap_vals[i]
normalized = max(min((cval - layer_state.cmap_vmin) / crange, 1), 0)
cindex = int(normalized * 255)
color = cmap(cindex)
points_by_color[color].append(point)

Check warning on line 93 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L88-L93

Added lines #L88 - L93 were not covered by tests

for color, points in points_by_color.items():
builder.add_material(color, opacity)
material_index = builder.material_count - 1

Check warning on line 97 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L95-L97

Added lines #L95 - L97 were not covered by tests

uri = f"layer_{unique_id()}_{color_identifier(color, opacity)}"

Check warning on line 99 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L99

Added line #L99 was not covered by tests

barr = bytearray()
add_points_to_bytearray(barr, points)
point_mins = index_mins(points)
point_maxes = index_maxes(points)

Check warning on line 104 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L101-L104

Added lines #L101 - L104 were not covered by tests

builder.add_buffer(byte_length=len(barr), uri=uri)
builder.add_buffer_view(

Check warning on line 107 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L106-L107

Added lines #L106 - L107 were not covered by tests
buffer=builder.buffer_count-1,
byte_length=len(barr),
byte_offset=0,
target=BufferTarget.ARRAY_BUFFER
)
builder.add_accessor(

Check warning on line 113 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L113

Added line #L113 was not covered by tests
buffer_view=builder.buffer_view_count-1,
component_type=ComponentType.FLOAT,
count=len(points),
type=AccessorType.VEC3,
mins=point_mins,
maxes=point_maxes
)
builder.add_mesh(

Check warning on line 121 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L121

Added line #L121 was not covered by tests
position_accessor=builder.accessor_count-1,
material=material_index,
mode=PrimitiveMode.POINTS,
)
builder.add_file_resource(uri, data=barr)

Check warning on line 126 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L126

Added line #L126 was not covered by tests


@ar_layer_export(ScatterLayerState, "Points", ARPointExportOptions, ("gltf", "glb"))
def add_vispy_points_layer_gltf(builder: GLTFBuilder,
viewer_state: Vispy3DViewerState,
layer_state: ScatterLayerState,
options: ARPointExportOptions,
bounds: Bounds,
clip_to_bounds: bool = True):
add_points_layer_gltf(builder=builder,

Check warning on line 136 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L136

Added line #L136 was not covered by tests
viewer_state=viewer_state,
layer_state=layer_state,
bounds=bounds,
clip_to_bounds=clip_to_bounds)


@ar_layer_export(Scatter3DLayerState, "Points", ARPointExportOptions, ("gltf", "glb"))
def add_ipyvolume_points_layer_gltf(builder: GLTFBuilder,
viewer_state: ViewerState3D,
layer_state: ScatterLayerState,
options: ARPointExportOptions,
bounds: Bounds,
clip_to_bounds: bool = True):
add_points_layer_gltf(builder=builder,

Check warning on line 150 in glue_ar/common/points_gltf.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_gltf.py#L150

Added line #L150 was not covered by tests
viewer_state=viewer_state,
layer_state=layer_state,
bounds=bounds,
clip_to_bounds=clip_to_bounds)
87 changes: 87 additions & 0 deletions glue_ar/common/points_usd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from glue_vispy_viewers.common.viewer_state import Vispy3DViewerState
from glue_vispy_viewers.scatter.layer_state import ScatterLayerState

from glue_ar.common.export_options import ar_layer_export
from glue_ar.common.scatter import Scatter3DLayerState, ScatterLayerState3D, scatter_layer_mask
from glue_ar.utils import Bounds, NoneType, Viewer3DState, hex_to_components, layer_color, \
unique_id, xyz_bounds, xyz_for_layer
from glue_ar.common.usd_builder import USDBuilder
from glue_ar.common.scatter_export_options import ARPointExportOptions

try:
from glue_jupyter.common.state3d import ViewerState3D
except ImportError:
ViewerState3D = NoneType


def add_points_layer_usd(builder: USDBuilder,
viewer_state: Viewer3DState,
layer_state: ScatterLayerState3D,
bounds: Bounds,
clip_to_bounds: bool = True):

if layer_state is None:
return

Check warning on line 24 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L23-L24

Added lines #L23 - L24 were not covered by tests

bounds = xyz_bounds(viewer_state, with_resolution=False)

Check warning on line 26 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L26

Added line #L26 was not covered by tests

vispy_layer_state = isinstance(layer_state, ScatterLayerState)
color_mode_attr = "color_mode" if vispy_layer_state else "cmap_mode"
fixed_color = getattr(layer_state, color_mode_attr, "Fixed") == "Fixed"

Check warning on line 30 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L28-L30

Added lines #L28 - L30 were not covered by tests

mask = scatter_layer_mask(viewer_state, layer_state, bounds, clip_to_bounds)
data = xyz_for_layer(viewer_state, layer_state,

Check warning on line 33 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L32-L33

Added lines #L32 - L33 were not covered by tests
preserve_aspect=viewer_state.native_aspect,
mask=mask,
scaled=True)
data = data[:, [1, 2, 0]]

Check warning on line 37 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L37

Added line #L37 was not covered by tests

identifier = f"layer_{unique_id()}"

Check warning on line 39 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L39

Added line #L39 was not covered by tests

if fixed_color:
color = layer_color(layer_state)
components = hex_to_components(color)[:3]
colors = [components for _ in range(data.shape[0])]

Check warning on line 44 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L41-L44

Added lines #L41 - L44 were not covered by tests
else:
cmap = layer_state.cmap
cmap_attr = "cmap_attribute" if vispy_layer_state else "cmap_att"
cmap_att = getattr(layer_state, cmap_attr)
cmap_vals = layer_state.layer[cmap_att][mask]
crange = layer_state.cmap_vmax - layer_state.cmap_vmin

Check warning on line 50 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L46-L50

Added lines #L46 - L50 were not covered by tests

def get_color(cval):
normalized = max(min((cval - layer_state.cmap_vmin) / crange, 1), 0)
cindex = int(normalized * 255)
return cmap(cindex)[:3]

Check warning on line 55 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L52-L55

Added lines #L52 - L55 were not covered by tests

colors = [get_color(cval) for cval in cmap_vals]

Check warning on line 57 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L57

Added line #L57 was not covered by tests

builder.add_points(data, colors, identifier)

Check warning on line 59 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L59

Added line #L59 was not covered by tests


@ar_layer_export(ScatterLayerState, "Points", ARPointExportOptions, ("usdz", "usdc", "usda"))
def add_vispy_points_layer_usd(builder: USDBuilder,
viewer_state: Vispy3DViewerState,
layer_state: ScatterLayerState,
options: ARPointExportOptions,
bounds: Bounds,
clip_to_bounds: bool = True):
add_points_layer_usd(builder=builder,

Check warning on line 69 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L69

Added line #L69 was not covered by tests
viewer_state=viewer_state,
layer_state=layer_state,
bounds=bounds,
clip_to_bounds=clip_to_bounds)


@ar_layer_export(Scatter3DLayerState, "Points", ARPointExportOptions, ("usdz", "usdc", "usda"))
def add_ipyvolume_points_layer_usd(builder: USDBuilder,
viewer_state: ViewerState3D,
layer_state: ScatterLayerState,
options: ARPointExportOptions,
bounds: Bounds,
clip_to_bounds: bool = True):
add_points_layer_usd(builder=builder,

Check warning on line 83 in glue_ar/common/points_usd.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/points_usd.py#L83

Added line #L83 was not covered by tests
viewer_state=viewer_state,
layer_state=layer_state,
bounds=bounds,
clip_to_bounds=clip_to_bounds)
4 changes: 4 additions & 0 deletions glue_ar/common/scatter_export_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ class ARVispyScatterExportOptions(State):

class ARIpyvolumeScatterExportOptions(State):
pass


class ARPointExportOptions(State):
pass
7 changes: 3 additions & 4 deletions glue_ar/common/scatter_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

from glue_ar.common.export_options import ar_layer_export
from glue_ar.common.scatter import IPYVOLUME_POINTS_GETTERS, IPYVOLUME_TRIANGLE_GETTERS, VECTOR_OFFSETS, PointsGetter, \
ScatterLayerState3D, box_points_getter, clip_vector_data, radius_for_scatter_layer, \
scatter_layer_mask, sizes_for_scatter_layer, sphere_points_getter
Scatter3DLayerState, ScatterLayerState3D, box_points_getter, clip_vector_data, \
radius_for_scatter_layer, scatter_layer_mask, sizes_for_scatter_layer, \
sphere_points_getter
from glue_ar.common.scatter_export_options import ARIpyvolumeScatterExportOptions, ARVispyScatterExportOptions
from glue_ar.common.usd_builder import USDBuilder
from glue_ar.common.shapes import cone_triangles, cone_points, cylinder_points, cylinder_triangles, \
Expand All @@ -20,10 +21,8 @@

try:
from glue_jupyter.common.state3d import ViewerState3D
from glue_jupyter.ipyvolume.scatter import Scatter3DLayerState
except ImportError:
ViewerState3D = NoneType
Scatter3DLayerState = NoneType


def add_vectors_usd(builder: USDBuilder,
Expand Down
4 changes: 2 additions & 2 deletions glue_ar/common/tests/test_base_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ def test_layer_change_state(self):
state = self.dialog.state

state.layer = "Scatter Data"
assert state.method_helper.choices == ["Scatter"]
assert state.method_helper.choices == ["Scatter", "Points"]
assert state.method == "Scatter"

state.layer = "Volume Data"
assert set(state.method_helper.choices) == {"Isosurface", "Voxel"}
assert state.method in {"Isosurface", "Voxel"}

state.layer = "Scatter Data"
assert state.method_helper.choices == ["Scatter"]
assert state.method_helper.choices == ["Scatter", "Points"]
assert state.method == "Scatter"

def test_method_settings_persistence(self):
Expand Down
38 changes: 32 additions & 6 deletions glue_ar/common/usd_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from os import extsep, remove
from os.path import splitext

from pxr import Usd, UsdGeom, UsdLux, UsdShade, UsdUtils
from pxr import Gf, Sdf, Usd, UsdGeom, UsdLux, UsdShade, UsdUtils
from typing import Dict, Iterable, Optional, Tuple
from glue_ar.common.scatter import Point

from glue_ar.registries import builder
from glue_ar.usd_utils import material_for_color, material_for_mesh, sanitize_path
Expand Down Expand Up @@ -33,7 +34,7 @@
self.default_prim = UsdGeom.Xform.Define(self.stage, self.default_prim_key).GetPrim()
self.stage.SetDefaultPrim(self.default_prim)

self._mesh_counts: Dict[str, int] = defaultdict(int)
self._geom_counts: Dict[str, int] = defaultdict(int)

light = UsdLux.RectLight.Define(self.stage, "/light")
light.CreateHeightAttr(-1)
Expand Down Expand Up @@ -70,12 +71,13 @@
This breaks the builder pattern but we'll potentially want this reference to it
for other meshes that we create.
"""

identifier = sanitize_path(identifier or unique_id())
count = self._mesh_counts[identifier]
count = self._geom_counts[identifier]
xform_key = f"{self.default_prim_key}/xform_{identifier}_{count}"
UsdGeom.Xform.Define(self.stage, xform_key)
mesh_key = f"{xform_key}/mesh_{identifier}_{count}"
self._mesh_counts[identifier] += 1
self._geom_counts[identifier] += 1
mesh = UsdGeom.Mesh.Define(self.stage, mesh_key)
mesh.CreateSubdivisionSchemeAttr().Set(UsdGeom.Tokens.none)
mesh.CreatePointsAttr(points)
Expand All @@ -95,11 +97,11 @@
identifier: Optional[str] = None) -> UsdGeom.Mesh:
prim = mesh.GetPrim()
identifier = sanitize_path(identifier or unique_id())
count = self._mesh_counts[identifier]
count = self._geom_counts[identifier]

Check warning on line 100 in glue_ar/common/usd_builder.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/usd_builder.py#L100

Added line #L100 was not covered by tests
xform_key = f"{self.default_prim_key}/xform_{identifier}_{count}"
UsdGeom.Xform.Define(self.stage, xform_key)
new_mesh_key = f"{xform_key}/mesh_{identifier}_{count}"
self._mesh_counts[identifier] += 1
self._geom_counts[identifier] += 1

Check warning on line 104 in glue_ar/common/usd_builder.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/usd_builder.py#L104

Added line #L104 was not covered by tests
new_mesh = UsdGeom.Mesh.Define(self.stage, new_mesh_key)
new_prim = new_mesh.GetPrim()

Expand All @@ -116,6 +118,30 @@

return mesh

def add_points(self,
points: Iterable[Point],
colors: Iterable[Tuple[int, int, int]],
identifier: Optional[str] = None) -> UsdGeom.Points:

identifier = identifier or unique_id()
identifier = self._sanitize(identifier)
count = self._geom_counts[identifier]
xform_key = f"{self.default_prim_key}/xform_{identifier}_{count}"
UsdGeom.Xform.Define(self.stage, xform_key)
points_key = f"{xform_key}/points_{identifier}_{count}"
self._geom_counts[identifier] += 1
geom = UsdGeom.Points.Define(self.stage, points_key)
point_vecs = [Gf.Vec3f(*point) for point in points]
geom.CreatePointsAttr(point_vecs)
widths_var = geom.CreateWidthsAttr()
widths = [1.0 for _ in points]
widths_var.Set(widths)
primvars = UsdGeom.PrimvarsAPI(geom.GetPrim())
colorvar = primvars.CreatePrimvar("displayColor", Sdf.ValueTypeNames.Color3f)
colorvar.Set([Gf.Vec3f(*tuple(c / 255 for c in color)) for color in colors])

Check warning on line 141 in glue_ar/common/usd_builder.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/usd_builder.py#L126-L141

Added lines #L126 - L141 were not covered by tests

return geom

Check warning on line 143 in glue_ar/common/usd_builder.py

View check run for this annotation

Codecov / codecov/patch

glue_ar/common/usd_builder.py#L143

Added line #L143 was not covered by tests

def export(self, filepath: str):
base, ext = splitext(filepath)
if ext == ".usdz":
Expand Down
Loading
Loading