Add comprehensive documentation for SnapForm extension
- Created development.md for development setup and licensing information. - Added features.md detailing functionalities and usage instructions for developers and users. - Introduced index.md to serve as a navigation hub for the documentation. - Developed outputs.md to explain output formats and their specifications. - Established service.md to describe the AI Service architecture and environment setup. - Created surface.md to outline the process of converting Image Empty to perspective meshes.
This commit is contained in:
@@ -25,12 +25,15 @@ def input_socket(node, name, fallback):
|
||||
|
||||
|
||||
def new_socket(interface, name, in_out, socket_type, parent=None):
|
||||
return interface.new_socket(
|
||||
socket = interface.new_socket(
|
||||
name=name,
|
||||
in_out=in_out,
|
||||
socket_type=socket_type,
|
||||
parent=parent,
|
||||
)
|
||||
if in_out == "INPUT" and hasattr(socket, "structure_type"):
|
||||
socket.structure_type = "SINGLE"
|
||||
return socket
|
||||
|
||||
|
||||
def new_compare(nodes, data_type, operation, location, value):
|
||||
@@ -361,8 +364,11 @@ def add_uv_reprojection(group, source, geometry, location):
|
||||
def build_group(
|
||||
group_name=GROUP_NAME,
|
||||
include_confidence=True,
|
||||
include_stretch=True,
|
||||
include_discontinuity=True,
|
||||
include_mask=False,
|
||||
discontinuity_name="Depth Continuity",
|
||||
discontinuity_attribute="o_depth_discontinuity",
|
||||
discontinuity_default=50.0,
|
||||
):
|
||||
group = bpy.data.node_groups.new(group_name, "GeometryNodeTree")
|
||||
group.is_modifier = True
|
||||
@@ -381,7 +387,7 @@ def build_group(
|
||||
"INPUT",
|
||||
"NodeSocketFloat",
|
||||
)
|
||||
resolution.default_value = 0.01
|
||||
resolution.default_value = 0.1
|
||||
resolution.min_value = 0.000001
|
||||
resolution.subtype = "DISTANCE"
|
||||
remesh_iterations = new_socket(
|
||||
@@ -403,22 +409,24 @@ def build_group(
|
||||
smooth_amount.min_value = 0.0
|
||||
smooth_amount.max_value = 1.0
|
||||
smooth_amount.subtype = "FACTOR"
|
||||
if include_mask:
|
||||
mask_socket = new_socket(
|
||||
interface,
|
||||
"Mask",
|
||||
"INPUT",
|
||||
"NodeSocketBool",
|
||||
)
|
||||
mask_socket.default_value = True
|
||||
mask_socket.structure_type = "SINGLE"
|
||||
|
||||
culling_panel = None
|
||||
if include_confidence or include_stretch:
|
||||
if include_confidence or include_discontinuity or include_mask:
|
||||
culling_panel = interface.new_panel(
|
||||
name="Culling",
|
||||
default_closed=False,
|
||||
)
|
||||
if include_discontinuity:
|
||||
discontinuity = new_socket(
|
||||
interface,
|
||||
discontinuity_name,
|
||||
"INPUT",
|
||||
"NodeSocketFloat",
|
||||
parent=culling_panel,
|
||||
)
|
||||
discontinuity.default_value = discontinuity_default
|
||||
discontinuity.min_value = 0.0
|
||||
discontinuity.max_value = 100.0
|
||||
discontinuity.subtype = "PERCENTAGE"
|
||||
if include_confidence:
|
||||
confidence = new_socket(
|
||||
interface,
|
||||
@@ -431,18 +439,15 @@ def build_group(
|
||||
confidence.min_value = 0.0
|
||||
confidence.max_value = 100.0
|
||||
confidence.subtype = "PERCENTAGE"
|
||||
if include_stretch:
|
||||
edge = new_socket(
|
||||
if include_mask:
|
||||
mask_socket = new_socket(
|
||||
interface,
|
||||
"Stretch",
|
||||
"Mask",
|
||||
"INPUT",
|
||||
"NodeSocketFloat",
|
||||
"NodeSocketBool",
|
||||
parent=culling_panel,
|
||||
)
|
||||
edge.default_value = 10.0
|
||||
edge.min_value = 0.0
|
||||
edge.max_value = 100.0
|
||||
edge.subtype = "PERCENTAGE"
|
||||
mask_socket.default_value = False
|
||||
|
||||
options_panel = interface.new_panel(
|
||||
name="Options",
|
||||
@@ -470,9 +475,13 @@ def build_group(
|
||||
(-1800.0, 800.0),
|
||||
{"Confidence"},
|
||||
)
|
||||
stretch_input = None
|
||||
if include_stretch:
|
||||
stretch_input = new_group_input(nodes, (-1800.0, 50.0), {"Stretch"})
|
||||
discontinuity_input = None
|
||||
if include_discontinuity:
|
||||
discontinuity_input = new_group_input(
|
||||
nodes,
|
||||
(-1800.0, 50.0),
|
||||
{discontinuity_name},
|
||||
)
|
||||
remesh_input = new_group_input(
|
||||
nodes,
|
||||
(-100.0, -500.0),
|
||||
@@ -524,54 +533,67 @@ def build_group(
|
||||
geometry = delete_confidence.outputs["Geometry"]
|
||||
|
||||
culled_geometry = geometry
|
||||
if include_stretch:
|
||||
edge_attribute = nodes.new("GeometryNodeInputNamedAttribute")
|
||||
edge_attribute.data_type = "FLOAT"
|
||||
edge_attribute.inputs["Name"].default_value = "o_edge_stretch"
|
||||
edge_attribute.location = (-1800.0, 350.0)
|
||||
edge_scale = nodes.new("ShaderNodeMath")
|
||||
edge_scale.operation = "MULTIPLY"
|
||||
edge_scale.inputs[1].default_value = 0.01
|
||||
edge_scale.location = (-1600.0, -50.0)
|
||||
edge_compare = new_compare(
|
||||
if include_discontinuity:
|
||||
discontinuity_data = nodes.new("GeometryNodeInputNamedAttribute")
|
||||
discontinuity_data.data_type = "FLOAT"
|
||||
discontinuity_data.inputs["Name"].default_value = discontinuity_attribute
|
||||
discontinuity_data.location = (-1800.0, 350.0)
|
||||
culling_scale = nodes.new("ShaderNodeMath")
|
||||
culling_scale.operation = "MULTIPLY"
|
||||
culling_scale.inputs[1].default_value = 0.01
|
||||
culling_scale.location = (-1650.0, -50.0)
|
||||
inverse_culling = nodes.new("ShaderNodeMath")
|
||||
inverse_culling.operation = "SUBTRACT"
|
||||
inverse_culling.inputs[0].default_value = 0.5
|
||||
inverse_culling.location = (-1450.0, -50.0)
|
||||
exponent_scale = nodes.new("ShaderNodeMath")
|
||||
exponent_scale.operation = "MULTIPLY"
|
||||
exponent_scale.inputs[1].default_value = 10.0
|
||||
exponent_scale.location = (-1250.0, -50.0)
|
||||
threshold_power = nodes.new("ShaderNodeMath")
|
||||
threshold_power.operation = "POWER"
|
||||
threshold_power.inputs[0].default_value = 2.0
|
||||
threshold_power.location = (-1050.0, -50.0)
|
||||
threshold_scale = nodes.new("ShaderNodeMath")
|
||||
threshold_scale.operation = "MULTIPLY"
|
||||
threshold_scale.inputs[1].default_value = 0.04
|
||||
threshold_scale.location = (-850.0, -50.0)
|
||||
discontinuity_compare = new_compare(
|
||||
nodes,
|
||||
"FLOAT",
|
||||
"GREATER_THAN",
|
||||
(-1200.0, 350.0),
|
||||
(-650.0, 350.0),
|
||||
0.0,
|
||||
)
|
||||
links.new(stretch_input.outputs["Stretch"], edge_scale.inputs[0])
|
||||
links.new(edge_attribute.outputs["Attribute"], input_socket(edge_compare, "A", 0))
|
||||
links.new(edge_scale.outputs[0], input_socket(edge_compare, "B", 1))
|
||||
links.new(
|
||||
discontinuity_input.outputs[discontinuity_name],
|
||||
culling_scale.inputs[0],
|
||||
)
|
||||
links.new(culling_scale.outputs[0], inverse_culling.inputs[1])
|
||||
links.new(inverse_culling.outputs[0], exponent_scale.inputs[0])
|
||||
links.new(exponent_scale.outputs[0], threshold_power.inputs[1])
|
||||
links.new(threshold_power.outputs[0], threshold_scale.inputs[0])
|
||||
links.new(
|
||||
discontinuity_data.outputs["Attribute"],
|
||||
input_socket(discontinuity_compare, "A", 0),
|
||||
)
|
||||
links.new(
|
||||
threshold_scale.outputs[0],
|
||||
input_socket(discontinuity_compare, "B", 1),
|
||||
)
|
||||
delete_geometry = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete_geometry.domain = "EDGE"
|
||||
delete_geometry.domain = "FACE"
|
||||
delete_geometry.location = (-350.0, 100.0)
|
||||
links.new(geometry, delete_geometry.inputs["Geometry"])
|
||||
links.new(edge_compare.outputs["Result"], delete_geometry.inputs["Selection"])
|
||||
links.new(
|
||||
discontinuity_compare.outputs["Result"],
|
||||
delete_geometry.inputs["Selection"],
|
||||
)
|
||||
culled_geometry = delete_geometry.outputs["Geometry"]
|
||||
|
||||
vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors")
|
||||
vertex_neighbors.location = (-550.0, -320.0)
|
||||
orphan_vertex = new_compare(
|
||||
nodes,
|
||||
"INT",
|
||||
"EQUAL",
|
||||
(-350.0, -320.0),
|
||||
0,
|
||||
)
|
||||
links.new(
|
||||
vertex_neighbors.outputs["Face Count"],
|
||||
input_socket(orphan_vertex, "A", 0),
|
||||
)
|
||||
delete_orphans = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete_orphans.domain = "POINT"
|
||||
delete_orphans.location = (-300.0, 100.0)
|
||||
links.new(culled_geometry, delete_orphans.inputs["Geometry"])
|
||||
links.new(orphan_vertex.outputs["Result"], delete_orphans.inputs["Selection"])
|
||||
|
||||
remeshed = add_remesh_zone(
|
||||
group,
|
||||
delete_orphans.outputs["Geometry"],
|
||||
culled_geometry,
|
||||
remesh_input.outputs["Resolution"],
|
||||
remesh_input.outputs["Relax Iterations"],
|
||||
topology_input.outputs["Fix Topology"],
|
||||
@@ -634,7 +656,11 @@ def build_group(
|
||||
def configure_moge_surface_group(group):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
mask_input = new_group_input(nodes, (-1800.0, 1050.0), {"Mask"})
|
||||
controls = new_group_input(
|
||||
nodes,
|
||||
(-1800.0, 1050.0),
|
||||
{"Mask"},
|
||||
)
|
||||
mask_attribute = nodes.new("GeometryNodeInputNamedAttribute")
|
||||
mask_attribute.data_type = "BOOLEAN"
|
||||
mask_attribute.inputs["Name"].default_value = "o_mask"
|
||||
@@ -646,7 +672,7 @@ def configure_moge_surface_group(group):
|
||||
use_mask.operation = "AND"
|
||||
use_mask.location = (-1150.0, 1050.0)
|
||||
links.new(mask_attribute.outputs["Attribute"], invert_mask.inputs[0])
|
||||
links.new(mask_input.outputs["Mask"], use_mask.inputs[0])
|
||||
links.new(controls.outputs["Mask"], use_mask.inputs[0])
|
||||
links.new(invert_mask.outputs[0], use_mask.inputs[1])
|
||||
first_processing = min(
|
||||
(
|
||||
@@ -667,7 +693,7 @@ def configure_moge_surface_group(group):
|
||||
links.new(use_mask.outputs[0], delete_mask.inputs["Selection"])
|
||||
links.new(delete_mask.outputs["Geometry"], geometry_input)
|
||||
group.description = (
|
||||
"Cull MoGe-2 stretched faces, repair topology, preserve boundaries, "
|
||||
"Cull MoGe-2 mask and depth-edge faces, repair topology, preserve boundaries, "
|
||||
"and reproject UVs."
|
||||
)
|
||||
group.use_fake_user = True
|
||||
@@ -1301,8 +1327,11 @@ def main():
|
||||
moge_surface_group = build_group(
|
||||
MOGE_SURFACE_GROUP_NAME,
|
||||
include_confidence=False,
|
||||
include_stretch=False,
|
||||
include_discontinuity=True,
|
||||
include_mask=True,
|
||||
discontinuity_name="Depth Continuity",
|
||||
discontinuity_attribute="o_depth_discontinuity",
|
||||
discontinuity_default=50.0,
|
||||
)
|
||||
configure_moge_surface_group(moge_surface_group)
|
||||
build_depth_plane_group()
|
||||
|
||||
@@ -21,14 +21,9 @@ def set_input(modifier, group, name, value):
|
||||
modifier[item.identifier] = value
|
||||
|
||||
|
||||
def add_face_attributes(mesh, count, background_values=None):
|
||||
def add_face_attributes(mesh, count):
|
||||
confidence = mesh.attributes.new("o_face_confidence", "FLOAT", "FACE")
|
||||
confidence.data.foreach_set("value", [1.0] * count)
|
||||
background = mesh.attributes.new("o_background_face", "BOOLEAN", "FACE")
|
||||
background.data.foreach_set(
|
||||
"value",
|
||||
background_values or [False] * count,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -44,6 +39,13 @@ def main():
|
||||
and item.name == "Geometry"
|
||||
)
|
||||
assert geometry_input.structure_type == "SINGLE"
|
||||
input_sockets = [
|
||||
item for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
||||
]
|
||||
assert all(item.structure_type == "SINGLE" for item in input_sockets)
|
||||
resolution = next(item for item in input_sockets if item.name == "Resolution")
|
||||
assert abs(resolution.default_value - 0.1) < 1e-6
|
||||
assert group.asset_data is None
|
||||
assert not any(node.bl_idname == "GeometryNodeGroup" for node in group.nodes)
|
||||
assert not any(node.bl_idname == "GeometryNodeTriangulate" for node in group.nodes)
|
||||
@@ -72,8 +74,8 @@ def main():
|
||||
assert not panels["Culling"].default_closed
|
||||
assert panels["Options"].default_closed
|
||||
assert [item.name for item in panels["Culling"].interface_items] == [
|
||||
"Depth Continuity",
|
||||
"Confidence",
|
||||
"Stretch",
|
||||
]
|
||||
assert [item.name for item in panels["Options"].interface_items] == [
|
||||
"Fix Topology",
|
||||
@@ -86,7 +88,10 @@ def main():
|
||||
and item.name == "Smooth"
|
||||
)
|
||||
assert smooth.default_value == 0.5
|
||||
for name, default_value in (("Confidence", 0.0), ("Stretch", 10.0)):
|
||||
for name, default_value in (
|
||||
("Confidence", 0.0),
|
||||
("Depth Continuity", 50.0),
|
||||
):
|
||||
item = next(
|
||||
item
|
||||
for item in panels["Culling"].interface_items
|
||||
@@ -105,11 +110,21 @@ def main():
|
||||
[],
|
||||
[(0, 2, 1), (1, 2, 3)],
|
||||
)
|
||||
edge_stretch = mesh.attributes.new("o_edge_stretch", "FLOAT", "EDGE")
|
||||
edge_stretch.data.foreach_set("value", [0.05] * len(mesh.edges))
|
||||
discontinuity = mesh.attributes.new(
|
||||
"o_depth_discontinuity",
|
||||
"FLOAT",
|
||||
"FACE",
|
||||
)
|
||||
discontinuity.data.foreach_set("value", [0.03] * len(mesh.polygons))
|
||||
uv = mesh.uv_layers.new(name="UVMap")
|
||||
uv.data.foreach_set("uv", [0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1])
|
||||
add_face_attributes(mesh, 2, [False, True])
|
||||
add_face_attributes(mesh, 2)
|
||||
assert not {
|
||||
"o_background",
|
||||
"o_background_face",
|
||||
"o_confidence",
|
||||
"o_confidence_percentile",
|
||||
}.intersection(mesh.attributes.keys())
|
||||
|
||||
obj = bpy.data.objects.new("Validation Object", mesh)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
@@ -130,13 +145,13 @@ def main():
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
set_input(modifier, group, "Stretch", 0.0)
|
||||
stretch_item = next(
|
||||
set_input(modifier, group, "Depth Continuity", 100.0)
|
||||
discontinuity_item = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Stretch"
|
||||
if item.item_type == "SOCKET" and item.name == "Depth Continuity"
|
||||
)
|
||||
assert modifier[stretch_item.identifier] == 0.0
|
||||
assert modifier[discontinuity_item.identifier] == 100.0
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
culled = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
|
||||
@@ -80,6 +80,7 @@ def main():
|
||||
]
|
||||
assert image_textures
|
||||
assert all(node.extension == "EXTEND" for node in image_textures)
|
||||
assert any(node.image.name == "MoGe Test.depth" for node in image_textures)
|
||||
geometry_textures = [
|
||||
node
|
||||
for node in geometry_group.nodes
|
||||
|
||||
@@ -51,6 +51,10 @@ def main():
|
||||
mask=mask,
|
||||
points=points,
|
||||
intrinsics=intrinsics,
|
||||
normal=np.broadcast_to(
|
||||
np.array([0.0, 0.0, -1.0], dtype=np.float32),
|
||||
(1, height, width, 3),
|
||||
),
|
||||
)
|
||||
prepare_moge_surface_archive(results_path, mesh_path)
|
||||
surface = build_image_mesh(
|
||||
@@ -60,7 +64,6 @@ def main():
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
)
|
||||
assert len(surface.data.vertices) > 0
|
||||
@@ -71,12 +74,16 @@ def main():
|
||||
if getattr(item, "in_out", None) == "INPUT"
|
||||
}
|
||||
assert "Stretch" not in inputs
|
||||
assert "Depth Continuity" in inputs
|
||||
assert "Mask" in inputs
|
||||
assert "Confidence" not in inputs
|
||||
assert "o_mask" in surface.data.attributes
|
||||
assert surface.data.attributes["o_mask"].domain == "FACE"
|
||||
assert not all(item.value for item in surface.data.attributes["o_mask"].data)
|
||||
attribute_names = set(surface.data.attributes.keys())
|
||||
assert attribute_names >= {"o_depth", "o_mask"}
|
||||
assert "o_depth_discontinuity" in attribute_names
|
||||
assert surface.data.attributes["o_depth_discontinuity"].data_type == "FLOAT"
|
||||
assert "o_edge_stretch" not in attribute_names
|
||||
assert not {
|
||||
"o_confidence",
|
||||
@@ -90,13 +97,35 @@ def main():
|
||||
for item in surface.modifiers[0].node_group.interface.items_tree
|
||||
if getattr(item, "in_out", None) == "INPUT" and item.name == "Mask"
|
||||
)
|
||||
assert mask_socket.default_value is True
|
||||
assert mask_socket.default_value is False
|
||||
assert mask_socket.structure_type == "SINGLE"
|
||||
assert not mask_socket.parent.name
|
||||
assert not any(
|
||||
item.item_type == "PANEL" and item.name == "Culling"
|
||||
assert mask_socket.parent.name == "Culling"
|
||||
depth_edge_socket = next(
|
||||
item
|
||||
for item in surface.modifiers[0].node_group.interface.items_tree
|
||||
if getattr(item, "in_out", None) == "INPUT"
|
||||
and item.name == "Depth Continuity"
|
||||
)
|
||||
assert depth_edge_socket.default_value == 50.0
|
||||
assert depth_edge_socket.subtype == "PERCENTAGE"
|
||||
assert depth_edge_socket.structure_type == "SINGLE"
|
||||
culling_panel = next(
|
||||
item for item in surface.modifiers[0].node_group.interface.items_tree
|
||||
if item.item_type == "PANEL" and item.name == "Culling"
|
||||
)
|
||||
assert [item.name for item in culling_panel.interface_items] == [
|
||||
"Depth Continuity",
|
||||
"Mask",
|
||||
]
|
||||
input_sockets = [
|
||||
item for item in surface.modifiers[0].node_group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
||||
]
|
||||
assert all(item.structure_type == "SINGLE" for item in input_sockets)
|
||||
resolution_socket = next(
|
||||
item for item in input_sockets if item.name == "Resolution"
|
||||
)
|
||||
assert abs(resolution_socket.default_value - 0.1) < 1e-6
|
||||
smooth_socket = next(
|
||||
item
|
||||
for item in surface.modifiers[0].node_group.interface.items_tree
|
||||
@@ -111,6 +140,26 @@ def main():
|
||||
assert MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME in bpy.data.node_groups
|
||||
material_group = bpy.data.node_groups[MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME]
|
||||
assert any(node.bl_idname == "ShaderNodeBump" for node in material_group.nodes)
|
||||
normal_map = next(
|
||||
node for node in material_group.nodes
|
||||
if node.bl_idname == "ShaderNodeNormalMap"
|
||||
)
|
||||
assert normal_map.space == "OBJECT"
|
||||
normal_strength = next(
|
||||
item for item in material_group.interface.items_tree
|
||||
if getattr(item, "in_out", None) == "INPUT"
|
||||
and item.name == "Normal Strength"
|
||||
)
|
||||
assert normal_strength.default_value == 1.0
|
||||
assert normal_strength.min_value == 0.0
|
||||
assert normal_map.inputs["Strength"].is_linked
|
||||
assert not surface.data.has_custom_normals
|
||||
normal_texture = next(
|
||||
node for node in surface.data.materials[0].node_tree.nodes
|
||||
if node.bl_idname == "ShaderNodeTexImage"
|
||||
and node.image.colorspace_settings.name == "Non-Color"
|
||||
)
|
||||
assert normal_texture.image.name == "MoGe Surface Test.normal"
|
||||
assert any(child.type == "CAMERA" for child in surface.children)
|
||||
print("MoGe-2 surface validation passed")
|
||||
|
||||
@@ -131,23 +180,24 @@ def validate_real_result(results_path, image_path):
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
)
|
||||
assert len(surface.data.vertices) > 100
|
||||
assert len(surface.data.polygons) > 100
|
||||
assert surface.modifiers[0].node_group.name == MOGE_NODE_GROUP_NAME
|
||||
assert MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME in bpy.data.node_groups
|
||||
assert surface.data.has_custom_normals
|
||||
normals = np.empty(len(surface.data.corner_normals) * 3, dtype=np.float32)
|
||||
surface.data.corner_normals.foreach_get("vector", normals)
|
||||
normals = normals.reshape(-1, 3)
|
||||
assert np.isfinite(normals).all()
|
||||
assert np.min(np.linalg.norm(normals, axis=1)) > 0.99
|
||||
assert not surface.data.has_custom_normals
|
||||
material_group = bpy.data.node_groups[MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME]
|
||||
assert any(node.bl_idname == "ShaderNodeBump" for node in material_group.nodes)
|
||||
normal_map = next(
|
||||
node for node in material_group.nodes
|
||||
if node.bl_idname == "ShaderNodeNormalMap"
|
||||
)
|
||||
assert normal_map.space == "OBJECT"
|
||||
assert normal_map.inputs["Strength"].is_linked
|
||||
attribute_names = set(surface.data.attributes.keys())
|
||||
assert {"o_depth", "o_mask"}.issubset(attribute_names)
|
||||
assert "o_depth_discontinuity" in attribute_names
|
||||
assert "o_edge_stretch" not in attribute_names
|
||||
assert not {
|
||||
"o_confidence",
|
||||
|
||||
Reference in New Issue
Block a user