Compare commits

..

39 Commits

Author SHA1 Message Date
Dogukan Karatas 00bcefba56 Merge pull request #269 from specklesystems/dogukan/fixing-applicationIds
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: unique applicationIds
2025-06-03 18:17:58 +02:00
Dogukan Karatas d53bc064e1 removes the volume calculation 2025-06-03 17:58:11 +02:00
Dogukan Karatas 50affbedf1 sets unique ids 2025-06-03 17:55:09 +02:00
Dogukan Karatas f00aeecead Merge pull request #268 from specklesystems/bilal/send-ui
feat: publish blender models
2025-06-03 17:52:08 +02:00
Dogukan Karatas bbf2ee79be fixes the applicationId 2025-06-03 16:22:11 +02:00
Dogukan Karatas abb1f042d4 Merge pull request #262 from specklesystems/bilal/v3-cnx-1815-blender-connector-legacy-to-next-gen-transition
v3 - prep for next-gen as stable
2025-06-02 14:42:51 +02:00
Dogukan Karatas 04b4e02d05 adds workspace to metrics 2025-06-02 13:02:52 +02:00
Dogukan Karatas 144990db7a adds metrics 2025-06-02 12:35:55 +02:00
Mucahit Bilal GOKER a5592cdd7d Merge branch 'bilal/cnx-1880-implement-send-ui' into bilal/send-ui 2025-05-28 22:44:02 +03:00
Dogukan Karatas 249e3a0a84 fixed model card issue 2025-05-28 20:54:05 +02:00
Dogukan Karatas f869139d2a fix the specklepy version 2025-05-28 19:41:13 +02:00
Dogukan Karatas 3954369db5 Merge branch 'v3-dev' into bilal/send-ui 2025-05-28 19:39:58 +02:00
Dogukan Karatas 7165d99b76 splitting the edges 2025-05-28 15:11:13 +02:00
Dogukan Karatas 8234db872d fixed the displayValue hierarchy 2025-05-27 22:56:14 +02:00
Dogukan Karatas 9acc84387f updates units 2025-05-27 21:47:06 +02:00
Dogukan Karatas 4eb1f2b773 base collection added 2025-05-27 21:04:27 +02:00
Dogukan Karatas 4d60607a92 materials are updated 2025-05-27 20:53:24 +02:00
Dogukan Karatas 09f809fc8f adds render material conversion 2025-05-27 15:34:26 +02:00
Mucahit Bilal GOKER 9f6833c36e show publish button only after selection made 2025-05-24 22:14:29 +03:00
Mucahit Bilal GOKER 1cc8ed804a store user selection in wm 2025-05-24 22:12:39 +03:00
Mucahit Bilal GOKER eb0f036641 publish > selection filter button 2025-05-24 22:09:11 +03:00
Mucahit Bilal GOKER f140c41af6 replace invoke on publish button 2025-05-24 21:59:42 +03:00
Mucahit Bilal GOKER 8a91634f55 placeholder publish button 2025-05-24 21:58:37 +03:00
Mucahit Bilal GOKER b025819307 hide version selector and load button when publish is selected 2025-05-24 21:57:40 +03:00
Mucahit Bilal GOKER 3ec98d3dd2 remove ui mode from speckle state 2025-05-24 21:55:07 +03:00
Mucahit Bilal GOKER ba182d48c4 add ui mode radio button 2025-05-24 21:53:54 +03:00
Dogukan Karatas f35853cff1 adds mesh to speckle 2025-05-21 15:59:03 +02:00
Dogukan Karatas d64ad50d32 adds spline conversion 2025-05-21 12:57:51 +02:00
Dogukan Karatas ab0b012f58 first pass of conversions 2025-05-20 16:51:53 +02:00
Mucahit Bilal GOKER 8c39da370f update tagline 2025-05-15 14:22:43 +03:00
Mucahit Bilal GOKER 532eba37a5 remove beta tag 2025-05-15 14:18:29 +03:00
Mucahit Bilal GOKER 39207e5716 update website 2025-05-15 14:17:59 +03:00
Mucahit Bilal GOKER 75763d0929 show publish button only after selection made 2025-04-25 11:28:26 +03:00
Mucahit Bilal GOKER 2f2a67a569 store user selection in wm 2025-04-25 11:28:14 +03:00
Mucahit Bilal GOKER 7246d239be publish > selection filter button 2025-04-24 23:06:12 +03:00
Mucahit Bilal GOKER 4d1fe83c1e show placeholder publish button 2025-04-24 22:52:30 +03:00
Mucahit Bilal GOKER 1097bba539 hide version selector and load button when publish is selected 2025-04-24 22:48:43 +03:00
Mucahit Bilal GOKER eb25b6d821 remove ui mode from speckle state 2025-04-24 22:46:26 +03:00
Mucahit Bilal GOKER 80515fdc69 add ui mode radio button in the main panel 2025-04-24 22:46:14 +03:00
22 changed files with 1419 additions and 175 deletions
+17 -2
View File
@@ -38,7 +38,7 @@ from .connector.ui.main_panel import SPECKLE_PT_main_panel
from .connector.ui.project_selection_dialog import SPECKLE_OT_project_selection_dialog, speckle_project, SPECKLE_UL_projects_list, speckle_workspace
from .connector.ui.model_selection_dialog import SPECKLE_OT_model_selection_dialog, speckle_model, SPECKLE_UL_models_list
from .connector.ui.version_selection_dialog import SPECKLE_OT_version_selection_dialog, speckle_version, SPECKLE_UL_versions_list
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog, speckle_object
from .connector.ui.model_card import speckle_model_card
# Operators
from .connector.blender_operators.publish_button import SPECKLE_OT_publish
@@ -81,6 +81,21 @@ def invoke_window_manager_properties():
)
WindowManager.selected_version_id = bpy.props.StringProperty()
WindowManager.selected_version_load_option = bpy.props.StringProperty()
# Send / Publish buttons
WindowManager.ui_mode = bpy.props.EnumProperty( # type: ignore
name="UI Mode",
description="Publish or Load a model",
items=[
("PUBLISH", "Publish", "Publish a model to Speckle", "EXPORT", 0),
("LOAD", "Load", "Load a model from Speckle", "IMPORT", 1),
],
default="PUBLISH",
)
# Objects
WindowManager.speckle_objects = bpy.props.CollectionProperty(
type=speckle_object
)
def save_model_cards(scene):
model_cards_data = [card.to_dict() for card in scene.speckle_state.model_cards]
@@ -103,7 +118,7 @@ classes = (
SPECKLE_OT_project_selection_dialog, speckle_project, SPECKLE_UL_projects_list, speckle_workspace,
SPECKLE_OT_model_selection_dialog, speckle_model, SPECKLE_UL_models_list,
SPECKLE_OT_version_selection_dialog, speckle_version, SPECKLE_UL_versions_list,
SPECKLE_OT_selection_filter_dialog,
SPECKLE_OT_selection_filter_dialog, speckle_object,
speckle_model_card, SPECKLE_OT_model_card_settings, SPECKLE_OT_view_in_browser, SPECKLE_OT_view_model_versions, SPECKLE_OT_delete_model_card,
SPECKLE_OT_select_objects,
SPECKLE_OT_add_account,
+4 -4
View File
@@ -4,14 +4,14 @@ schema_version = "1.0.0"
# Change the values according to your extension
id = "speckle_blender_addon"
version = "3.0.0"
name = "Speckle for Blender BETA"
tagline = "Speckle connector for Blender"
maintainer = "AEC SYSTEMS LTD"
name = "Speckle Connector"
tagline = "Load models from other AEC apps into Blender with Speckle."
maintainer = "Speckle"
# Supported types: "add-on", "theme"
type = "add-on"
# Optional link to documentation, support, source files, etc
website = "https://speckle.guide/user/blender.html"
website = "https://app.speckle.systems/connectors"
# Optional list defined by Blender and server, see:
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
@@ -1,7 +1,24 @@
import bpy
from bpy.types import Context
from bpy.types import Event
from typing import Set
from typing import Set, List, Optional
from specklepy.objects import Base
from specklepy.objects.models.collections.collection import Collection
from specklepy.core.api import operations
from specklepy.core.api.client import SpeckleClient
from specklepy.transports.server import ServerTransport
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.core.api.credentials import get_local_accounts
from specklepy.objects.models.units import Units
from ...converter.to_speckle import convert_to_speckle
from ...converter.to_speckle.material_to_speckle import (
add_render_material_proxies_to_base,
)
from ...converter.utils import get_project_workspace_id
from specklepy.logging import metrics
from ....bpy_speckle import bl_info
class SPECKLE_OT_publish(bpy.types.Operator):
@@ -14,7 +31,177 @@ class SPECKLE_OT_publish(bpy.types.Operator):
return self.execute(context)
def execute(self, context: Context) -> Set[str]:
context.scene.speckle_state.ui_mode = "PUBLISH"
wm = context.window_manager
bpy.ops.speckle.project_selection_dialog("INVOKE_DEFAULT")
return {"FINISHED"}
if not context.selected_objects and not context.active_object:
self.report({"ERROR"}, "No objects selected to publish")
return {"CANCELLED"}
account_id = getattr(wm, "selected_account_id", "")
project_id = getattr(wm, "selected_project_id", "")
model_id = getattr(wm, "selected_model_id", "")
if not account_id:
self.report({"ERROR"}, "No account selected")
return {"CANCELLED"}
if not project_id:
self.report({"ERROR"}, "No project selected")
return {"CANCELLED"}
if not model_id:
self.report({"ERROR"}, "No model selected")
return {"CANCELLED"}
try:
account = next(
(acc for acc in get_local_accounts() if acc.id == account_id),
None,
)
if account is None:
self.report({"ERROR"}, "No Speckle account found")
return {"CANCELLED"}
client = SpeckleClient(host=account.serverInfo.url)
client.authenticate_with_account(account)
transport = ServerTransport(stream_id=project_id, client=client)
# get objects to convert
objects_to_convert = context.selected_objects or [context.active_object]
speckle_objects = self.convert_selected_objects(context)
if not speckle_objects:
self.report(
{"ERROR"}, "No objects could be converted to Speckle format"
)
return {"CANCELLED"}
# get the Blender file name to set the name
file_name = bpy.path.basename(bpy.data.filepath)
collection_name = file_name if file_name else "Untitled.blend"
# create a collection to hold all objects
collection = Collection(name=collection_name)
collection.units = Units.m.value
collection["version"] = 3
for obj in speckle_objects:
if obj is not None:
collection.elements.append(obj)
add_render_material_proxies_to_base(collection, objects_to_convert)
obj_id = operations.send(collection, [transport])
version_input = CreateVersionInput(
objectId=obj_id,
modelId=model_id,
projectId=project_id,
message="",
sourceApplication="blender",
)
version = client.version.create(version_input)
version_id = version.id
metrics.set_host_app("blender")
metrics.track(
metrics.SEND,
account,
{
"ui": "dui3",
"hostAppVersion": ",".join(map(str, bl_info["blender"])),
"core_version": ",".join(map(str, bl_info["version"])),
"workspace_id": get_project_workspace_id(client, project_id),
},
)
# Update model card if needed
if hasattr(context.scene, "speckle_state") and hasattr(
context.scene.speckle_state, "model_cards"
):
model_card = context.scene.speckle_state.model_cards.add()
model_card.account_id = account_id
model_card.server_url = account.serverInfo.url
model_card.project_id = project_id
model_card.project_name = getattr(wm, "selected_project_name", "")
model_card.model_id = model_id
model_card.model_name = getattr(wm, "selected_model_name", "")
model_card.is_publish = True
model_card.load_option = "SPECIFIC" # Published versions are specific
model_card.version_id = version_id
model_card.collection_name = (
f"{getattr(wm, 'selected_model_name', 'Model')} - {version_id[:8]}"
)
# Clear selected model details from Window Manager AFTER creating model card
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_project_name = ""
wm.selected_model_id = ""
wm.selected_model_name = ""
wm.selected_version_load_option = ""
wm.selected_version_id = ""
self.report(
{"INFO"},
f"Successfully published {len(speckle_objects)} objects to Speckle with materials",
)
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to publish: {str(e)}")
import traceback
traceback.print_exc()
return {"CANCELLED"}
def convert_selected_objects(self, context: Context) -> List[Optional[Base]]:
scene = context.scene
unit_settings = scene.unit_settings
# get units from Blender's unit system
if unit_settings.system == "METRIC":
if unit_settings.length_unit == "METERS":
units = Units.m
elif unit_settings.length_unit == "CENTIMETERS":
units = Units.cm
elif unit_settings.length_unit == "MILLIMETERS":
units = Units.mm
elif unit_settings.length_unit == "KILOMETERS":
units = Units.km
else:
units = Units.m
elif unit_settings.system == "IMPERIAL":
if unit_settings.length_unit == "FEET":
units = Units.feet
elif unit_settings.length_unit == "INCHES":
units = Units.inches
elif unit_settings.length_unit == "YARDS":
units = Units.yards
elif unit_settings.length_unit == "MILES":
units = Units.miles
else:
units = Units.feet # default to feet
else:
units = Units.m # default to meters
scale_factor = unit_settings.scale_length
# convert each selected object
speckle_objects = []
objects_to_convert = context.selected_objects or [context.active_object]
for obj in objects_to_convert:
# Skip objects that are not supported
if not obj or obj.type not in ["MESH", "CURVE", "EMPTY"]:
continue
# convert the object
speckle_obj = convert_to_speckle(obj, scale_factor, units.value)
if speckle_obj:
speckle_objects.append(speckle_obj)
return speckle_objects
@@ -1,22 +1,25 @@
import bpy
from bpy.types import Context
from specklepy.api.credentials import get_local_accounts
from specklepy.core.api.credentials import get_local_accounts
from specklepy.transports.server import ServerTransport
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api import operations
from specklepy.core.api.client import SpeckleClient
from specklepy.objects.models.collections.collection import Collection as SCollection
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from specklepy.core.api import host_applications
from ..utils.get_ascendants import get_ascendants
from ...converter.utils import find_object_by_id
from ...converter.utils import find_object_by_id, get_project_workspace_id
from ...converter.to_native import (
convert_to_native,
render_material_proxy_to_native,
instance_definition_proxy_to_native,
find_instance_definitions,
)
from specklepy.logging import metrics
from ....bpy_speckle import bl_info
def load_operation(context: Context) -> None:
@@ -52,6 +55,23 @@ def load_operation(context: Context) -> None:
version_data = operations.receive(obj_id, transport)
metrics.set_host_app("blender")
metrics.track(
metrics.RECEIVE,
account,
{
"ui": "dui3",
"hostAppVersion": ",".join(map(str, bl_info["blender"])),
"core_version": ",".join(map(str, bl_info["version"])),
"sourceHostApp": host_applications.get_host_app_from_string(
version.source_application
).slug,
"isMultiplayer": version.author_user.id != account.userInfo.id,
"workspace_id": get_project_workspace_id(client, wm.selected_project_id),
},
)
# Create material mapping first
material_mapping = render_material_proxy_to_native(version_data)
@@ -11,7 +11,6 @@ class SpeckleState(PropertyGroup):
manages the state of the Speckle addon in Blender
"""
ui_mode: StringProperty(name="UI Mode", default="NONE") # type: ignore
model_cards: CollectionProperty(type=speckle_model_card) # type: ignore
def get_model_card_by_id(self, model_card_id: str) -> Optional[speckle_model_card]:
+42 -22
View File
@@ -17,7 +17,7 @@ class SPECKLE_PT_main_panel(bpy.types.Panel):
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.label(text="Speckle Connector BETA", icon_value=get_icon("speckle_logo"))
layout.label(text="Speckle Connector", icon_value=get_icon("speckle_logo"))
# check to see if there are any speckle models in the file
if not context.scene.speckle_state.model_cards:
@@ -30,6 +30,11 @@ class SPECKLE_PT_main_panel(bpy.types.Panel):
project_selected = bool(getattr(wm, "selected_project_name", None))
model_selected = bool(getattr(wm, "selected_model_name", None))
version_selected = bool(getattr(wm, "selected_version_id", None))
selection_made = bool(getattr(wm, "speckle_objects", None))
# UI Mode Switch
row = layout.row()
row.prop(wm, "ui_mode", expand=True)
# select Project button
row = layout.row()
@@ -53,30 +58,45 @@ class SPECKLE_PT_main_panel(bpy.types.Panel):
text=model_button_text,
icon=model_button_icon,
)
if wm.ui_mode == "PUBLISH":
#TODO: implement Publish flow
# Selection filter
row = layout.row()
row.enabled = project_selected and model_selected
selection_button_text = f"{len(wm.speckle_objects)} Objects" if wm.speckle_objects else "Select Objects"
row.operator("speckle.selection_filter_dialog", text=selection_button_text, icon="PLUS")
# select Version button
row = layout.row()
version_id = getattr(wm, "selected_version_id", "")
load_option = getattr(wm, "selected_version_load_option", "")
if load_option == "LATEST":
version_button_text = "Latest"
elif load_option == "SPECIFIC":
version_button_text = version_id
else:
version_button_text = "Select Version"
# Publish button
row = layout.row()
row.enabled = project_selected and model_selected and selection_made
row.operator("speckle.publish", text="Publish Model", icon="EXPORT")
pass
version_button_icon = "CHECKMARK" if version_selected else "PLUS"
row.enabled = project_selected and model_selected
row.operator(
"speckle.version_selection_dialog",
text=version_button_text,
icon=version_button_icon,
)
if wm.ui_mode == "LOAD":
# select Version button
row = layout.row()
version_id = getattr(wm, "selected_version_id", "")
load_option = getattr(wm, "selected_version_load_option", "")
if load_option == "LATEST":
version_button_text = "Latest"
elif load_option == "SPECIFIC":
version_button_text = version_id
else:
version_button_text = "Select Version"
# load button
row = layout.row()
row.enabled = project_selected and model_selected and version_selected
row.operator("speckle.load", text="Load Model", icon="IMPORT")
version_button_icon = "CHECKMARK" if version_selected else "PLUS"
row.enabled = project_selected and model_selected
row.operator(
"speckle.version_selection_dialog",
text=version_button_text,
icon=version_button_icon,
)
# load button
row = layout.row()
row.enabled = project_selected and model_selected and version_selected
row.operator("speckle.load", text="Load Model", icon="IMPORT")
layout.separator()
@@ -1,7 +1,7 @@
import bpy
from typing import List
from bpy.types import Operator, Context, Object
from bpy.props import EnumProperty, StringProperty
from bpy.props import EnumProperty
class SPECKLE_OT_selection_filter_dialog(Operator):
@@ -11,6 +11,7 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
bl_idname = "speckle.selection_filter_dialog"
bl_label = "Select Objects"
bl_description = "Select objects to publish"
selection_type: EnumProperty(
name="Selection",
@@ -20,44 +21,36 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
default="SELECTION",
) # type: ignore
project_name: StringProperty(
name="Project Name", description="Name of the selected project", default=""
) # type: ignore
project_id: StringProperty(
name="Project ID", description="ID of the selected project", default=""
) # type: ignore
model_name: StringProperty(
name="Model Name", description="Name of the selected model", default=""
) # type: ignore
model_id: StringProperty(
name="Model ID", description="ID of the selected model", default=""
) # type: ignore
def execute(self, context: Context) -> set:
model_card = context.scene.speckle_state.model_cards.add()
model_card.project_name = self.project_name
model_card.model_name = self.model_name
model_card.model_id = self.model_id
model_card.project_id = self.project_id
model_card.is_publish = True
# model_card = context.scene.speckle_state.model_cards.add()
# model_card.project_name = self.project_name
# model_card.model_name = self.model_name
# model_card.model_id = self.model_id
# model_card.project_id = self.project_id
# model_card.is_publish = True
selected_objects: list[Object] = context.selected_objects
total_selected: int = len(selected_objects)
object_types: dict[str, int] = {}
for obj in selected_objects:
if obj.type not in object_types:
object_types[obj.type] = 1
else:
object_types[obj.type] += 1
# selected_objects: list[Object] = context.selected_objects
# total_selected: int = len(selected_objects)
# object_types: dict[str, int] = {}
# for obj in selected_objects:
# if obj.type not in object_types:
# object_types[obj.type] = 1
# else:
# object_types[obj.type] += 1
summary: str = f"{total_selected} objects - "
for obj_type, count in object_types.items():
summary += f"{obj_type}: {count}, "
# summary: str = f"{total_selected} objects - "
# for obj_type, count in object_types.items():
# summary += f"{obj_type}: {count}, "
model_card.selection_summary = summary.strip()
# model_card.selection_summary = summary.strip()
#TODO: implement selection filter dialog
wm = context.window_manager
wm.speckle_objects.clear()
user_selection = context.selected_objects
for sel in user_selection:
obj = wm.speckle_objects.add()
obj.name = sel.name
return {"FINISHED"}
def invoke(self, context: Context, event: bpy.types.Event) -> set:
@@ -65,9 +58,10 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
def draw(self, context: Context):
layout = self.layout
wm = context.window_manager
layout.label(text=f"Project: {self.project_name}")
layout.label(text=f"Model: {self.model_name}")
layout.label(text=f"Project: {wm.selected_project_name}")
layout.label(text=f"Model: {wm.selected_model_name}")
layout.prop(self, "selection_type")
layout.separator()
@@ -115,3 +109,10 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
def check(self, context: Context) -> bool:
return True # this forces the dialog to redraw
class speckle_object(bpy.types.PropertyGroup):
"""
PropertyGroup for storing model information
"""
name: bpy.props.StringProperty() #type: ignore
@@ -1,9 +1,9 @@
import bpy
from specklepy.api.credentials import get_local_accounts
from specklepy.core.api.credentials import get_local_accounts
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
from specklepy.api.client import SpeckleClient
from specklepy.api.wrapper import StreamWrapper
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.wrapper import StreamWrapper
from .misc import strip_non_ascii
+7 -3
View File
@@ -1,5 +1,5 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_local_accounts, Account
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import get_local_accounts, Account
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models.current import Model
from typing import List, Tuple, Optional
@@ -43,7 +43,11 @@ def get_models_for_project(
).items
return [
(strip_non_ascii(model.name), model.id, format_relative_time(model.updated_at))
(
strip_non_ascii(model.name),
model.id,
format_relative_time(model.updated_at),
)
for model in models
]
@@ -1,5 +1,5 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_local_accounts
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import get_local_accounts
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
@@ -1,5 +1,5 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_local_accounts, Account
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import get_local_accounts, Account
from typing import List, Tuple, Optional
from .misc import format_relative_time, strip_non_ascii
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
+1
View File
@@ -1,2 +1,3 @@
from ..converter.to_native import * #noqa: F403
from ..converter.to_speckle import * #noqa: F403
from ..converter.utils import * # noqa: F403
@@ -0,0 +1,6 @@
from .to_speckle import convert_to_speckle # noqa: F401
from .material_to_speckle import ( # noqa: F401
blender_material_to_speckle,
create_render_material_proxies,
add_render_material_proxies_to_base
)
@@ -0,0 +1,272 @@
from bpy.types import Object
from typing import Union, Optional, Tuple, List
from specklepy.objects.geometry import Polyline, Curve
from specklepy.objects.primitive import Interval
from specklepy.objects.base import Base
from mathutils import Matrix
from mathutils.geometry import interpolate_bezier
from .utils import nurb_make_curve, make_knots
def curve_to_speckle(
blender_obj: Object, scale_factor: float = 1.0
) -> Union[Base, None]:
assert blender_obj.type == "CURVE", "Object must be a curve"
assert blender_obj.data is not None, "Curve data cannot be None"
curve_data = blender_obj.data
matrix = blender_obj.matrix_world
units = "m" # TODO: Use the unit system from the scene
base = Base()
curves = []
for spline in curve_data.splines:
if spline.type == "BEZIER":
curves.append(
bezier_to_speckle(matrix, spline, blender_obj.name, scale_factor, units)
)
elif spline.type == "NURBS":
curves.append(
nurbs_to_speckle(matrix, spline, blender_obj.name, scale_factor, units)
)
if curves:
base["@elements"] = curves
base["name"] = blender_obj.name
return base
return None
def bezier_to_speckle(
matrix: Matrix,
spline,
name: Optional[str] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points: List[Tuple[float, float, float]] = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
transformed_point = matrix @ bp.handle_left * scale_factor
points.append(
(transformed_point.x, transformed_point.y, transformed_point.z)
)
transformed_point = matrix @ bp.co * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
if i < len(spline.bezier_points) - 1:
transformed_point = matrix @ bp.handle_right * scale_factor
points.append(
(transformed_point.x, transformed_point.y, transformed_point.z)
)
if closed:
transformed_point = (
matrix @ spline.bezier_points[-1].handle_right * scale_factor
)
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
transformed_point = matrix @ spline.bezier_points[0].handle_left * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
transformed_point = matrix @ spline.bezier_points[0].co * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
num_points = len(points)
flattened_points = []
for point in points:
flattened_points.extend(point)
knot_count = num_points + degree - 1
knots = [0] * knot_count
for i in range(1, len(knots)):
knots[i] = i // 3
length = spline.calc_length()
domain = Interval(start=0, end=length)
display_value = bezier_to_speckle_polyline(
matrix, spline, length, scale_factor, units
)
curve = Curve(
degree=degree,
periodic=not spline.use_endpoint_u,
rational=True,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
closed=spline.use_cyclic_u,
displayValue=display_value,
units=units,
bbox=None,
)
curve.__dict__["_length"] = length
curve.__dict__["_area"] = 0.0
curve["domain"] = domain
if name:
curve["name"] = name
return curve
def bezier_to_speckle_polyline(
matrix: Matrix,
spline,
length: Optional[float] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Optional[Polyline]:
segments = len(spline.bezier_points)
if segments < 2:
return None
resolution = spline.resolution_u + 1
points: List[float] = []
if not spline.use_cyclic_u:
segments -= 1
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
sampled_points = interpolate_bezier(knot1, handle1, handle2, knot2, resolution)
for p in sampled_points:
scaled_point = matrix @ p * scale_factor
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
polyline = Polyline(value=points, units=units)
polyline["domain"] = {"start": 0, "end": length}
polyline["closed"] = spline.use_cyclic_u
return polyline
def nurbs_to_speckle(
matrix: Matrix,
spline,
name: Optional[str] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
length = spline.calc_length()
domain = Interval(start=0, end=length)
weights = [pt.weight for pt in spline.points]
first_weight = weights[0] if weights else 1.0
is_rational = any(abs(w - first_weight) > 1e-9 for w in weights)
points = []
for pt in spline.points:
transformed_point = matrix @ pt.co.xyz * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
flattened_points = []
for point in points:
flattened_points.extend(point)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
resolution_multiplier = (
4 if (spline.use_cyclic_u and spline.point_count_u <= 16) else 1
)
display_value = nurbs_to_speckle_polyline(
matrix, spline, length, scale_factor, units, resolution_multiplier
)
curve = Curve(
degree=degree,
periodic=not spline.use_endpoint_u,
rational=is_rational,
points=flattened_points,
weights=weights,
knots=knots,
closed=spline.use_cyclic_u,
displayValue=display_value,
units=units,
bbox=None,
)
curve.__dict__["_length"] = length
curve["domain"] = domain
if name:
curve["name"] = name
return curve
def nurbs_to_speckle_polyline(
matrix: Matrix,
spline,
length: Optional[float] = None,
scale_factor: float = 1.0,
units: str = "m",
resolution_multiplier: int = 1,
) -> Polyline:
from mathutils import Vector
points: List[float] = []
resolution = spline.resolution_u * resolution_multiplier
sampled_points = nurb_make_curve(spline, resolution)
for i in range(0, len(sampled_points), 3):
point_vector = Vector(
(sampled_points[i], sampled_points[i + 1], sampled_points[i + 2])
)
transformed_point = matrix @ point_vector * scale_factor
points.append(transformed_point.x)
points.append(transformed_point.y)
points.append(transformed_point.z)
length = length or spline.calc_length()
polyline = Polyline(value=points, units=units)
polyline["domain"] = {"start": 0, "end": length}
polyline["closed"] = spline.use_cyclic_u
# Set length property if needed
if hasattr(polyline, "length") or hasattr(polyline, "_length"):
polyline.__dict__["_length"] = length
# Set area property if needed
if hasattr(polyline, "area") or hasattr(polyline, "_area"):
polyline.__dict__["_area"] = 0
return polyline
@@ -0,0 +1,257 @@
from typing import Dict, List, Set
import bpy
from bpy.types import Material, Object
from specklepy.objects.base import Base
from specklepy.objects.other import RenderMaterial
from specklepy.objects.proxies import RenderMaterialProxy
from ..utils import to_argb_int
from .utils import get_submesh_id, get_unique_id
def blender_material_to_speckle(material: Material) -> RenderMaterial:
"""
convert a Blender material to a Speckle RenderMaterial
"""
diffuse = -1 # default white
opacity = 1.0
emissive = -16777216 # default black
metalness = 0.0
roughness = 1.0
# extract material properties if using nodes
if material.use_nodes and material.node_tree:
output_node = None
for node in material.node_tree.nodes:
if node.type == "OUTPUT_MATERIAL":
output_node = node
break
# find the main shader node connected to output
main_shader = None
if output_node and output_node.inputs["Surface"].is_linked:
main_shader = output_node.inputs["Surface"].links[0].from_node
# handle different shader types
# we're supporting: principled, diffuse, emmision and glass - for now
if main_shader:
if main_shader.type == "BSDF_PRINCIPLED":
diffuse, opacity, metalness, roughness, emissive = (
_extract_principled_properties(main_shader)
)
elif main_shader.type == "BSDF_DIFFUSE":
color_input = main_shader.inputs.get("Color")
if color_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
diffuse = to_argb_int(rgba)
roughness = 1.0
elif main_shader.type == "EMISSION":
color_input = main_shader.inputs.get("Color")
strength_input = main_shader.inputs.get("Strength")
if color_input and strength_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
strength = (
float(strength_input.default_value)
if not strength_input.is_linked
else 1.0
)
if strength > 0:
emission_rgba = [c * strength for c in rgba[:3]] + [rgba[3]]
emission_rgba = [min(1.0, max(0.0, c)) for c in emission_rgba]
emissive = to_argb_int(emission_rgba)
diffuse = to_argb_int(rgba)
elif main_shader.type == "BSDF_GLASS":
color_input = main_shader.inputs.get("Color")
if color_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
diffuse = to_argb_int(rgba)
roughness_input = main_shader.inputs.get("Roughness")
if roughness_input:
roughness = (
float(roughness_input.default_value)
if not roughness_input.is_linked
else 0.0
)
opacity = 0.5
else:
# fallback to legacy material properties
if hasattr(material, "diffuse_color"):
rgba = list(material.diffuse_color) + [1.0]
diffuse = to_argb_int(rgba)
if hasattr(material, "metallic"):
metalness = float(material.metallic)
if hasattr(material, "roughness"):
roughness = float(material.roughness)
render_material = RenderMaterial(
name=material.name,
diffuse=diffuse,
opacity=opacity,
emissive=emissive,
metalness=metalness,
roughness=roughness,
)
return render_material
def _extract_principled_properties(principled_node):
diffuse = -1
opacity = 1.0
metalness = 0.0
roughness = 1.0
emissive = -16777216
base_color_input = principled_node.inputs.get("Base Color")
if base_color_input:
if base_color_input.is_linked:
rgba = _get_color_from_connected_node(base_color_input.links[0].from_node)
else:
rgba = list(base_color_input.default_value)
diffuse = to_argb_int(rgba)
# Alpha/Opacity
alpha_input = principled_node.inputs.get("Alpha")
if alpha_input and not alpha_input.is_linked:
opacity = float(alpha_input.default_value)
# Metallic
metallic_input = principled_node.inputs.get("Metallic")
if metallic_input and not metallic_input.is_linked:
metalness = float(metallic_input.default_value)
# Roughness
roughness_input = principled_node.inputs.get("Roughness")
if roughness_input and not roughness_input.is_linked:
roughness = float(roughness_input.default_value)
# Emission - try different possible input names for different versions
emission_color_input = principled_node.inputs.get(
"Emission Color"
) or principled_node.inputs.get("Emission")
emission_strength_input = principled_node.inputs.get("Emission Strength")
if emission_color_input:
if emission_color_input.is_linked:
emission_rgba = _get_color_from_connected_node(
emission_color_input.links[0].from_node
)
else:
emission_rgba = list(emission_color_input.default_value)
emission_strength = 1.0
if emission_strength_input and not emission_strength_input.is_linked:
emission_strength = float(emission_strength_input.default_value)
if emission_strength > 0 and any(
c > 0.01 for c in emission_rgba[:3]
): # Check if color is not black
final_emission_rgba = [c * emission_strength for c in emission_rgba[:3]] + [
emission_rgba[3]
]
final_emission_rgba = [min(1.0, max(0.0, c)) for c in final_emission_rgba]
emissive = to_argb_int(final_emission_rgba)
return diffuse, opacity, metalness, roughness, emissive
def _get_color_from_connected_node(node):
if node.type == "RGB":
rgba = list(node.outputs["Color"].default_value)
return rgba
elif node.type == "VALTORGB":
if node.color_ramp.elements:
rgba = list(node.color_ramp.elements[0].color)
return rgba
elif hasattr(node, "color"):
rgba = list(node.color) + [1.0]
return rgba
# fallback to white
return [1.0, 1.0, 1.0, 1.0]
def collect_material_assignments(objects: List[Object]) -> Dict[str, Set[str]]:
"""
collect material assignments for objects, creating unique applicationIds
for each material slot use a unique id
"""
material_assignments: Dict[str, Set[str]] = {}
for obj in objects:
if not obj or not hasattr(obj, "data") or not obj.data:
continue
# check if object has materials
if hasattr(obj.data, "materials") and obj.data.materials:
for material_index, material_slot in enumerate(obj.data.materials):
if material_slot:
material_name = material_slot.name
# set unique ID for submeshes
application_id = get_submesh_id(obj, material_index)
if material_name not in material_assignments:
material_assignments[material_name] = set()
material_assignments[material_name].add(application_id)
return material_assignments
def create_render_material_proxies(objects: List[Object]) -> List[RenderMaterialProxy]:
material_assignments = collect_material_assignments(objects)
if not material_assignments:
return []
proxies = []
for material_name, object_ids in material_assignments.items():
blender_material = bpy.data.materials.get(material_name)
if not blender_material:
continue
speckle_material = blender_material_to_speckle(blender_material)
proxy = RenderMaterialProxy(objects=list(object_ids), value=speckle_material)
proxy.applicationId = get_unique_id(blender_material)
proxies.append(proxy)
return proxies
def add_render_material_proxies_to_base(base: Base, objects: List[Object]) -> None:
"""
add render material proxies to the base object.
"""
proxies = create_render_material_proxies(objects)
if proxies:
base.renderMaterialProxies = proxies
@@ -0,0 +1,132 @@
from typing import Dict, List, cast
import bpy
from bpy.types import Mesh as BMesh
from bpy.types import MeshPolygon, Object
from mathutils import Matrix as MMatrix
from mathutils import Vector as MVector
from specklepy.objects.base import Base
from specklepy.objects.geometry.mesh import Mesh
from .utils import get_submesh_id
def mesh_to_speckle(
blender_object: Object, data: bpy.types.Mesh, units_scale: float, units: str
) -> Base:
"""
convert a Blender mesh object
"""
meshes = mesh_to_speckle_meshes(blender_object, data, units_scale, units)
return meshes
def mesh_to_speckle_meshes(
blender_object: Object, data: bpy.types.Mesh, units_scale: float, units: str
) -> List[Mesh]:
"""
convert a Blender mesh to a list of Speckle meshes
each face corner (loop) gets its own vertex
"""
assert isinstance(data, BMesh), "Data must be a Blender mesh"
assert units_scale > 0, "Units scale must be positive"
submesh_data: Dict[int, List[MeshPolygon]] = {}
for p in data.polygons:
if p.material_index not in submesh_data:
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world)
normal_transform = transform.to_3x3().inverted().transposed()
submeshes = []
# sort material indices to ensure consistent ordering
for material_index in sorted(submesh_data.keys()):
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
m_normals: List[float] = []
vertex_counter = 0
for face in submesh_data[material_index]:
mesh_area += face.area
loop_indices = face.loop_indices
m_faces.append(len(loop_indices))
for loop_index in loop_indices:
loop = data.loops[loop_index]
vertex = data.vertices[loop.vertex_index]
transformed_vertex = transform @ vertex.co * units_scale
m_verts.extend(
[transformed_vertex.x, transformed_vertex.y, transformed_vertex.z]
)
# get and transform the loop normal
# try to get split normal, fallback to face normal if not available
try:
if hasattr(loop, "normal") and len(loop.normal) > 0:
# Use split normal from loop
loop_normal = normal_transform @ loop.normal
else:
# Fallback to face normal
loop_normal = normal_transform @ face.normal
except: # noqa: E722
# Final fallback: use face normal
loop_normal = normal_transform @ face.normal
loop_normal.normalize()
m_normals.extend([loop_normal.x, loop_normal.y, loop_normal.z])
# add UV coordinates if available
if data.uv_layers.active:
uv_data = data.uv_layers.active.data[loop_index]
uv = cast(MVector, uv_data.uv)
m_texcoords.extend([uv.x, uv.y])
m_faces.append(vertex_counter)
vertex_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
vertexNormals=m_normals,
units=units,
)
if len(m_verts) > 0:
speckle_mesh.area = mesh_area
speckle_mesh.applicationId = get_submesh_id(blender_object, material_index)
submeshes.append(speckle_mesh)
return submeshes
def is_closed_mesh(faces: List[int]) -> bool:
"""
check if a mesh is closed by verifying that each edge is shared by exactly 2 faces.
"""
edge_counts = {}
i = 0
while i < len(faces):
vertex_count = faces[i]
for j in range(vertex_count):
v1 = faces[i + 1 + j]
v2 = faces[i + 1 + ((j + 1) % vertex_count)]
edge = tuple(sorted([v1, v2]))
edge_counts[edge] = edge_counts.get(edge, 0) + 1
i += vertex_count + 1
return all(count == 2 for count in edge_counts.values())
@@ -0,0 +1,17 @@
from bpy.types import Object
from specklepy.objects.geometry import Point
def point_to_speckle(blender_object: Object, scale_factor: float = 1.0) -> Point:
assert blender_object.type == "EMPTY", "Object must be an empty."
location = blender_object.location
speckle_point = Point(
x=location.x * scale_factor,
y=location.y * scale_factor,
z=location.z * scale_factor,
units="", # TODO: implement units in object level
)
return speckle_point
@@ -0,0 +1,49 @@
from bpy.types import Object
from typing import Optional
from specklepy.objects.data_objects import BlenderObject
from .curve_to_speckle import curve_to_speckle
from .mesh_to_speckle import mesh_to_speckle_meshes
from .utils import get_object_id, get_curve_element_id
def convert_to_speckle(
blender_object: Object,
scale_factor: float = 1.0,
units: str = "m",
) -> Optional[BlenderObject]:
display_value = []
properties = {}
if blender_object.type == "CURVE":
curve_result = curve_to_speckle(blender_object, scale_factor)
if curve_result and hasattr(curve_result, "@elements"):
display_value = curve_result["@elements"]
for i, element in enumerate(display_value):
if hasattr(element, "applicationId"):
element.applicationId = get_curve_element_id(blender_object, i)
elif curve_result:
if hasattr(curve_result, "applicationId"):
curve_result.applicationId = get_curve_element_id(blender_object, 0)
display_value = [curve_result]
elif blender_object.type == "MESH":
meshes = mesh_to_speckle_meshes(
blender_object, blender_object.data, scale_factor, units
)
if meshes:
display_value = meshes
if not display_value:
return None
if not isinstance(display_value, list):
display_value = [display_value]
return BlenderObject(
name=blender_object.name,
type=blender_object.type,
displayValue=display_value,
applicationId=get_object_id(blender_object),
properties=properties,
units=units,
)
+242
View File
@@ -0,0 +1,242 @@
import bpy
from bpy.types import ID, Object
import math
from typing import Tuple, Optional
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
SPECKLE_ID_LENGTH = 32
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = (
len(blender_object.name) > _QUICK_TEST_NAME_LENGTH
and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
)
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * macro_knotsu(nu)
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order
if flag == 1: # CU_NURB_ENDPOINT
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2: # CU_NURB_BEZIER
if order == 4:
k = 0.34
for a in range(pts_order):
knots[a] = math.floor(k)
k += 1.0 / 3.0
elif order == 3:
k = 0.6
for a in range(pts_order):
if a >= order and a <= point_count:
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(1, len(knots) - 1):
knots[a] = a - 1
knots[-1] = knots[-2]
def basis_nurb(
t: float,
order: int,
point_count: int,
knots: list[float],
basis: list[float],
start: int,
end: int,
) -> Tuple[int, int]:
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
# this part is order '1'
o2 = order + 1
for i in range(opp2):
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
basis[i] = 1.0
i1 = i - o2
if i1 < 0:
i1 = 0
i2 = i
i += 1
while i < opp2:
basis[i] = 0.0
i += 1
break
else:
basis[i] = 0.0
basis[i] = 0.0 # type: ignore
# this is order 2, 3, ...
for j in range(2, order + 1):
if i2 + j >= orderpluspnts:
i2 = opp2 - j
for i in range(i1, i2 + 1):
if basis[i] != 0.0:
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
else:
d = 0.0
if basis[i + 1] != 0.0:
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
else:
e = 0.0
basis[i] = d + e
start = 1000
end = 0
for i in range(i1, i2 + 1):
if basis[i] > 0.0:
end = i
if start == 1000:
start = i
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""" "BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
sum_array = [0] * nu.point_count_u
basisu = [0.0] * macro_knotsu(nu)
knots = make_knots(nu)
resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1]
uend = (
knots[nu.point_count_u + nu.order_u - 1]
if nu.use_cyclic_u
else knots[nu.point_count_u]
)
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart
while resolu:
resolu -= 1
istart, iend = basis_nurb(
u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend
)
# /* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] # type: ignore
sumdiv += sum_array[sum_index]
sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0
for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv # type: ignore
sum_index += 1
coord_array[coord_index : coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
if sum_array[sum_index] != 0.0:
for j in range(3):
coord_array[coord_index + j] += (
sum_array[sum_index] * nu.points[pt_index].co[j]
)
sum_index += 1
coord_index += stride
u += ustep
return coord_array
def get_unique_id(native_object: ID, suffix: Optional[str] = None) -> str:
base_id = f"{type(native_object).__name__}:{native_object.name_full}"
if suffix:
return f"{base_id}:{suffix}"
return base_id
def get_submesh_id(blender_object: Object, material_index: int) -> str:
mesh_data = blender_object.data
if not mesh_data:
return f"Mesh:{blender_object.name_full}_mat{material_index}"
return f"Mesh:{mesh_data.name_full}_mat{material_index}"
def get_curve_element_id(blender_object: Object, curve_index: int = 0) -> str:
curve_data = blender_object.data
if not curve_data:
return f"Curve:{blender_object.name_full}_curve{curve_index}"
if curve_index == 0:
return f"Curve:{curve_data.name_full}"
return f"Curve:{curve_data.name_full}_curve{curve_index}"
def get_object_id(blender_object: Object) -> str:
return get_unique_id(blender_object)
+26 -4
View File
@@ -5,6 +5,7 @@ from specklepy.objects import Base
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from specklepy.core.api.client import SpeckleClient
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
@@ -59,13 +60,11 @@ def create_material_from_proxy(
1.0,
)
if hasattr(render_material, "opacity"):
opacity = float(render_material.opacity)
if opacity < 1.0:
material.blend_method = "BLEND"
bsdf.inputs["Alpha"].default_value = opacity
if hasattr(render_material, "metalness"):
metalness = float(render_material.metalness)
@@ -88,10 +87,15 @@ def create_material_from_proxy(
1.0,
)
bsdf.inputs["Emission Strength"].default_value = 1.0
# set viewport display color
if hasattr(render_material, "diffuse") and hasattr(render_material, "opacity"):
material.diffuse_color = (diffuse_rgba[0], diffuse_rgba[1], diffuse_rgba[2], opacity)
material.diffuse_color = (
diffuse_rgba[0],
diffuse_rgba[1],
diffuse_rgba[2],
opacity,
)
return material
@@ -183,3 +187,21 @@ def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
return None
return deep_search(root_object)
def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional[str]:
workspace_id = None
server_version = client.project.server_version or client.server.version()
# Local yarn builds of server will report a server version if "dev"
# We'll assume that local builds are up-to-date with the latest features
if server_version[0] == "dev":
maj = 999
min = 999
else:
maj = server_version[0]
min = server_version[1]
if maj > 2 or (maj == 2 and min > 20):
workspace_id = client.project.get(project_id).workspace_id
return workspace_id
+1 -1
View File
@@ -5,7 +5,7 @@ description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.11.9, <4.0.0"
license = "Apache-2.0"
dependencies = [
"specklepy==3.0.0a15",
"specklepy==3.0.0a16",
]
[dependency-groups]
Generated
+87 -87
View File
@@ -132,7 +132,7 @@ wheels = [
[[package]]
name = "gql"
version = "3.5.2"
version = "3.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -140,9 +140,9 @@ dependencies = [
{ name = "graphql-core" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/ef/5298d9d628b6a54b3b810052cb5a935d324fe28d9bfdeb741733d5c2446b/gql-3.5.2.tar.gz", hash = "sha256:07e1325b820c8ba9478e95de27ce9f23250486e7e79113dbb7659a442dc13e74", size = 180502 }
sdist = { url = "https://files.pythonhosted.org/packages/34/ed/44ffd30b06b3afc8274ee2f38c3c1b61fe4740bf03d92083e43d2c17ac77/gql-3.5.3.tar.gz", hash = "sha256:393b8c049d58e0d2f5461b9d738a2b5f904186a40395500b4a84dd092d56e42b", size = 180504 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/71/b028b937992056e721bbf0371e13819fcca0dacde7b3c821f775ed903917/gql-3.5.2-py2.py3-none-any.whl", hash = "sha256:c830ffc38b3997b2a146317b27758305ab3d0da3bde607b49f34e32affb23ba2", size = 74346 },
{ url = "https://files.pythonhosted.org/packages/cb/50/2f4e99b216821ac921dbebf91c644ba95818f5d07857acadee17220221f3/gql-3.5.3-py2.py3-none-any.whl", hash = "sha256:e1fcbde2893fcafdd28114ece87ff47f1cc339a31db271fc4e1d528f5a1d4fbc", size = 74348 },
]
[package.optional-dependencies]
@@ -156,11 +156,11 @@ websockets = [
[[package]]
name = "graphql-core"
version = "3.2.4"
version = "3.2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/9e/aa527fb09a9d7399d5d7d2aa2da490e4580707652d3b4fc156996ae88a5b/graphql-core-3.2.4.tar.gz", hash = "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264", size = 504611 }
sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/33/cc72c4c658c6316f188a60bc4e5a91cd4ceaaa8c3e7e691ac9297e4e72c7/graphql_core-3.2.4-py3-none-any.whl", hash = "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0", size = 203179 },
{ url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416 },
]
[[package]]
@@ -211,79 +211,79 @@ wheels = [
[[package]]
name = "multidict"
version = "6.4.3"
version = "6.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372 }
sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e0/53cf7f27eda48fffa53cfd4502329ed29e00efb9e4ce41362cbf8aa54310/multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", size = 65259 },
{ url = "https://files.pythonhosted.org/packages/44/79/1dcd93ce7070cf01c2ee29f781c42b33c64fce20033808f1cc9ec8413d6e/multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", size = 38451 },
{ url = "https://files.pythonhosted.org/packages/f4/35/2292cf29ab5f0d0b3613fad1b75692148959d3834d806be1885ceb49a8ff/multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad", size = 37706 },
{ url = "https://files.pythonhosted.org/packages/f6/d1/6b157110b2b187b5a608b37714acb15ee89ec773e3800315b0107ea648cd/multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", size = 226669 },
{ url = "https://files.pythonhosted.org/packages/40/7f/61a476450651f177c5570e04bd55947f693077ba7804fe9717ee9ae8de04/multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", size = 223182 },
{ url = "https://files.pythonhosted.org/packages/51/7b/eaf7502ac4824cdd8edcf5723e2e99f390c879866aec7b0c420267b53749/multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", size = 235025 },
{ url = "https://files.pythonhosted.org/packages/3b/f6/facdbbd73c96b67a93652774edd5778ab1167854fa08ea35ad004b1b70ad/multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", size = 231481 },
{ url = "https://files.pythonhosted.org/packages/70/57/c008e861b3052405eebf921fd56a748322d8c44dcfcab164fffbccbdcdc4/multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", size = 223492 },
{ url = "https://files.pythonhosted.org/packages/30/4d/7d8440d3a12a6ae5d6b202d6e7f2ac6ab026e04e99aaf1b73f18e6bc34bc/multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", size = 217279 },
{ url = "https://files.pythonhosted.org/packages/7f/e7/bca0df4dd057597b94138d2d8af04eb3c27396a425b1b0a52e082f9be621/multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", size = 228733 },
{ url = "https://files.pythonhosted.org/packages/88/f5/383827c3f1c38d7c92dbad00a8a041760228573b1c542fbf245c37bbca8a/multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", size = 218089 },
{ url = "https://files.pythonhosted.org/packages/36/8a/a5174e8a7d8b94b4c8f9c1e2cf5d07451f41368ffe94d05fc957215b8e72/multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", size = 225257 },
{ url = "https://files.pythonhosted.org/packages/8c/76/1d4b7218f0fd00b8e5c90b88df2e45f8af127f652f4e41add947fa54c1c4/multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", size = 234728 },
{ url = "https://files.pythonhosted.org/packages/64/44/18372a4f6273fc7ca25630d7bf9ae288cde64f29593a078bff450c7170b6/multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", size = 230087 },
{ url = "https://files.pythonhosted.org/packages/0f/ae/28728c314a698d8a6d9491fcacc897077348ec28dd85884d09e64df8a855/multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", size = 223137 },
{ url = "https://files.pythonhosted.org/packages/22/50/785bb2b3fe16051bc91c70a06a919f26312da45c34db97fc87441d61e343/multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378", size = 34959 },
{ url = "https://files.pythonhosted.org/packages/2f/63/2a22e099ae2f4d92897618c00c73a09a08a2a9aa14b12736965bf8d59fd3/multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589", size = 38541 },
{ url = "https://files.pythonhosted.org/packages/fc/bb/3abdaf8fe40e9226ce8a2ba5ecf332461f7beec478a455d6587159f1bf92/multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", size = 64019 },
{ url = "https://files.pythonhosted.org/packages/7e/b5/1b2e8de8217d2e89db156625aa0fe4a6faad98972bfe07a7b8c10ef5dd6b/multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", size = 37925 },
{ url = "https://files.pythonhosted.org/packages/b4/e2/3ca91c112644a395c8eae017144c907d173ea910c913ff8b62549dcf0bbf/multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", size = 37008 },
{ url = "https://files.pythonhosted.org/packages/60/23/79bc78146c7ac8d1ac766b2770ca2e07c2816058b8a3d5da6caed8148637/multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", size = 224374 },
{ url = "https://files.pythonhosted.org/packages/86/35/77950ed9ebd09136003a85c1926ba42001ca5be14feb49710e4334ee199b/multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", size = 230869 },
{ url = "https://files.pythonhosted.org/packages/49/97/2a33c6e7d90bc116c636c14b2abab93d6521c0c052d24bfcc231cbf7f0e7/multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", size = 231949 },
{ url = "https://files.pythonhosted.org/packages/56/ce/e9b5d9fcf854f61d6686ada7ff64893a7a5523b2a07da6f1265eaaea5151/multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", size = 231032 },
{ url = "https://files.pythonhosted.org/packages/f0/ac/7ced59dcdfeddd03e601edb05adff0c66d81ed4a5160c443e44f2379eef0/multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", size = 223517 },
{ url = "https://files.pythonhosted.org/packages/db/e6/325ed9055ae4e085315193a1b58bdb4d7fc38ffcc1f4975cfca97d015e17/multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", size = 216291 },
{ url = "https://files.pythonhosted.org/packages/fa/84/eeee6d477dd9dcb7691c3bb9d08df56017f5dd15c730bcc9383dcf201cf4/multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", size = 228982 },
{ url = "https://files.pythonhosted.org/packages/82/94/4d1f3e74e7acf8b0c85db350e012dcc61701cd6668bc2440bb1ecb423c90/multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", size = 226823 },
{ url = "https://files.pythonhosted.org/packages/09/f0/1e54b95bda7cd01080e5732f9abb7b76ab5cc795b66605877caeb2197476/multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", size = 222714 },
{ url = "https://files.pythonhosted.org/packages/e7/a2/f6cbca875195bd65a3e53b37ab46486f3cc125bdeab20eefe5042afa31fb/multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", size = 233739 },
{ url = "https://files.pythonhosted.org/packages/79/68/9891f4d2b8569554723ddd6154375295f789dc65809826c6fb96a06314fd/multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", size = 230809 },
{ url = "https://files.pythonhosted.org/packages/e6/72/a7be29ba1e87e4fc5ceb44dabc7940b8005fd2436a332a23547709315f70/multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", size = 226934 },
{ url = "https://files.pythonhosted.org/packages/12/c1/259386a9ad6840ff7afc686da96808b503d152ac4feb3a96c651dc4f5abf/multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", size = 35242 },
{ url = "https://files.pythonhosted.org/packages/06/24/c8fdff4f924d37225dc0c56a28b1dca10728fc2233065fafeb27b4b125be/multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", size = 38635 },
{ url = "https://files.pythonhosted.org/packages/6c/4b/86fd786d03915c6f49998cf10cd5fe6b6ac9e9a071cb40885d2e080fb90d/multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", size = 63831 },
{ url = "https://files.pythonhosted.org/packages/45/05/9b51fdf7aef2563340a93be0a663acba2c428c4daeaf3960d92d53a4a930/multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", size = 37888 },
{ url = "https://files.pythonhosted.org/packages/0b/43/53fc25394386c911822419b522181227ca450cf57fea76e6188772a1bd91/multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", size = 36852 },
{ url = "https://files.pythonhosted.org/packages/8a/68/7b99c751e822467c94a235b810a2fd4047d4ecb91caef6b5c60116991c4b/multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", size = 223644 },
{ url = "https://files.pythonhosted.org/packages/80/1b/d458d791e4dd0f7e92596667784fbf99e5c8ba040affe1ca04f06b93ae92/multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", size = 230446 },
{ url = "https://files.pythonhosted.org/packages/e2/46/9793378d988905491a7806d8987862dc5a0bae8a622dd896c4008c7b226b/multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", size = 231070 },
{ url = "https://files.pythonhosted.org/packages/a7/b8/b127d3e1f8dd2a5bf286b47b24567ae6363017292dc6dec44656e6246498/multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", size = 229956 },
{ url = "https://files.pythonhosted.org/packages/0c/93/f70a4c35b103fcfe1443059a2bb7f66e5c35f2aea7804105ff214f566009/multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", size = 222599 },
{ url = "https://files.pythonhosted.org/packages/63/8c/e28e0eb2fe34921d6aa32bfc4ac75b09570b4d6818cc95d25499fe08dc1d/multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", size = 216136 },
{ url = "https://files.pythonhosted.org/packages/72/f5/fbc81f866585b05f89f99d108be5d6ad170e3b6c4d0723d1a2f6ba5fa918/multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", size = 228139 },
{ url = "https://files.pythonhosted.org/packages/bb/ba/7d196bad6b85af2307d81f6979c36ed9665f49626f66d883d6c64d156f78/multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", size = 226251 },
{ url = "https://files.pythonhosted.org/packages/cc/e2/fae46a370dce79d08b672422a33df721ec8b80105e0ea8d87215ff6b090d/multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", size = 221868 },
{ url = "https://files.pythonhosted.org/packages/26/20/bbc9a3dec19d5492f54a167f08546656e7aef75d181d3d82541463450e88/multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", size = 233106 },
{ url = "https://files.pythonhosted.org/packages/ee/8d/f30ae8f5ff7a2461177f4d8eb0d8f69f27fb6cfe276b54ec4fd5a282d918/multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", size = 230163 },
{ url = "https://files.pythonhosted.org/packages/15/e9/2833f3c218d3c2179f3093f766940ded6b81a49d2e2f9c46ab240d23dfec/multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", size = 225906 },
{ url = "https://files.pythonhosted.org/packages/f1/31/6edab296ac369fd286b845fa5dd4c409e63bc4655ed8c9510fcb477e9ae9/multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", size = 35238 },
{ url = "https://files.pythonhosted.org/packages/23/57/2c0167a1bffa30d9a1383c3dab99d8caae985defc8636934b5668830d2ef/multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", size = 38799 },
{ url = "https://files.pythonhosted.org/packages/c9/13/2ead63b9ab0d2b3080819268acb297bd66e238070aa8d42af12b08cbee1c/multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", size = 68642 },
{ url = "https://files.pythonhosted.org/packages/85/45/f1a751e1eede30c23951e2ae274ce8fad738e8a3d5714be73e0a41b27b16/multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", size = 40028 },
{ url = "https://files.pythonhosted.org/packages/a7/29/fcc53e886a2cc5595cc4560df333cb9630257bda65003a7eb4e4e0d8f9c1/multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", size = 39424 },
{ url = "https://files.pythonhosted.org/packages/f6/f0/056c81119d8b88703971f937b371795cab1407cd3c751482de5bfe1a04a9/multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", size = 226178 },
{ url = "https://files.pythonhosted.org/packages/a3/79/3b7e5fea0aa80583d3a69c9d98b7913dfd4fbc341fb10bb2fb48d35a9c21/multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", size = 222617 },
{ url = "https://files.pythonhosted.org/packages/06/db/3ed012b163e376fc461e1d6a67de69b408339bc31dc83d39ae9ec3bf9578/multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", size = 227919 },
{ url = "https://files.pythonhosted.org/packages/b1/db/0433c104bca380989bc04d3b841fc83e95ce0c89f680e9ea4251118b52b6/multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", size = 226097 },
{ url = "https://files.pythonhosted.org/packages/c2/95/910db2618175724dd254b7ae635b6cd8d2947a8b76b0376de7b96d814dab/multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", size = 220706 },
{ url = "https://files.pythonhosted.org/packages/d1/af/aa176c6f5f1d901aac957d5258d5e22897fe13948d1e69063ae3d5d0ca01/multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", size = 211728 },
{ url = "https://files.pythonhosted.org/packages/e7/42/d51cc5fc1527c3717d7f85137d6c79bb7a93cd214c26f1fc57523774dbb5/multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", size = 226276 },
{ url = "https://files.pythonhosted.org/packages/28/6b/d836dea45e0b8432343ba4acf9a8ecaa245da4c0960fb7ab45088a5e568a/multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", size = 212069 },
{ url = "https://files.pythonhosted.org/packages/55/34/0ee1a7adb3560e18ee9289c6e5f7db54edc312b13e5c8263e88ea373d12c/multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", size = 217858 },
{ url = "https://files.pythonhosted.org/packages/04/08/586d652c2f5acefe0cf4e658eedb4d71d4ba6dfd4f189bd81b400fc1bc6b/multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", size = 226988 },
{ url = "https://files.pythonhosted.org/packages/82/e3/cc59c7e2bc49d7f906fb4ffb6d9c3a3cf21b9f2dd9c96d05bef89c2b1fd1/multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", size = 220435 },
{ url = "https://files.pythonhosted.org/packages/e0/32/5c3a556118aca9981d883f38c4b1bfae646f3627157f70f4068e5a648955/multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", size = 221494 },
{ url = "https://files.pythonhosted.org/packages/b9/3b/1599631f59024b75c4d6e3069f4502409970a336647502aaf6b62fb7ac98/multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", size = 41775 },
{ url = "https://files.pythonhosted.org/packages/e8/4e/09301668d675d02ca8e8e1a3e6be046619e30403f5ada2ed5b080ae28d02/multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", size = 45946 },
{ url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400 },
{ url = "https://files.pythonhosted.org/packages/19/1b/4c6e638195851524a63972c5773c7737bea7e47b1ba402186a37773acee2/multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95", size = 65515 },
{ url = "https://files.pythonhosted.org/packages/25/d5/10e6bca9a44b8af3c7f920743e5fc0c2bcf8c11bf7a295d4cfe00b08fb46/multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a", size = 38609 },
{ url = "https://files.pythonhosted.org/packages/26/b4/91fead447ccff56247edc7f0535fbf140733ae25187a33621771ee598a18/multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223", size = 37871 },
{ url = "https://files.pythonhosted.org/packages/3b/37/cbc977cae59277e99d15bbda84cc53b5e0c4929ffd91d958347200a42ad0/multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44", size = 226661 },
{ url = "https://files.pythonhosted.org/packages/15/cd/7e0b57fbd4dc2fc105169c4ecce5be1a63970f23bb4ec8c721b67e11953d/multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065", size = 223422 },
{ url = "https://files.pythonhosted.org/packages/f1/01/1de268da121bac9f93242e30cd3286f6a819e5f0b8896511162d6ed4bf8d/multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f", size = 235447 },
{ url = "https://files.pythonhosted.org/packages/d2/8c/8b9a5e4aaaf4f2de14e86181a3a3d7b105077f668b6a06f043ec794f684c/multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a", size = 231455 },
{ url = "https://files.pythonhosted.org/packages/35/db/e1817dcbaa10b319c412769cf999b1016890849245d38905b73e9c286862/multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2", size = 223666 },
{ url = "https://files.pythonhosted.org/packages/4a/e1/66e8579290ade8a00e0126b3d9a93029033ffd84f0e697d457ed1814d0fc/multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1", size = 217392 },
{ url = "https://files.pythonhosted.org/packages/7b/6f/f8639326069c24a48c7747c2a5485d37847e142a3f741ff3340c88060a9a/multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42", size = 228969 },
{ url = "https://files.pythonhosted.org/packages/d2/c3/3d58182f76b960eeade51c89fcdce450f93379340457a328e132e2f8f9ed/multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e", size = 217433 },
{ url = "https://files.pythonhosted.org/packages/e1/4b/f31a562906f3bd375f3d0e83ce314e4a660c01b16c2923e8229b53fba5d7/multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd", size = 225418 },
{ url = "https://files.pythonhosted.org/packages/99/89/78bb95c89c496d64b5798434a3deee21996114d4d2c28dd65850bf3a691e/multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925", size = 235042 },
{ url = "https://files.pythonhosted.org/packages/74/91/8780a6e5885a8770442a8f80db86a0887c4becca0e5a2282ba2cae702bc4/multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c", size = 230280 },
{ url = "https://files.pythonhosted.org/packages/68/c1/fcf69cabd542eb6f4b892469e033567ee6991d361d77abdc55e3a0f48349/multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08", size = 223322 },
{ url = "https://files.pythonhosted.org/packages/b8/85/5b80bf4b83d8141bd763e1d99142a9cdfd0db83f0739b4797172a4508014/multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49", size = 35070 },
{ url = "https://files.pythonhosted.org/packages/09/66/0bed198ffd590ab86e001f7fa46b740d58cf8ff98c2f254e4a36bf8861ad/multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529", size = 38667 },
{ url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293 },
{ url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096 },
{ url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214 },
{ url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686 },
{ url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061 },
{ url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412 },
{ url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563 },
{ url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811 },
{ url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524 },
{ url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012 },
{ url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765 },
{ url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888 },
{ url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041 },
{ url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046 },
{ url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106 },
{ url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351 },
{ url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791 },
{ url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123 },
{ url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049 },
{ url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078 },
{ url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097 },
{ url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768 },
{ url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331 },
{ url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169 },
{ url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947 },
{ url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761 },
{ url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605 },
{ url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144 },
{ url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100 },
{ url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731 },
{ url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637 },
{ url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594 },
{ url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359 },
{ url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903 },
{ url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895 },
{ url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183 },
{ url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592 },
{ url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071 },
{ url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597 },
{ url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253 },
{ url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146 },
{ url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585 },
{ url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080 },
{ url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558 },
{ url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168 },
{ url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970 },
{ url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980 },
{ url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641 },
{ url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728 },
{ url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913 },
{ url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112 },
{ url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481 },
]
[[package]]
@@ -361,7 +361,7 @@ wheels = [
[[package]]
name = "pydantic"
version = "2.11.4"
version = "2.11.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -369,9 +369,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540 }
sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900 },
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 },
]
[[package]]
@@ -537,7 +537,7 @@ dev = [
]
[package.metadata]
requires-dist = [{ name = "specklepy", specifier = "==3.0.0a15" }]
requires-dist = [{ name = "specklepy", specifier = "==3.0.0a16" }]
[package.metadata.requires-dev]
dev = [
@@ -547,7 +547,7 @@ dev = [
[[package]]
name = "specklepy"
version = "3.0.0a15"
version = "3.0.0a16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "appdirs" },
@@ -559,9 +559,9 @@ dependencies = [
{ name = "pydantic-settings" },
{ name = "ujson" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/00/ef793063b92c4e998bae227cf3a94c640181ac63f385a5be2b15ff806f0d/specklepy-3.0.0a15.tar.gz", hash = "sha256:60bfce016d58e31f7f4b5b8bb61cc8528dc0404b1246fac96bc961ee46cb0816", size = 199633 }
sdist = { url = "https://files.pythonhosted.org/packages/54/8f/8730b78c6e669117d7c97489e47bd8b75b93aabadc03020dd2aad7ab198c/specklepy-3.0.0a16.tar.gz", hash = "sha256:3f5949758cbc8b30d7b1ec21094e0457be4491a90dd2450bc9c15f3b40cbf5cc", size = 200721 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/f5/a8acbfa11b9d8a02baf291fd36ad67d495f90c862b510133f88e7cc59861/specklepy-3.0.0a15-py3-none-any.whl", hash = "sha256:129816063ef692e8566e95e8201b3dabfb8835a67aa91d05bd6d2b10ed8449fa", size = 100948 },
{ url = "https://files.pythonhosted.org/packages/83/59/895e4a980a75c27d39c2cf453bb16af54b70a8d1e75d6ed4e2ac960747e4/specklepy-3.0.0a16-py3-none-any.whl", hash = "sha256:919feae14e319524c89063c22fd34aaa45deafb3d8c8b3f06094c82af4873e8c", size = 101009 },
]
[[package]]
@@ -575,14 +575,14 @@ wheels = [
[[package]]
name = "typing-inspection"
version = "0.4.0"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
]
[[package]]