mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-24 19:49:58 +00:00
clash - typing fixes and some refactor
This commit is contained in:
@@ -29,6 +29,7 @@ from math import radians
|
|||||||
from mathutils import Matrix, Vector
|
from mathutils import Matrix, Vector
|
||||||
from bonsai.bim.ifc import IfcStore
|
from bonsai.bim.ifc import IfcStore
|
||||||
from bonsai.bim.module.clash.decorator import ClashDecorator
|
from bonsai.bim.module.clash.decorator import ClashDecorator
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
class ExportClashSets(bpy.types.Operator, ExportHelper):
|
class ExportClashSets(bpy.types.Operator, ExportHelper):
|
||||||
@@ -127,7 +128,8 @@ class AddClashSource(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Clash.get_clash_props()
|
props = tool.Clash.get_clash_props()
|
||||||
clash_set = props.active_clash_set
|
clash_set = props.active_clash_set
|
||||||
source = getattr(clash_set, self.group).add()
|
assert clash_set
|
||||||
|
clash_set.get_clash_sources_group(self.group).add()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -142,7 +144,8 @@ class RemoveClashSource(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Clash.get_clash_props()
|
props = tool.Clash.get_clash_props()
|
||||||
clash_set = props.active_clash_set
|
clash_set = props.active_clash_set
|
||||||
getattr(clash_set, self.group).remove(self.index)
|
assert clash_set
|
||||||
|
clash_set.get_clash_sources_group(self.group).remove(self.index)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -159,7 +162,9 @@ class SelectClashSource(bpy.types.Operator, ImportHelper):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Clash.get_clash_props()
|
props = tool.Clash.get_clash_props()
|
||||||
clash_set = props.active_clash_set
|
clash_set = props.active_clash_set
|
||||||
getattr(clash_set, self.group)[self.index].name = self.filepath
|
assert clash_set
|
||||||
|
clash_source = clash_set.get_clash_sources_group(self.group)[self.index]
|
||||||
|
clash_source.name = self.filepath
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -191,9 +196,21 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
|||||||
bl_idname = "bim.execute_ifc_clash"
|
bl_idname = "bim.execute_ifc_clash"
|
||||||
bl_label = "Execute IFC Clash"
|
bl_label = "Execute IFC Clash"
|
||||||
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
|
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
|
||||||
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
|
|
||||||
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
|
filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
||||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
default="*.bcf;*.json", options={"HIDDEN"}
|
||||||
|
)
|
||||||
|
format: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||||
|
name="Format", items=[(i, i, "") for i in ("bcf", "json")]
|
||||||
|
)
|
||||||
|
filepath: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
||||||
|
subtype="FILE_PATH", options={"SKIP_SAVE"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
filter_glob: str
|
||||||
|
format: str
|
||||||
|
filepath: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename_ext(self) -> str:
|
def filename_ext(self) -> str:
|
||||||
@@ -225,11 +242,14 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
|||||||
|
|
||||||
if self.props.should_create_clash_snapshots:
|
if self.props.should_create_clash_snapshots:
|
||||||
|
|
||||||
def get_viewpoint_snapshot(viewpoint):
|
def get_viewpoint_snapshot(viewpoint) -> tuple[str, bytes]:
|
||||||
|
assert context.scene
|
||||||
|
|
||||||
camera = bpy.data.objects.get("IFC Clash Camera")
|
camera = bpy.data.objects.get("IFC Clash Camera")
|
||||||
if not camera:
|
if not camera:
|
||||||
camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera"))
|
camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera"))
|
||||||
context.scene.collection.objects.link(camera)
|
context.scene.collection.objects.link(camera)
|
||||||
|
assert isinstance(camera.data, bpy.types.Camera)
|
||||||
|
|
||||||
bcf_camera = viewpoint.visualization_info.perspective_camera
|
bcf_camera = viewpoint.visualization_info.perspective_camera
|
||||||
p = bcf_camera.camera_view_point
|
p = bcf_camera.camera_view_point
|
||||||
@@ -238,6 +258,7 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
|||||||
y = bcf_camera.camera_up_vector
|
y = bcf_camera.camera_up_vector
|
||||||
y = Vector([y.x, y.y, y.z])
|
y = Vector([y.x, y.y, y.z])
|
||||||
x = y.cross(z)
|
x = y.cross(z)
|
||||||
|
assert isinstance(x, Vector)
|
||||||
|
|
||||||
mat = Matrix(
|
mat = Matrix(
|
||||||
[
|
[
|
||||||
@@ -251,9 +272,9 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
|||||||
camera.matrix_world = mat
|
camera.matrix_world = mat
|
||||||
context.scene.camera = camera
|
context.scene.camera = camera
|
||||||
camera.data.angle = radians(60)
|
camera.data.angle = radians(60)
|
||||||
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
|
assert (space := tool.Blender.get_view3d_space()) and space.region_3d
|
||||||
area.spaces[0].region_3d.view_perspective = "CAMERA"
|
space.region_3d.view_perspective = "CAMERA"
|
||||||
area.spaces[0].shading.show_xray = True
|
space.shading.show_xray = True
|
||||||
context.scene.render.resolution_x = 480
|
context.scene.render.resolution_x = 480
|
||||||
context.scene.render.resolution_y = 270
|
context.scene.render.resolution_y = 270
|
||||||
context.scene.render.image_settings.file_format = "PNG"
|
context.scene.render.image_settings.file_format = "PNG"
|
||||||
@@ -271,7 +292,7 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
|||||||
if extension == ".json":
|
if extension == ".json":
|
||||||
tool.Clash.load_clash_sets(self.filepath)
|
tool.Clash.load_clash_sets(self.filepath)
|
||||||
tool.Clash.import_active_clashes()
|
tool.Clash.import_active_clashes()
|
||||||
self.report({"INFO"}, "Finished IFC clash.")
|
self.report({"INFO"}, f"IFC Clash results are saved to '{Path(self.filepath).name}'.")
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -348,8 +369,10 @@ class SelectClash(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
self.props = tool.Clash.get_clash_props()
|
self.props = tool.Clash.get_clash_props()
|
||||||
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
|
assert (active_clash := self.props.active_clash)
|
||||||
active_clash = self.props.active_clash
|
assert (active_clash_set := self.props.active_clash_set)
|
||||||
|
clash_set = tool.Clash.get_clash_set(active_clash_set.name)
|
||||||
|
assert clash_set
|
||||||
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
|
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
|
||||||
|
|
||||||
if not clash:
|
if not clash:
|
||||||
|
|||||||
@@ -35,9 +35,12 @@ from typing import TYPE_CHECKING, Literal, Union
|
|||||||
|
|
||||||
|
|
||||||
class ClashSource(PropertyGroup):
|
class ClashSource(PropertyGroup):
|
||||||
name: StringProperty(name="File")
|
name: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
name="File",
|
||||||
mode: EnumProperty(
|
description="Absolute filepath to existing .ifc file to use as a clash source.",
|
||||||
|
)
|
||||||
|
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") # pyright: ignore[reportRedeclaration]
|
||||||
|
mode: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||||
items=[
|
items=[
|
||||||
("a", "All Elements", "All elements will be used for clashing"),
|
("a", "All Elements", "All elements will be used for clashing"),
|
||||||
("i", "Include", "Only the selected elements are included for clashing"),
|
("i", "Include", "Only the selected elements are included for clashing"),
|
||||||
@@ -47,6 +50,7 @@ class ClashSource(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
name: str
|
||||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||||
mode: Literal["a", "i", "e"]
|
mode: Literal["a", "i", "e"]
|
||||||
|
|
||||||
@@ -56,7 +60,11 @@ class Clash(PropertyGroup):
|
|||||||
b_global_id: StringProperty(name="B")
|
b_global_id: StringProperty(name="B")
|
||||||
a_name: StringProperty(name="A Name")
|
a_name: StringProperty(name="A Name")
|
||||||
b_name: StringProperty(name="B Name")
|
b_name: StringProperty(name="B Name")
|
||||||
status: BoolProperty(name="Status", default=False)
|
status: BoolProperty(
|
||||||
|
name="Status",
|
||||||
|
description="Clash status, not stored anywhere - currently just displayed in UI for convenience.",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
a_global_id: str
|
a_global_id: str
|
||||||
@@ -99,6 +107,16 @@ class ClashSet(PropertyGroup):
|
|||||||
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
|
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
|
||||||
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
|
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
|
||||||
|
|
||||||
|
def get_clash_sources_group(
|
||||||
|
self, group: tool.Clash.ClashSourceGroup
|
||||||
|
) -> "bpy.types.bpy_prop_collection_idprop[ClashSource]":
|
||||||
|
return getattr(self, group)
|
||||||
|
|
||||||
|
def get_clash_sources(
|
||||||
|
self,
|
||||||
|
) -> "dict[tool.Clash.ClashSourceGroup, bpy.types.bpy_prop_collection_idprop[ClashSource]]":
|
||||||
|
return {g: self.get_clash_sources_group(g) for g in tool.Clash.CLASH_SOURCE_GROUP_LITERALS}
|
||||||
|
|
||||||
|
|
||||||
class SmartClashGroup(PropertyGroup):
|
class SmartClashGroup(PropertyGroup):
|
||||||
number: StringProperty(name="Number")
|
number: StringProperty(name="Number")
|
||||||
|
|||||||
@@ -16,11 +16,16 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
import bpy
|
import bpy
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
import bonsai.bim.helper
|
import bonsai.bim.helper
|
||||||
from bpy.types import Panel
|
from bpy.types import Panel
|
||||||
from bonsai.bim.module.clash.data import ClashData
|
from bonsai.bim.module.clash.data import ClashData
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bonsai.bim.module.clash.prop import BIMClashProperties, ClashSet, SmartClashGroup, Clash
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_ifcclash(Panel):
|
class BIM_PT_ifcclash(Panel):
|
||||||
@@ -36,6 +41,7 @@ class BIM_PT_ifcclash(Panel):
|
|||||||
if not ClashData.is_loaded:
|
if not ClashData.is_loaded:
|
||||||
ClashData.load()
|
ClashData.load()
|
||||||
|
|
||||||
|
assert self.layout
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
props = tool.Clash.get_clash_props()
|
props = tool.Clash.get_clash_props()
|
||||||
|
|
||||||
@@ -155,6 +161,7 @@ class BIM_PT_smart_clash_manager(Panel):
|
|||||||
bl_parent_id = "BIM_PT_clash_manager"
|
bl_parent_id = "BIM_PT_clash_manager"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
assert self.layout
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
props = tool.Clash.get_clash_props()
|
props = tool.Clash.get_clash_props()
|
||||||
|
|
||||||
@@ -188,8 +195,16 @@ class BIM_PT_smart_clash_manager(Panel):
|
|||||||
|
|
||||||
|
|
||||||
class BIM_UL_clash_sets(bpy.types.UIList):
|
class BIM_UL_clash_sets(bpy.types.UIList):
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
def draw_item(
|
||||||
ob = data
|
self,
|
||||||
|
context,
|
||||||
|
layout: bpy.types.UILayout,
|
||||||
|
data: BIMClashProperties,
|
||||||
|
item: ClashSet,
|
||||||
|
icon,
|
||||||
|
active_data,
|
||||||
|
active_propname,
|
||||||
|
) -> None:
|
||||||
if item:
|
if item:
|
||||||
layout.prop(item, "name", text="", emboss=False)
|
layout.prop(item, "name", text="", emboss=False)
|
||||||
else:
|
else:
|
||||||
@@ -197,8 +212,16 @@ class BIM_UL_clash_sets(bpy.types.UIList):
|
|||||||
|
|
||||||
|
|
||||||
class BIM_UL_smart_groups(bpy.types.UIList):
|
class BIM_UL_smart_groups(bpy.types.UIList):
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
def draw_item(
|
||||||
ob = data
|
self,
|
||||||
|
context,
|
||||||
|
layout: bpy.types.UILayout,
|
||||||
|
data: BIMClashProperties,
|
||||||
|
item: SmartClashGroup,
|
||||||
|
icon,
|
||||||
|
active_data,
|
||||||
|
active_propname,
|
||||||
|
) -> None:
|
||||||
if item:
|
if item:
|
||||||
layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0)
|
layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0)
|
||||||
else:
|
else:
|
||||||
@@ -206,7 +229,16 @@ class BIM_UL_smart_groups(bpy.types.UIList):
|
|||||||
|
|
||||||
|
|
||||||
class BIM_UL_clashes(bpy.types.UIList):
|
class BIM_UL_clashes(bpy.types.UIList):
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
def draw_item(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
layout: bpy.types.UILayout,
|
||||||
|
data: BIMClashProperties,
|
||||||
|
item: Clash,
|
||||||
|
icon,
|
||||||
|
active_data,
|
||||||
|
active_propname,
|
||||||
|
) -> None:
|
||||||
if item:
|
if item:
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text=str(item.a_name), translate=False, icon="NONE", icon_value=0)
|
row.label(text=str(item.a_name), translate=False, icon="NONE", icon_value=0)
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import bonsai.tool as tool
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
from ifcclash import ifcclash
|
from ifcclash import ifcclash
|
||||||
from typing import TYPE_CHECKING, Union
|
from ifcclash.ifcclash import ClashSource
|
||||||
|
from typing import TYPE_CHECKING, Union, Literal, get_args
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bonsai.bim.module.clash.prop import BIMClashProperties
|
from bonsai.bim.module.clash.prop import BIMClashProperties
|
||||||
@@ -38,16 +39,19 @@ class Clash(bonsai.core.tool.Clash):
|
|||||||
def get_clash_props(cls) -> BIMClashProperties:
|
def get_clash_props(cls) -> BIMClashProperties:
|
||||||
return bpy.context.scene.BIMClashProperties
|
return bpy.context.scene.BIMClashProperties
|
||||||
|
|
||||||
|
ClashSourceGroup = Literal["a", "b"]
|
||||||
|
CLASH_SOURCE_GROUP_LITERALS = ("a", "b")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_clash_sets(cls) -> list[ifcclash.ClashSet]:
|
def export_clash_sets(cls) -> list[ifcclash.ClashSet]:
|
||||||
clash_sets: list[ifcclash.ClashSet] = []
|
clash_sets: list[ifcclash.ClashSet] = []
|
||||||
props = cls.get_clash_props()
|
props = cls.get_clash_props()
|
||||||
for clash_set in props.clash_sets:
|
for clash_set in props.clash_sets:
|
||||||
a = []
|
a: list[ClashSource] = []
|
||||||
b = []
|
b: list[ClashSource] = []
|
||||||
for ab in ["a", "b"]:
|
for ab, ab_data in clash_set.get_clash_sources().items():
|
||||||
for data in getattr(clash_set, ab):
|
for data in ab_data:
|
||||||
clash_source = {"file": data.name}
|
clash_source: ClashSource = {"file": data.name}
|
||||||
query = tool.Search.export_filter_query(data.filter_groups)
|
query = tool.Search.export_filter_query(data.filter_groups)
|
||||||
if query and data.mode != "a":
|
if query and data.mode != "a":
|
||||||
clash_source["selector"] = query
|
clash_source["selector"] = query
|
||||||
@@ -96,13 +100,15 @@ class Clash(bonsai.core.tool.Clash):
|
|||||||
clash_set.clashes.clear()
|
clash_set.clashes.clear()
|
||||||
result = tool.Clash.get_clash_set(clash_set.name)
|
result = tool.Clash.get_clash_set(clash_set.name)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]):
|
if "clashes" not in result:
|
||||||
|
return
|
||||||
|
for clash in sorted(result["clashes"].values(), key=lambda x: x["distance"]):
|
||||||
blender_clash = clash_set.clashes.add()
|
blender_clash = clash_set.clashes.add()
|
||||||
blender_clash.a_global_id = clash["a_global_id"]
|
blender_clash.a_global_id = clash["a_global_id"]
|
||||||
blender_clash.b_global_id = clash["b_global_id"]
|
blender_clash.b_global_id = clash["b_global_id"]
|
||||||
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
|
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
|
||||||
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
|
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
|
||||||
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
|
blender_clash.status = False if not "status" in clash else clash["status"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_clash_sets(cls, fn: str) -> None:
|
def load_clash_sets(cls, fn: str) -> None:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import ifcopenshell
|
|||||||
import ifcopenshell.geom
|
import ifcopenshell.geom
|
||||||
import ifcopenshell.util.selector
|
import ifcopenshell.util.selector
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from typing import Literal, TypedDict
|
from typing import Literal, TypedDict, Union
|
||||||
from typing_extensions import NotRequired
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
|
|
||||||
@@ -206,6 +206,7 @@ class Clasher:
|
|||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
def export(self) -> None:
|
def export(self) -> None:
|
||||||
|
"""Save clash results to ``settings.output``."""
|
||||||
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
|
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
|
||||||
return self.export_bcfxml()
|
return self.export_bcfxml()
|
||||||
self.export_json()
|
self.export_json()
|
||||||
@@ -230,12 +231,12 @@ class Clasher:
|
|||||||
suffix = f".{i}" if i else ""
|
suffix = f".{i}" if i else ""
|
||||||
bcfxml.save(f"{self.settings.output}{suffix}")
|
bcfxml.save(f"{self.settings.output}{suffix}")
|
||||||
|
|
||||||
def get_viewpoint_snapshot(self, viewpoint) -> None:
|
def get_viewpoint_snapshot(self, viewpoint) -> Union[None, tuple[str, bytes]]:
|
||||||
# Possible to overload this function in a GUI application if used as a library.
|
# Possible to overload this function in a GUI application if used as a library.
|
||||||
# Should return a tuple of (filename, bytes).
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def export_json(self) -> None:
|
def export_json(self) -> None:
|
||||||
|
"""Saved clash results as ``list[ClashSet]``."""
|
||||||
clash_sets = self.clash_sets.copy()
|
clash_sets = self.clash_sets.copy()
|
||||||
for clash_set in clash_sets:
|
for clash_set in clash_sets:
|
||||||
for source in clash_set["a"]:
|
for source in clash_set["a"]:
|
||||||
|
|||||||
Reference in New Issue
Block a user