Init Pymesh Base

This commit is contained in:
Jonathon Broughton
2023-11-12 14:51:53 +00:00
parent 83def46696
commit 97dfafcaa4
15 changed files with 490 additions and 40 deletions
+7 -22
View File
@@ -1,39 +1,24 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "Python 3",
"name": "speckle-automate-pymesh-spatial-joins",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
"features": {
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
},
"remoteEnv": {
"SPECKLE_TOKEN": "foobar"
},
"containerEnv": {
"SPECKLE_TOKEN": "asdfasdf"
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
"dockerFile": "../Dockerfile",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp .env.example .env && POETRY_VIRTUALENVS_IN_PROJECT=true poetry install --no-root",
// "postCreateCommand": "poetry install --no-root",
// Configure tool-specific properties.
"customizations": {
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.vscode-pylance",
"ms-python.python",
"ms-python.black-formatter",
"mikestead.dotenv",
"streetsidesoftware.code-spell-checker",
"mikestead.dotenv"
"ms-python.vscode-pylance",
"ms-python.black-formatter",
"charliermarsh.ruff"
]
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4.1.1
- uses: actions/setup-python@v4
with:
python-version: '3.11'
python-version: '3.10'
- name: Install and configure Poetry
uses: snok/install-poetry@v1
with:
+236 -3
View File
@@ -1,5 +1,237 @@
# We use the official Python 3.11 image as our base image and will add our code to it. For more details, see https://hub.docker.com/_/python
FROM python:3.11-slim
FROM debian:bullseye-slim
# Install basic packages
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg \
netbase \
wget; \
rm -rf /var/lib/apt/lists/*
# Install version control systems and procps
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
mercurial \
openssh-client \
subversion \
procps; \
rm -rf /var/lib/apt/lists/*
# Install various development tools and libraries
RUN set -ex; \
apt-get update; \
apt-get install -y --no-install-recommends \
autoconf \
automake \
bzip2 \
dpkg-dev \
file \
g++ \
gcc \
imagemagick \
libbz2-dev \
libc6-dev \
libcurl4-openssl-dev \
libdb-dev \
libevent-dev \
libffi-dev \
libgdbm-dev \
libglib2.0-dev \
libgmp-dev \
libjpeg-dev \
libkrb5-dev \
liblzma-dev \
libmagickcore-dev \
libmagickwand-dev \
libmaxminddb-dev \
libncurses5-dev \
libncursesw5-dev \
libpng-dev \
libpq-dev \
libreadline-dev \
libsqlite3-dev \
libssl-dev \
libtool \
libwebp-dev \
libxml2-dev \
libxslt-dev \
libyaml-dev \
make \
patch \
unzip \
xz-utils \
zlib1g-dev \
$(if apt-cache show 'default-libmysqlclient-dev' 2>/dev/null | grep -q '^Version:'; then \
echo 'default-libmysqlclient-dev'; \
else \
echo 'libmysqlclient-dev'; \
fi); \
rm -rf /var/lib/apt/lists/*
# Set environment variables
ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV LANG=C.UTF-8
# Install additional libraries
RUN /bin/sh -c set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
libbluetooth-dev \
tk-dev \
uuid-dev; \
rm -rf /var/lib/apt/lists/*
# Set GPG key and Python version
ENV GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
ENV PYTHON_VERSION=3.10.13
# Install Python from source
RUN /bin/sh -c set -eux; \
wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz"; \
wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc"; \
GNUPGHOME="$(mktemp -d)"; export GNUPGHOME; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$GPG_KEY"; \
gpg --batch --verify python.tar.xz.asc python.tar.xz; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" python.tar.xz.asc; \
mkdir -p /usr/src/python; \
tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; \
rm python.tar.xz; \
cd /usr/src/python; \
gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
./configure \
--build="$gnuArch" \
--enable-loadable-sqlite-extensions \
--enable-optimizations \
--enable-option-checking=fatal \
--enable-shared \
--with-lto \
--with-system-expat \
--without-ensurepip; \
nproc="$(nproc)"; \
EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"; \
LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"; \
make -j "$nproc" \
"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
"LDFLAGS=${LDFLAGS:-}" \
"PROFILE_TASK=${PROFILE_TASK:-}"; \
rm python; \
make -j "$nproc" \
"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
"LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" \
"PROFILE_TASK=${PROFILE_TASK:-}" \
python; \
make install; \
bin="$(readlink -ve /usr/local/bin/python3)"; \
dir="$(dirname "$bin")"; \
mkdir -p "/usr/share/gdb/auto-load/$dir"; \
cp -vL Tools/gdb/libpython.py "/usr/share/gdb/auto-load/$bin-gdb.py"; \
cd /; \
rm -rf /usr/src/python; \
find /usr/local -depth \
\( \
\( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \
-o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \) \) \
\) -exec rm -rf '{}' +; \
ldconfig; \
python3 --version
# Create symlinks for Python tools
RUN /bin/sh -c set -eux; \
for src in idle3 pydoc3 python3 python3-config; do \
dst="$(echo "$src" | tr -d 3)"; \
[ -s "/usr/local/bin/$src" ]; \
[ ! -e "/usr/local/bin/$dst" ]; \
ln -svT "$src" "/usr/local/bin/$dst"; \
done
# Set Python pip and setuptools versions and install pip
ENV PYTHON_PIP_VERSION=23.0.1
ENV PYTHON_SETUPTOOLS_VERSION=65.5.1
ENV PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/c6add47b0abf67511cdfb4734771cbab403af062/public/get-pip.py
ENV PYTHON_GET_PIP_SHA256=22b849a10f86f5ddf7ce148ca2a31214504ee6c83ef626840fde6e5dcd809d11
RUN /bin/sh -c set -eux; \
wget -O get-pip.py "$PYTHON_GET_PIP_URL"; \
echo "$PYTHON_GET_PIP_SHA256 *get-pip.py" | sha256sum -c -; \
export PYTHONDONTWRITEBYTECODE=1; \
python get-pip.py \
--disable-pip-version-check \
--no-cache-dir \
--no-compile \
"pip==$PYTHON_PIP_VERSION" \
"setuptools==$PYTHON_SETUPTOOLS_VERSION"; \
rm -f get-pip.py; \
pip --version
# Set working directory
WORKDIR /root/
# Set build arguments
ARG BRANCH=main
ARG NUM_CORES=10
# Clone a git repository
RUN /bin/sh -c "git clone --single-branch --depth 1 -b $BRANCH https://github.com/nuvolos-cloud/PyMesh.git"
# Set environment variables
ENV PYMESH_PATH=/root/PyMesh
ENV NUM_CORES=10
# Install additional packages
RUN echo "deb http://ftp.us.debian.org/debian unstable main contrib non-free" >> /etc/apt/sources.list.d/unstable.list && \
apt-get update && apt-get install -y \
gcc-9 \
g++-9 \
git \
cmake \
libgmp-dev \
libmpfr-dev \
libgmpxx4ldbl \
libboost-dev \
libboost-thread-dev \
zip unzip patchelf && \
apt-get clean
# Build and install PyMesh
WORKDIR /root/PyMesh
RUN git clone --depth 1 https://github.com/PyMesh/cgal.git $PYMESH_PATH/third_party/cgal
RUN git clone --depth 1 https://github.com/PyMesh/libigl.git $PYMESH_PATH/third_party/libigl
RUN git clone --depth 1 https://github.com/PyMesh/carve.git $PYMESH_PATH/third_party/carve
RUN git clone --depth 1 https://github.com/PyMesh/cork.git $PYMESH_PATH/third_party/cork
RUN git clone --depth 1 https://github.com/PyMesh/tetgen.git $PYMESH_PATH/third_party/tetgen
RUN git clone --depth 1 https://github.com/PyMesh/qhull.git $PYMESH_PATH/third_party/qhull
RUN git clone --depth 1 https://github.com/PyMesh/Clipper.git $PYMESH_PATH/third_party/Clipper
RUN git clone --depth 1 https://github.com/PyMesh/eigen.git $PYMESH_PATH/third_party/eigen
RUN git clone --depth 1 https://github.com/PyMesh/pybind11.git $PYMESH_PATH/third_party/pybind11
RUN git clone --depth 1 https://github.com/PyMesh/geogram.git $PYMESH_PATH/third_party/geogram
RUN git clone --depth 1 https://github.com/PyMesh/draco.git $PYMESH_PATH/third_party/draco
RUN git clone --depth 1 https://github.com/PyMesh/TetWild.git $PYMESH_PATH/third_party/TetWild
RUN git clone --depth 1 https://github.com/PyMesh/WindingNumber.git $PYMESH_PATH/third_party/WindingNumber
RUN git clone --depth 1 https://github.com/PyMesh/tbb.git $PYMESH_PATH/third_party/tbb
RUN git clone --depth 1 https://github.com/PyMesh/jigsaw.git $PYMESH_PATH/third_party/jigsaw
RUN git clone --depth 1 https://github.com/fmtlib/fmt.git $PYMESH_PATH/third_party/fmt
RUN git clone --depth 1 https://github.com/gabime/spdlog.git $PYMESH_PATH/third_party/spdlog
RUN git submodule update --init third_party/triangle
RUN git submodule update --init third_party/quartet
RUN git submodule update --init third_party/mmg
RUN git submodule update --init third_party/json
RUN pip install -r $PYMESH_PATH/python/requirements.txt
RUN ./setup.py bdist_wheel
RUN rm -rf build_3.7 third_party/build
RUN python $PYMESH_PATH/docker/patches/patch_wheel.py dist/pymesh2*.whl
RUN pip install --upgrade pip
RUN pip install dist/pymesh2*.whl
# Build third-party libraries for PyMesh
WORKDIR /root/PyMesh/third_party
RUN /bin/sh -c "python ./build.py mmg && python ./build.py tetgen"
# We install poetry to generate a list of dependencies which will be required by our application
RUN pip install poetry
@@ -13,4 +245,5 @@ WORKDIR /home/speckle
COPY . /home/speckle
# Using poetry, we generate a list of requirements, save them to requirements.txt, and then use pip to install them
RUN poetry export --format requirements.txt --output /home/speckle/requirements.txt && pip install --requirement /home/speckle/requirements.txt
RUN poetry export --format requirements.txt --output /home/speckle/requirements.txt --without-hashes && \
pip install --requirement /home/speckle/requirements.txt
View File
+107
View File
@@ -0,0 +1,107 @@
from typing import Tuple, Optional
import numpy as np
import trimesh
from specklepy.objects import Base
from specklepy.objects.geometry import Mesh as SpeckleMesh
from specklepy.objects.other import Transform
from trimesh import Trimesh
class Element:
def __init__(self, id, meshes):
"""
Initialize an Element object with an ID and a list of meshes.
Args:
id (str): The ID of the Element.
meshes (List[Trimesh]): List of trimesh Mesh objects.
"""
self.id = id
self.meshes = meshes
def speckle_transform_to_trimesh_matrix(transform: Transform) -> np.ndarray:
"""
Convert the Speckle Transform matrix to a NumPy array format suitable for trimesh.
Returns:
np.ndarray: 4x4 transformation matrix in NumPy array format.
"""
return np.array(transform.value).reshape(4, 4)
def speckle_to_element(
base_with_transforms: Tuple[Base, str, Optional[Transform]]
) -> Element:
"""
Convert a SpecklePy Base object and its associated Transform to an Element object.
Args:
base_with_transforms (tuple): Contains a SpecklePy Base object and its
associated Transform object.
Returns:
Element: The resulting Element object.
"""
# Unpack the tuple to get the base, speckle ID, and transform.
base, speckle_id, transform = base_with_transforms
# To convert the Base object to a trimesh Mesh, use the displayValue property.
# This property provides the display mesh, expected to be an iterable of
# SpecklePy Mesh objects. However, legacy objects might be a single mesh.
display_value = base.displayValue
if isinstance(display_value, SpeckleMesh):
display_value = [display_value]
if isinstance(display_value, list):
# Initialize an Element with an empty list of meshes.
element = Element(speckle_id, meshes=[])
for mesh in display_value:
if mesh:
# Convert the SpecklePy Mesh to a trimesh Mesh.
t_mesh = speckle_to_trimesh(mesh)
if not isinstance(t_mesh, Trimesh):
continue
# If there's a transform, apply it to the trimesh Mesh.
if transform is not None:
trimesh_matrix = speckle_transform_to_trimesh_matrix(transform)
t_mesh.apply_transform(trimesh_matrix)
# Append the trimesh Mesh to the Element's list of meshes.
element.meshes.append(t_mesh)
return element
def speckle_to_trimesh(speckle_mesh: SpeckleMesh) -> Trimesh:
"""
Convert a SpecklePy Mesh to a trimesh Mesh object.
Args:
speckle_mesh: The SpecklePy Mesh to convert.
Returns:
trimesh.Trimesh: The resulting trimesh Mesh object.
"""
# Convert the list of vertices to a numpy array. Reshape it to
# (num_vertices, 3) to fit the trimesh format.
vertices_array = np.array(speckle_mesh.vertices).reshape((-1, 3))
# Faces are expected to be triangular. Reshape the faces list accordingly.
# Convert the faces list to a numpy array
faces_array_raw = np.array(speckle_mesh.faces)
# Remove the leading 3s by skipping every 4th value
faces_cleaned = np.delete(faces_array_raw, np.arange(0, faces_array_raw.size, 4))
# Reshape the array into (-1, 3) shape
faces_array = faces_cleaned.reshape((-1, 3))
# Return a new trimesh object using the reshaped vertices and faces.
return trimesh.Trimesh(vertices=vertices_array, faces=faces_array)
+16
View File
@@ -0,0 +1,16 @@
import pymesh
vertices = [
[0, 0, 0],
[1, 0, 0],
[1, 1, 0],
[0, 1, 0]
]
faces = [
[0, 1, 2],
[0, 2, 3]
]
mesh = pymesh.form_mesh(vertices, faces)
View File
View File
+51
View File
@@ -0,0 +1,51 @@
# Required imports
from typing import Callable, List, Union
from specklepy.objects import Base
# We're going to define a set of rules that will allow us to filter and
# process parameters in our Speckle objects. These rules will be encapsulated
# in a class called `ParameterRules`.
class ElementCheckRules:
"""A collection of rules for processing parameters in Speckle objects.
This class provides static methods that return lambda functions. These
lambda functions serve as filters or conditions we can use in our main
processing logic. By encapsulating these rules, we can easily extend
or modify them in the future.
"""
@staticmethod
def rule_combiner(*rules: Callable[[Base], bool]) -> Callable[[Base], bool]:
def combined(obj: Base) -> bool:
return all(rule(obj) for rule in rules)
return combined
@staticmethod
def is_displayable_rule() -> Callable[[Base], bool]:
"""Rule: Check if a parameter is displayable."""
return (
lambda parameter: parameter.displayValue
and parameter.displayValue is not None
)
@staticmethod
def speckle_type_rule(
desired_type: Union[str, List[str]]
) -> Callable[[Base], bool]:
"""Rule: Check if a parameter's speckle_type matches the desired type."""
# Convert single string to list for consistent handling
if isinstance(desired_type, str):
desired_type = [desired_type]
print(desired_type)
return (
lambda speckle_object: getattr(speckle_object, "speckle_type", None)
in desired_type
)
View File
+71
View File
@@ -0,0 +1,71 @@
"""Helper module for a simple speckle object tree flattening."""
from typing import Tuple, Optional
from specklepy.objects import Base
from specklepy.objects.other import Instance, Transform
# def flatten_base(base: Base) -> Iterable[Base]:
# """Take a base and flatten it to an iterable of bases."""
# if hasattr(base, "elements") and base["elements"] is not None:
# for element in base["elements"]:
# yield from flatten_base(element)
# yield base
def extract_base_and_transform(
base: Base,
inherited_instance_id: Optional[str] = None,
transform_list: Optional[List[Transform]] = None,
) -> Tuple[Base, str, Optional[List[Transform]]]:
"""
Traverses Speckle object hierarchies to yield `Base` objects and their transformations.
Tailored to Speckle's AEC data structures, it covers the newer hierarchical structures
with Collections and also with patterns found in older Revit specific data.
Parameters:
- base (Base): The starting point `Base` object for traversal.
- inherited_instance_id (str, optional): The inherited identifier for `Base` objects without a unique ID.
- transform_list (List[Transform], optional): Accumulated list of transformations from parent to child objects.
Yields:
- tuple: A `Base` object, its identifier, and a list of applicable `Transform` objects or None.
The id of the `Base` object is either the inherited identifier for a definition from an instance
or the one defined in the object.
"""
# Derive the identifier for the current `Base` object, defaulting to an inherited one if needed.
current_id = getattr(base, "id", inherited_instance_id)
transform_list = transform_list or []
if isinstance(base, Instance):
# Append transformation data and dive into the definition of `Instance` objects.
if base.transform:
transform_list.append(base.transform)
if base.definition:
yield from extract_base_and_transform(
base.definition, current_id, transform_list.copy()
)
else:
# Initial yield for the current `Base` object.
yield base, current_id, transform_list
# Process 'elements' and '@elements', typical containers for `Base` objects in AEC models.
elements_attr = getattr(base, "elements", []) or getattr(base, "@elements", [])
for element in elements_attr:
if isinstance(element, Base):
# Recurse into each `Base` object within 'elements' or '@elements'.
yield from extract_base_and_transform(
element, current_id, transform_list.copy()
)
# Recursively process '@'-prefixed properties that are Base objects with 'elements'.
# This is a common pattern in older Speckle data models, such as those used for Revit commits.
for attr_name in dir(base):
if attr_name.startswith("@"):
attr_value = getattr(base, attr_name)
# If the attribute is a Base object containing 'elements', recurse into it.
if isinstance(attr_value, Base) and hasattr(attr_value, "elements"):
yield from extract_base_and_transform(
attr_value, current_id, transform_list.copy()
)
-13
View File
@@ -1,13 +0,0 @@
"""Helper module for a simple speckle object tree flattening."""
from collections.abc import Iterable
from specklepy.objects import Base
def flatten_base(base: Base) -> Iterable[Base]:
"""Take a base and flatten it to an iterable of bases."""
if hasattr(base, "elements"):
for element in base["elements"]:
yield from flatten_base(element)
yield base
+1 -1
View File
@@ -10,7 +10,7 @@ from speckle_automate import (
execute_automate_function,
)
from flatten import flatten_base
from Utilities.flatten import flatten_base
class FunctionInputs(AutomateBase):
View File
View File