mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-07 08:21:42 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a072c72666 |
@@ -70,6 +70,7 @@ modules = {
|
||||
"pset": None,
|
||||
"qto": None,
|
||||
"classification": None,
|
||||
"license": None,
|
||||
"library": None,
|
||||
"constraint": None,
|
||||
"document": None,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# AI-assisted development tool was used in writing this file.
|
||||
|
||||
import bpy
|
||||
|
||||
from . import operator, prop, ui
|
||||
|
||||
classes = (
|
||||
operator.DisableEditingObjectLicense,
|
||||
operator.DisableEditingProjectLicense,
|
||||
operator.EditObjectLicense,
|
||||
operator.EditProjectLicense,
|
||||
operator.EnableEditingObjectLicense,
|
||||
operator.EnableEditingProjectLicense,
|
||||
operator.RemoveObjectLicense,
|
||||
operator.RemoveProjectLicense,
|
||||
prop.BIMLicenseProperties,
|
||||
ui.BIM_PT_object_license,
|
||||
ui.BIM_PT_project_license,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMLicenseProperties = bpy.props.PointerProperty(type=prop.BIMLicenseProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMLicenseProperties
|
||||
@@ -0,0 +1,77 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# AI-assisted development tool was used in writing this file.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
import bpy
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
ProjectLicenseData.is_loaded = False
|
||||
ObjectLicenseData.is_loaded = False
|
||||
|
||||
|
||||
class ProjectLicenseData:
|
||||
data: dict[str, Any] = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["license"] = cls.license()
|
||||
|
||||
@classmethod
|
||||
def license(cls) -> Optional[dict]:
|
||||
ifc = tool.Ifc.get()
|
||||
if not ifc:
|
||||
return None
|
||||
projects = ifc.by_type("IfcProject")
|
||||
if not projects:
|
||||
return None
|
||||
return tool.License.get_pset(projects[0])
|
||||
|
||||
|
||||
class ObjectLicenseData:
|
||||
data: dict[str, Any] = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["license"] = None
|
||||
cls.data["inherited_from"] = None
|
||||
cls.data["inherited_from_type"] = None
|
||||
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
|
||||
pset, source = tool.License.get_effective_pset(element)
|
||||
cls.data["license"] = pset
|
||||
if source is not None and source.id() != element.id():
|
||||
cls.data["inherited_from"] = getattr(source, "Name", None) or source.is_a()
|
||||
cls.data["inherited_from_type"] = source.is_a()
|
||||
@@ -0,0 +1,200 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# AI-assisted development tool was used in writing this file.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
import bonsai.bim.handler
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.license.data import ObjectLicenseData, ProjectLicenseData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.license.prop import BIMLicenseProperties
|
||||
|
||||
|
||||
def _get_props() -> BIMLicenseProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMLicenseProperties # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _populate_props_from_pset(props: BIMLicenseProperties, pset: dict) -> None:
|
||||
props.spdx_license_identifier = pset.get("SpdxLicenseIdentifier", "") or ""
|
||||
props.copyright_notice = pset.get("CopyrightNotice", "") or ""
|
||||
props.attribution_text = pset.get("AttributionText", "") or ""
|
||||
props.source_url = pset.get("SourceUrl", "") or ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Project-level license
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EnableEditingProjectLicense(Operator):
|
||||
bl_idname = "bim.enable_editing_project_license"
|
||||
bl_label = "Edit Project License"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = _get_props()
|
||||
existing = ProjectLicenseData.data.get("license")
|
||||
if existing:
|
||||
_populate_props_from_pset(props, existing)
|
||||
props.is_editing = True
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingProjectLicense(Operator):
|
||||
bl_idname = "bim.disable_editing_project_license"
|
||||
bl_label = "Cancel"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
_get_props().is_editing = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditProjectLicense(Operator):
|
||||
bl_idname = "bim.edit_project_license"
|
||||
bl_label = "Save Project License"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
file = tool.Ifc.get()
|
||||
projects = file.by_type("IfcProject")
|
||||
if not projects:
|
||||
return {"CANCELLED"}
|
||||
props = _get_props()
|
||||
tool.License.set_license(
|
||||
file,
|
||||
projects[0],
|
||||
spdx_id=props.spdx_license_identifier,
|
||||
copyright_notice=props.copyright_notice,
|
||||
attribution_text=props.attribution_text,
|
||||
source_url=props.source_url,
|
||||
)
|
||||
props.is_editing = False
|
||||
ProjectLicenseData.is_loaded = False
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveProjectLicense(Operator):
|
||||
bl_idname = "bim.remove_project_license"
|
||||
bl_label = "Remove Project License"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
file = tool.Ifc.get()
|
||||
projects = file.by_type("IfcProject")
|
||||
if not projects:
|
||||
return {"CANCELLED"}
|
||||
tool.License.remove_license(file, projects[0])
|
||||
ProjectLicenseData.is_loaded = False
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Object-level license
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EnableEditingObjectLicense(Operator):
|
||||
bl_idname = "bim.enable_editing_object_license"
|
||||
bl_label = "Edit Object License"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = _get_props()
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return {"CANCELLED"}
|
||||
existing = tool.License.get_pset(element)
|
||||
if existing:
|
||||
_populate_props_from_pset(props, existing)
|
||||
else:
|
||||
# Pre-populate from effective (inherited) license as a convenience
|
||||
eff, _ = tool.License.get_effective_pset(element)
|
||||
if eff:
|
||||
_populate_props_from_pset(props, eff)
|
||||
props.is_editing = True
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingObjectLicense(Operator):
|
||||
bl_idname = "bim.disable_editing_object_license"
|
||||
bl_label = "Cancel"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
_get_props().is_editing = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditObjectLicense(Operator):
|
||||
bl_idname = "bim.edit_object_license"
|
||||
bl_label = "Save Object License"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return {"CANCELLED"}
|
||||
props = _get_props()
|
||||
tool.License.set_license(
|
||||
tool.Ifc.get(),
|
||||
element,
|
||||
spdx_id=props.spdx_license_identifier,
|
||||
copyright_notice=props.copyright_notice,
|
||||
attribution_text=props.attribution_text,
|
||||
source_url=props.source_url,
|
||||
)
|
||||
props.is_editing = False
|
||||
ObjectLicenseData.is_loaded = False
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveObjectLicense(Operator):
|
||||
bl_idname = "bim.remove_object_license"
|
||||
bl_label = "Remove Object License"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return {"CANCELLED"}
|
||||
tool.License.remove_license(tool.Ifc.get(), element)
|
||||
ObjectLicenseData.is_loaded = False
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# AI-assisted development tool was used in writing this file.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bpy.props import BoolProperty, EnumProperty, StringProperty
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
from bonsai.tool.license import SPDX_ENUM_ITEMS
|
||||
|
||||
|
||||
class BIMLicenseProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
spdx_license_identifier: EnumProperty(
|
||||
name="License",
|
||||
items=SPDX_ENUM_ITEMS,
|
||||
description="SPDX license identifier",
|
||||
)
|
||||
copyright_notice: StringProperty(name="Copyright Notice", description='e.g. "© 2026 Acme Architecture Ltd"')
|
||||
attribution_text: StringProperty(name="Attribution Text", description="Text to use when crediting this work")
|
||||
source_url: StringProperty(name="Source URL", description="URL to the original source or license text")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
spdx_license_identifier: str
|
||||
copyright_notice: str
|
||||
attribution_text: str
|
||||
source_url: str
|
||||
@@ -0,0 +1,137 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# AI-assisted development tool was used in writing this file.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bpy
|
||||
from bpy.types import Panel
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.license.data import ObjectLicenseData, ProjectLicenseData
|
||||
|
||||
|
||||
def _draw_license_fields(layout: bpy.types.UILayout, props) -> None:
|
||||
layout.prop(props, "spdx_license_identifier")
|
||||
layout.prop(props, "copyright_notice")
|
||||
layout.prop(props, "attribution_text")
|
||||
layout.prop(props, "source_url")
|
||||
|
||||
|
||||
def _draw_license_display(layout: bpy.types.UILayout, pset: dict) -> None:
|
||||
spdx = pset.get("SpdxLicenseIdentifier") or ""
|
||||
notice = pset.get("CopyrightNotice") or ""
|
||||
attribution = pset.get("AttributionText") or ""
|
||||
source = pset.get("SourceUrl") or ""
|
||||
if spdx:
|
||||
row = layout.row()
|
||||
row.label(text=spdx, icon="COPYDOWN")
|
||||
if notice:
|
||||
row = layout.row()
|
||||
row.label(text=notice, icon="USER")
|
||||
if attribution:
|
||||
row = layout.row()
|
||||
row.label(text=attribution, icon="INFO")
|
||||
if source:
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Source", icon="URL")
|
||||
row.operator("bim.open_uri", text=source, icon="LINKED").uri = source
|
||||
|
||||
|
||||
class BIM_PT_project_license(Panel):
|
||||
bl_label = "License"
|
||||
bl_idname = "BIM_PT_project_license"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
|
||||
def draw(self, context):
|
||||
if not ProjectLicenseData.is_loaded:
|
||||
ProjectLicenseData.load()
|
||||
|
||||
layout = self.layout
|
||||
props = context.scene.BIMLicenseProperties # type: ignore[attr-defined]
|
||||
pset = ProjectLicenseData.data["license"]
|
||||
|
||||
if props.is_editing:
|
||||
_draw_license_fields(layout, props)
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.edit_project_license", text="Save", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_project_license", text="", icon="CANCEL")
|
||||
elif pset:
|
||||
_draw_license_display(layout, pset)
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.enable_editing_project_license", text="Edit", icon="GREASEPENCIL")
|
||||
row.operator("bim.remove_project_license", text="", icon="X")
|
||||
else:
|
||||
row = layout.row()
|
||||
row.label(text="No license set", icon="QUESTION")
|
||||
row.operator("bim.enable_editing_project_license", text="Set License", icon="ADD")
|
||||
|
||||
|
||||
class BIM_PT_object_license(Panel):
|
||||
bl_label = "License"
|
||||
bl_idname = "BIM_PT_object_license"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_tab_object_metadata"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (obj := tool.Blender.get_active_object()) and tool.Ifc.get_entity(obj)
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectLicenseData.is_loaded:
|
||||
ObjectLicenseData.load()
|
||||
|
||||
layout = self.layout
|
||||
props = context.scene.BIMLicenseProperties # type: ignore[attr-defined]
|
||||
pset = ObjectLicenseData.data["license"]
|
||||
inherited_from = ObjectLicenseData.data["inherited_from"]
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj) if obj else None
|
||||
has_own_pset = bool(element and tool.License.get_pset(element))
|
||||
|
||||
if props.is_editing:
|
||||
_draw_license_fields(layout, props)
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.edit_object_license", text="Save", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_object_license", text="", icon="CANCEL")
|
||||
elif pset:
|
||||
if inherited_from:
|
||||
row = layout.row()
|
||||
row.label(text=f"Inherited from: {inherited_from}", icon="LINKED")
|
||||
_draw_license_display(layout, pset)
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.enable_editing_object_license", text="Override", icon="GREASEPENCIL")
|
||||
if has_own_pset:
|
||||
row.operator("bim.remove_object_license", text="", icon="X")
|
||||
else:
|
||||
row = layout.row()
|
||||
row.label(text="No license set", icon="QUESTION")
|
||||
row.operator("bim.enable_editing_object_license", text="Set License", icon="ADD")
|
||||
@@ -629,14 +629,16 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.file = tool.Ifc.get()
|
||||
library_file = IfcStore.library_file
|
||||
assert library_file
|
||||
library_element = library_file.by_id(self.definition)
|
||||
element = ifcopenshell.api.project.append_asset(
|
||||
self.file,
|
||||
library=library_file,
|
||||
element=library_file.by_id(self.definition),
|
||||
element=library_element,
|
||||
assume_asset_uniqueness_by_name=self.assume_unique_by_name,
|
||||
)
|
||||
if not element:
|
||||
return {"FINISHED"}
|
||||
tool.License.inherit_library_license(self.file, element, library_file, library_element)
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
self.import_type_from_ifc(element, context)
|
||||
elif element.is_a("IfcProduct"):
|
||||
@@ -2539,6 +2541,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement):
|
||||
library=linked_ifc_file,
|
||||
element=element_to_append,
|
||||
)
|
||||
tool.License.inherit_library_license(tool.Ifc.get(), element, linked_ifc_file, element_to_append)
|
||||
self.import_product_from_ifc(element, context)
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type and tool.Ifc.get_object(element_type) is None:
|
||||
|
||||
@@ -190,6 +190,15 @@ class Classification:
|
||||
def set_location(cls, classification): pass
|
||||
|
||||
|
||||
@interface
|
||||
class License:
|
||||
def get_pset(cls, element): pass
|
||||
def get_effective_pset(cls, element): pass
|
||||
def set_license(cls, file, element, spdx_id, copyright_notice, attribution_text, source_url): pass
|
||||
def remove_license(cls, file, element): pass
|
||||
def inherit_library_license(cls, file, element, library, library_element): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Collector:
|
||||
def assign(cls, obj, should_clean_users_collection=False): pass
|
||||
|
||||
@@ -44,6 +44,7 @@ from bonsai.tool.group import Group
|
||||
from bonsai.tool.ifc import Ifc
|
||||
from bonsai.tool.ifcgit import IfcGit, IfcGitRepo
|
||||
from bonsai.tool.layer import Layer
|
||||
from bonsai.tool.license import License
|
||||
from bonsai.tool.library import Library
|
||||
from bonsai.tool.loader import Loader
|
||||
from bonsai.tool.material import Material
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# AI-assisted development tool was used in writing this file.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
|
||||
import bonsai.core.tool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
PSET_NAME = "BBIM_LicenseInformation"
|
||||
|
||||
# Curated SPDX identifiers appropriate for construction documentation.
|
||||
# Format: (identifier, label, description)
|
||||
SPDX_LICENSES: list[tuple[str, str, str]] = [
|
||||
# Open content
|
||||
("CC0-1.0", "CC0 1.0", "Public domain dedication — no rights reserved"),
|
||||
("CC-BY-4.0", "CC BY 4.0", "Attribution required"),
|
||||
("CC-BY-SA-4.0", "CC BY-SA 4.0", "Attribution + share-alike (copyleft)"),
|
||||
("CC-BY-ND-4.0", "CC BY-ND 4.0", "Attribution, no derivatives"),
|
||||
("CC-BY-NC-4.0", "CC BY-NC 4.0", "Attribution, non-commercial only"),
|
||||
("CC-BY-NC-SA-4.0", "CC BY-NC-SA 4.0", "Attribution, non-commercial, share-alike"),
|
||||
# Data
|
||||
("ODbL-1.0", "ODbL 1.0", "Open Database License — for data-heavy BIM content"),
|
||||
# Software / parametric content
|
||||
("MIT", "MIT", "Permissive — suitable for parametric/procedural content"),
|
||||
("Apache-2.0", "Apache 2.0", "Permissive with patent grant"),
|
||||
("LGPL-2.1-or-later", "LGPL 2.1+", "Weak copyleft (IfcOpenShell's own license)"),
|
||||
# Proprietary
|
||||
("LicenseRef-Proprietary", "Proprietary", "All rights reserved — no reuse without permission"),
|
||||
("LicenseRef-AllRightsReserved", "All Rights Reserved", "Explicit all-rights-reserved declaration"),
|
||||
]
|
||||
|
||||
SPDX_ENUM_ITEMS: list[tuple[str, str, str]] = [(id_, label, desc) for id_, label, desc in SPDX_LICENSES]
|
||||
|
||||
|
||||
def _get_pset_from_element(element: ifcopenshell.entity_instance) -> Optional[dict]:
|
||||
"""Return raw pset dict from this element only, no inheritance."""
|
||||
return ifcopenshell.util.element.get_pset(element, PSET_NAME, should_inherit=False)
|
||||
|
||||
|
||||
def _walk_up(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
"""Return ancestor chain from element up to IfcProject (inclusive)."""
|
||||
ancestors: list[ifcopenshell.entity_instance] = []
|
||||
current = element
|
||||
seen: set[int] = set()
|
||||
while current is not None and current.id() not in seen:
|
||||
seen.add(current.id())
|
||||
ancestors.append(current)
|
||||
if current.is_a("IfcProject"):
|
||||
break
|
||||
# Spatial containment
|
||||
container = ifcopenshell.util.element.get_container(current, should_get_direct=True)
|
||||
if container is not None:
|
||||
current = container
|
||||
continue
|
||||
# Aggregation (e.g. element inside assembly, storey inside building)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(current)
|
||||
if aggregate is not None:
|
||||
current = aggregate
|
||||
continue
|
||||
# Type objects may declare themselves to a project/library context
|
||||
if hasattr(current, "HasContext"):
|
||||
for rel in current.HasContext:
|
||||
current = rel.RelatingContext
|
||||
break
|
||||
else:
|
||||
break
|
||||
continue
|
||||
break
|
||||
return ancestors
|
||||
|
||||
|
||||
class License(bonsai.core.tool.License):
|
||||
@classmethod
|
||||
def get_pset(cls, element: ifcopenshell.entity_instance) -> Optional[dict]:
|
||||
"""Return BBIM_LicenseInformation pset dict if directly on this element, else None."""
|
||||
return _get_pset_from_element(element)
|
||||
|
||||
@classmethod
|
||||
def get_effective_pset(
|
||||
cls, element: ifcopenshell.entity_instance
|
||||
) -> tuple[Optional[dict], Optional[ifcopenshell.entity_instance]]:
|
||||
"""Walk up the spatial/aggregation hierarchy returning (pset_dict, source_entity).
|
||||
|
||||
The source entity is the element where the pset was actually found, so the
|
||||
UI can indicate whether the license was inherited. Returns (None, None) if
|
||||
no license is found anywhere in the chain.
|
||||
"""
|
||||
for ancestor in _walk_up(element):
|
||||
pset = _get_pset_from_element(ancestor)
|
||||
if pset:
|
||||
return pset, ancestor
|
||||
return None, None
|
||||
|
||||
@classmethod
|
||||
def set_license(
|
||||
cls,
|
||||
file: ifcopenshell.file,
|
||||
element: ifcopenshell.entity_instance,
|
||||
spdx_id: str,
|
||||
copyright_notice: str,
|
||||
attribution_text: str = "",
|
||||
source_url: str = "",
|
||||
) -> None:
|
||||
"""Create or update BBIM_LicenseInformation on element."""
|
||||
props: dict = {"SpdxLicenseIdentifier": spdx_id, "CopyrightNotice": copyright_notice}
|
||||
if attribution_text:
|
||||
props["AttributionText"] = attribution_text
|
||||
if source_url:
|
||||
props["SourceUrl"] = source_url
|
||||
|
||||
existing = cls._get_pset_entity(file, element)
|
||||
if existing:
|
||||
ifcopenshell.api.pset.edit_pset(file, pset=existing, properties=props)
|
||||
else:
|
||||
pset = ifcopenshell.api.pset.add_pset(file, product=element, name=PSET_NAME)
|
||||
ifcopenshell.api.pset.edit_pset(file, pset=pset, properties=props)
|
||||
|
||||
@classmethod
|
||||
def remove_license(cls, file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> None:
|
||||
"""Remove BBIM_LicenseInformation pset from element."""
|
||||
existing = cls._get_pset_entity(file, element)
|
||||
if existing:
|
||||
ifcopenshell.api.pset.remove_pset(file, product=element, pset=existing)
|
||||
|
||||
@classmethod
|
||||
def _get_pset_entity(
|
||||
cls, file: ifcopenshell.file, element: ifcopenshell.entity_instance
|
||||
) -> Optional[ifcopenshell.entity_instance]:
|
||||
"""Return the actual IfcPropertySet entity, or None."""
|
||||
pset_data = _get_pset_from_element(element)
|
||||
if not pset_data:
|
||||
return None
|
||||
return file.by_id(pset_data["id"])
|
||||
|
||||
@classmethod
|
||||
def inherit_library_license(
|
||||
cls,
|
||||
file: ifcopenshell.file,
|
||||
element: ifcopenshell.entity_instance,
|
||||
library: ifcopenshell.file,
|
||||
library_element: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
"""After append_asset, stamp an inherited library license onto the element if it has none.
|
||||
|
||||
Checks, in order: the library element itself, its declaring IfcProjectLibrary,
|
||||
then the library's IfcProject. The first match found is copied to the
|
||||
imported element in the destination file.
|
||||
"""
|
||||
# If the element already has an explicit tag it traveled with the asset.
|
||||
if _get_pset_from_element(element):
|
||||
return
|
||||
|
||||
pset_data = cls._resolve_library_license(library, library_element)
|
||||
if not pset_data:
|
||||
return
|
||||
|
||||
spdx_id = pset_data.get("SpdxLicenseIdentifier", "")
|
||||
copyright_notice = pset_data.get("CopyrightNotice", "")
|
||||
if not spdx_id and not copyright_notice:
|
||||
return
|
||||
|
||||
cls.set_license(
|
||||
file,
|
||||
element,
|
||||
spdx_id=spdx_id,
|
||||
copyright_notice=copyright_notice,
|
||||
attribution_text=pset_data.get("AttributionText", "") or "",
|
||||
source_url=pset_data.get("SourceUrl", "") or "",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _resolve_library_license(
|
||||
cls, library: ifcopenshell.file, library_element: ifcopenshell.entity_instance
|
||||
) -> Optional[dict]:
|
||||
"""Walk up inside the library file to find an effective license."""
|
||||
# 1. Directly on the element (should already have traveled, but check anyway)
|
||||
pset = _get_pset_from_element(library_element)
|
||||
if pset:
|
||||
return pset
|
||||
|
||||
# 2. Declaring IfcProjectLibrary context
|
||||
if hasattr(library_element, "HasContext"):
|
||||
for rel in library_element.HasContext:
|
||||
pset = _get_pset_from_element(rel.RelatingContext)
|
||||
if pset:
|
||||
return pset
|
||||
|
||||
# 3. IfcProject
|
||||
for project in library.by_type("IfcProject"):
|
||||
pset = _get_pset_from_element(project)
|
||||
if pset:
|
||||
return pset
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,310 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# AI-assisted development tool was used in writing this file.
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
import pytest
|
||||
|
||||
import bonsai.core.tool
|
||||
from bonsai.tool.license import PSET_NAME
|
||||
from bonsai.tool.license import License as subject
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ifc():
|
||||
return ifcopenshell.file(schema="IFC4")
|
||||
|
||||
|
||||
def make_spatial_hierarchy(ifc):
|
||||
"""Return (project, storey, wall) connected through full spatial chain."""
|
||||
project = ifc.create_entity("IfcProject", Name="TestProject")
|
||||
site = ifc.create_entity("IfcSite", Name="Site")
|
||||
building = ifc.create_entity("IfcBuilding", Name="Building")
|
||||
storey = ifc.create_entity("IfcBuildingStorey", Name="Ground Floor")
|
||||
wall = ifc.create_entity("IfcWall", Name="W1")
|
||||
ifc.create_entity(
|
||||
"IfcRelAggregates", GlobalId=ifcopenshell.guid.new(), RelatingObject=project, RelatedObjects=[site]
|
||||
)
|
||||
ifc.create_entity(
|
||||
"IfcRelAggregates", GlobalId=ifcopenshell.guid.new(), RelatingObject=site, RelatedObjects=[building]
|
||||
)
|
||||
ifc.create_entity(
|
||||
"IfcRelAggregates", GlobalId=ifcopenshell.guid.new(), RelatingObject=building, RelatedObjects=[storey]
|
||||
)
|
||||
ifc.create_entity(
|
||||
"IfcRelContainedInSpatialStructure",
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
RelatingStructure=storey,
|
||||
RelatedElements=[wall],
|
||||
)
|
||||
return project, storey, wall
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interface
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestImplementsTool:
|
||||
def test_run(self):
|
||||
assert isinstance(subject(), bonsai.core.tool.License)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_pset
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetPset:
|
||||
def test_returns_none_when_no_pset(self, ifc):
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
assert subject.get_pset(wall) is None
|
||||
|
||||
def test_returns_pset_when_present(self, ifc):
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
subject.set_license(ifc, wall, "CC-BY-4.0", "(c) 2026 Test")
|
||||
result = subject.get_pset(wall)
|
||||
assert result is not None
|
||||
assert result["SpdxLicenseIdentifier"] == "CC-BY-4.0"
|
||||
|
||||
def test_does_not_inherit_from_type(self, ifc):
|
||||
"""get_pset is direct-only; a pset on the type must not appear on the occurrence."""
|
||||
wall_type = ifc.create_entity("IfcWallType", Name="WT")
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
ifc.create_entity(
|
||||
"IfcRelDefinesByType",
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
RelatingType=wall_type,
|
||||
RelatedObjects=[wall],
|
||||
)
|
||||
subject.set_license(ifc, wall_type, "CC0-1.0", "Public Domain")
|
||||
assert subject.get_pset(wall) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_effective_pset
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetEffectivePset:
|
||||
def test_returns_none_when_no_license_anywhere(self, ifc):
|
||||
_, _, wall = make_spatial_hierarchy(ifc)
|
||||
pset, source = subject.get_effective_pset(wall)
|
||||
assert pset is None
|
||||
assert source is None
|
||||
|
||||
def test_returns_own_pset_with_self_as_source(self, ifc):
|
||||
_, _, wall = make_spatial_hierarchy(ifc)
|
||||
subject.set_license(ifc, wall, "MIT", "(c) 2026 Someone")
|
||||
pset, source = subject.get_effective_pset(wall)
|
||||
assert pset is not None
|
||||
assert pset["SpdxLicenseIdentifier"] == "MIT"
|
||||
assert source.id() == wall.id()
|
||||
|
||||
def test_inherits_from_project(self, ifc):
|
||||
project, _, wall = make_spatial_hierarchy(ifc)
|
||||
subject.set_license(ifc, project, "CC-BY-4.0", "(c) 2026 Corp")
|
||||
pset, source = subject.get_effective_pset(wall)
|
||||
assert pset["SpdxLicenseIdentifier"] == "CC-BY-4.0"
|
||||
assert source.id() == project.id()
|
||||
|
||||
def test_own_pset_overrides_project(self, ifc):
|
||||
project, _, wall = make_spatial_hierarchy(ifc)
|
||||
subject.set_license(ifc, project, "CC-BY-4.0", "(c) 2026 Corp")
|
||||
subject.set_license(ifc, wall, "CC-BY-SA-4.0", "(c) 2026 Other")
|
||||
pset, source = subject.get_effective_pset(wall)
|
||||
assert pset["SpdxLicenseIdentifier"] == "CC-BY-SA-4.0"
|
||||
assert source.id() == wall.id()
|
||||
|
||||
def test_inherits_from_storey(self, ifc):
|
||||
_, storey, wall = make_spatial_hierarchy(ifc)
|
||||
subject.set_license(ifc, storey, "ODbL-1.0", "(c) 2026 Storey Owner")
|
||||
pset, source = subject.get_effective_pset(wall)
|
||||
assert pset["SpdxLicenseIdentifier"] == "ODbL-1.0"
|
||||
assert source.id() == storey.id()
|
||||
|
||||
def test_stops_at_project(self, ifc):
|
||||
project, _, _ = make_spatial_hierarchy(ifc)
|
||||
subject.set_license(ifc, project, "CC0-1.0", "Public Domain")
|
||||
pset, source = subject.get_effective_pset(project)
|
||||
assert source.id() == project.id()
|
||||
|
||||
def test_type_without_own_pset(self, ifc):
|
||||
wall_type = ifc.create_entity("IfcWallType", Name="WT")
|
||||
pset, source = subject.get_effective_pset(wall_type)
|
||||
assert pset is None
|
||||
assert source is None
|
||||
|
||||
def test_type_with_own_pset(self, ifc):
|
||||
wall_type = ifc.create_entity("IfcWallType", Name="WT")
|
||||
subject.set_license(ifc, wall_type, "Apache-2.0", "(c) 2026 Firm")
|
||||
pset, source = subject.get_effective_pset(wall_type)
|
||||
assert pset["SpdxLicenseIdentifier"] == "Apache-2.0"
|
||||
assert source.id() == wall_type.id()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# set_license
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSetLicense:
|
||||
def test_creates_pset_on_occurrence(self, ifc):
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
subject.set_license(ifc, wall, "CC-BY-4.0", "(c) 2026 Corp")
|
||||
result = ifcopenshell.util.element.get_pset(wall, PSET_NAME, should_inherit=False)
|
||||
assert result["SpdxLicenseIdentifier"] == "CC-BY-4.0"
|
||||
assert result["CopyrightNotice"] == "(c) 2026 Corp"
|
||||
|
||||
def test_creates_pset_on_type(self, ifc):
|
||||
wall_type = ifc.create_entity("IfcWallType", Name="WT")
|
||||
subject.set_license(ifc, wall_type, "MIT", "(c) 2026 Lib")
|
||||
result = ifcopenshell.util.element.get_pset(wall_type, PSET_NAME, should_inherit=False)
|
||||
assert result["SpdxLicenseIdentifier"] == "MIT"
|
||||
|
||||
def test_creates_pset_on_project(self, ifc):
|
||||
project = ifc.create_entity("IfcProject", Name="P")
|
||||
subject.set_license(ifc, project, "CC0-1.0", "Public Domain")
|
||||
result = ifcopenshell.util.element.get_pset(project, PSET_NAME, should_inherit=False)
|
||||
assert result["SpdxLicenseIdentifier"] == "CC0-1.0"
|
||||
|
||||
def test_updates_existing_pset_in_place(self, ifc):
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
subject.set_license(ifc, wall, "CC-BY-4.0", "First")
|
||||
subject.set_license(ifc, wall, "MIT", "Second")
|
||||
psets = [e for e in ifc.by_type("IfcPropertySet") if e.Name == PSET_NAME]
|
||||
assert len(psets) == 1, "Should update in place, not duplicate"
|
||||
result = ifcopenshell.util.element.get_pset(wall, PSET_NAME, should_inherit=False)
|
||||
assert result["SpdxLicenseIdentifier"] == "MIT"
|
||||
assert result["CopyrightNotice"] == "Second"
|
||||
|
||||
def test_optional_fields_stored_when_provided(self, ifc):
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
subject.set_license(
|
||||
ifc,
|
||||
wall,
|
||||
"CC-BY-4.0",
|
||||
"(c) 2026 Corp",
|
||||
attribution_text="Designed by Corp",
|
||||
source_url="https://example.com",
|
||||
)
|
||||
result = ifcopenshell.util.element.get_pset(wall, PSET_NAME, should_inherit=False)
|
||||
assert result["AttributionText"] == "Designed by Corp"
|
||||
assert result["SourceUrl"] == "https://example.com"
|
||||
|
||||
def test_optional_fields_omitted_when_empty(self, ifc):
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
subject.set_license(ifc, wall, "CC-BY-4.0", "(c) 2026 Corp")
|
||||
result = ifcopenshell.util.element.get_pset(wall, PSET_NAME, should_inherit=False)
|
||||
assert not result.get("AttributionText")
|
||||
assert not result.get("SourceUrl")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# remove_license
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRemoveLicense:
|
||||
def test_removes_pset(self, ifc):
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
subject.set_license(ifc, wall, "CC-BY-4.0", "(c) 2026 Corp")
|
||||
assert subject.get_pset(wall) is not None
|
||||
subject.remove_license(ifc, wall)
|
||||
assert subject.get_pset(wall) is None
|
||||
|
||||
def test_no_error_when_no_pset(self, ifc):
|
||||
wall = ifc.create_entity("IfcWall")
|
||||
subject.remove_license(ifc, wall) # Must not raise
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# inherit_library_license
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestInheritLibraryLicense:
|
||||
def make_library(self, spdx_id, notice, on="project"):
|
||||
"""Library with license on IfcProject (default) or on the type."""
|
||||
lib = ifcopenshell.file(schema="IFC4")
|
||||
lib_project = lib.create_entity("IfcProject", Name="Library")
|
||||
wall_type = lib.create_entity("IfcWallType", Name="WAL01")
|
||||
if on == "project":
|
||||
subject.set_license(lib, lib_project, spdx_id, notice)
|
||||
elif on == "type":
|
||||
subject.set_license(lib, wall_type, spdx_id, notice)
|
||||
return lib, wall_type
|
||||
|
||||
def test_stamps_project_license_onto_untagged_type(self, ifc):
|
||||
lib, lib_type = self.make_library("CC0-1.0", "Public Domain", on="project")
|
||||
dest_type = ifc.create_entity("IfcWallType", Name="WAL01")
|
||||
subject.inherit_library_license(ifc, dest_type, lib, lib_type)
|
||||
result = subject.get_pset(dest_type)
|
||||
assert result is not None
|
||||
assert result["SpdxLicenseIdentifier"] == "CC0-1.0"
|
||||
assert result["CopyrightNotice"] == "Public Domain"
|
||||
|
||||
def test_does_not_overwrite_existing_tag(self, ifc):
|
||||
"""An element already tagged (pset traveled via append_asset) must not be overwritten."""
|
||||
lib, lib_type = self.make_library("CC0-1.0", "Public Domain", on="project")
|
||||
dest_type = ifc.create_entity("IfcWallType", Name="WAL01")
|
||||
subject.set_license(ifc, dest_type, "CC-BY-SA-4.0", "(c) 2026 Studio")
|
||||
subject.inherit_library_license(ifc, dest_type, lib, lib_type)
|
||||
assert subject.get_pset(dest_type)["SpdxLicenseIdentifier"] == "CC-BY-SA-4.0"
|
||||
|
||||
def test_no_license_in_library_does_nothing(self, ifc):
|
||||
lib = ifcopenshell.file(schema="IFC4")
|
||||
lib.create_entity("IfcProject", Name="Library")
|
||||
lib_type = lib.create_entity("IfcWallType", Name="WAL01")
|
||||
dest_type = ifc.create_entity("IfcWallType", Name="WAL01")
|
||||
subject.inherit_library_license(ifc, dest_type, lib, lib_type)
|
||||
assert subject.get_pset(dest_type) is None
|
||||
|
||||
def test_type_level_library_license_is_resolved(self, ifc):
|
||||
"""License on the library type itself is also found."""
|
||||
lib, lib_type = self.make_library("MIT", "(c) 2026 Lib", on="type")
|
||||
dest_type = ifc.create_entity("IfcWallType", Name="WAL01")
|
||||
subject.inherit_library_license(ifc, dest_type, lib, lib_type)
|
||||
result = subject.get_pset(dest_type)
|
||||
assert result is not None
|
||||
assert result["SpdxLicenseIdentifier"] == "MIT"
|
||||
|
||||
def test_all_optional_fields_are_propagated(self, ifc):
|
||||
lib = ifcopenshell.file(schema="IFC4")
|
||||
lib_project = lib.create_entity("IfcProject", Name="Library")
|
||||
lib_type = lib.create_entity("IfcWallType", Name="WAL01")
|
||||
subject.set_license(
|
||||
lib,
|
||||
lib_project,
|
||||
"CC-BY-4.0",
|
||||
"(c) 2026 Corp",
|
||||
attribution_text="Credit: Corp",
|
||||
source_url="https://example.com/lib",
|
||||
)
|
||||
dest_type = ifc.create_entity("IfcWallType", Name="WAL01")
|
||||
subject.inherit_library_license(ifc, dest_type, lib, lib_type)
|
||||
result = subject.get_pset(dest_type)
|
||||
assert result["AttributionText"] == "Credit: Corp"
|
||||
assert result["SourceUrl"] == "https://example.com/lib"
|
||||
Reference in New Issue
Block a user