Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f5631cd90 | |||
| af50afe3ff | |||
| b6493df77f | |||
| 59d3c8c3ea | |||
| 4e3405f1fb | |||
| 49eabdd712 | |||
| 96a31f0678 | |||
| 91506b0b20 | |||
| b0de9e31b5 | |||
| 2075783134 | |||
| 071f2449c3 | |||
| a419664461 | |||
| 4a0c07009b | |||
| 682bcbfa9f | |||
| ccf284e8fa | |||
| 23102a28ff | |||
| 5475edb253 | |||
| c52f80c1ef | |||
| 21eecfa24c | |||
| 5dde1bfcf1 | |||
| 82c9d874c9 | |||
| 9acf2c8a92 | |||
| 95012e60c1 | |||
| 19b6500bbd | |||
| e5a8b40bb2 |
+13
-4
@@ -2,14 +2,15 @@ version: 2.1
|
||||
|
||||
orbs:
|
||||
python: circleci/python@1.3.2
|
||||
codecov: codecov/codecov@3.2.2
|
||||
|
||||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: "cimg/python:<<parameters.tag>>"
|
||||
- image: "circleci/node:12"
|
||||
- image: "circleci/redis:6"
|
||||
- image: "circleci/postgres:12"
|
||||
- image: 'cimg/node:14.18'
|
||||
- image: 'circleci/redis:6'
|
||||
- image: 'cimg/postgres:12.8'
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
@@ -38,7 +39,15 @@ jobs:
|
||||
name: upgrade pip
|
||||
- python/install-packages:
|
||||
pkg-manager: poetry
|
||||
- run: poetry run pytest
|
||||
- run: poetry run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
|
||||
|
||||
- store_test_results:
|
||||
path: reports
|
||||
|
||||
- store_artifacts:
|
||||
path: reports
|
||||
|
||||
- codecov/upload
|
||||
|
||||
deploy:
|
||||
docker:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.tool-versions
|
||||
.envrc
|
||||
reports/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
|
||||
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
<p align="center"><a href="https://github.com/specklesystems/specklepy/"><img src="https://circleci.com/gh/specklesystems/specklepy.svg?style=svg&circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
|
||||
<p align="center"><a href="https://github.com/specklesystems/specklepy/"><img src="https://circleci.com/gh/specklesystems/specklepy.svg?style=svg&circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a><a href="https://codecov.io/gh/specklesystems/specklepy">
|
||||
<img src="https://codecov.io/gh/specklesystems/specklepy/branch/main/graph/badge.svg?token=8KQFL5N0YF"/>
|
||||
</a> </p>
|
||||
|
||||
# About Speckle
|
||||
|
||||
|
||||
Generated
+531
-281
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -12,16 +12,17 @@ homepage = "https://speckle.systems/"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6.5"
|
||||
pydantic = "^1.7.3"
|
||||
pydantic = "^1.8.2"
|
||||
appdirs = "^1.4.4"
|
||||
gql = {version = ">=3.0.0a6", extras = ["all"], allow-prereleases = true}
|
||||
ujson = "^4.1.0"
|
||||
gql = {version = ">=3.0.0b1", extras = ["all"], allow-prereleases = true}
|
||||
ujson = "^4.3.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^20.8b1"
|
||||
isort = "^5.7.0"
|
||||
pytest = "^6.2.2"
|
||||
pytest-ordering = "^0.6"
|
||||
pytest-cov = "^3.0.0"
|
||||
|
||||
|
||||
[tool.black]
|
||||
|
||||
@@ -144,9 +144,9 @@ class StreamWrapper:
|
||||
parsed = urlparse(url)
|
||||
self.host = parsed.netloc
|
||||
self.use_ssl = parsed.scheme == "https"
|
||||
segments = parsed.path.strip("/").split("/")
|
||||
segments = parsed.path.strip("/").split("/", 3)
|
||||
|
||||
if not segments or len(segments) > 4 or len(segments) < 2:
|
||||
if not segments or len(segments) < 2:
|
||||
raise SpeckleException(
|
||||
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ class Commit(BaseModel):
|
||||
parents: Optional[List[str]]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Commit( id: {self.id}, message: {self.message}, referencedObject: {self.referencedObject}, authorName: {self.authorName}, createdAt: {self.createdAt} )"
|
||||
return f"Commit( id: {self.id}, message: {self.message}, referencedObject: {self.referencedObject}, authorName: {self.authorName}, branchName: {self.branchName}, createdAt: {self.createdAt} )"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
@@ -34,8 +34,8 @@ class Resource(ResourceBase):
|
||||
stream(id: $stream_id) {
|
||||
commit(id: $commit_id) {
|
||||
id
|
||||
referencedObject
|
||||
message
|
||||
referencedObject
|
||||
authorId
|
||||
authorName
|
||||
authorAvatar
|
||||
@@ -79,6 +79,7 @@ class Resource(ResourceBase):
|
||||
authorId
|
||||
authorName
|
||||
authorAvatar
|
||||
branchName
|
||||
createdAt
|
||||
sourceApplication
|
||||
totalChildrenCount
|
||||
|
||||
@@ -207,7 +207,7 @@ class Base(_RegisteringBase):
|
||||
try:
|
||||
attr.__set__(self, value)
|
||||
except AttributeError:
|
||||
pass # the prop probably doesn't have a setter
|
||||
return # the prop probably doesn't have a setter
|
||||
super().__setattr__(name, value)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -391,6 +391,15 @@ class Mesh(
|
||||
area: float = None
|
||||
volume: float = None
|
||||
|
||||
def transform_to(self, transform: "Transform") -> "Mesh":
|
||||
mesh = Mesh(vertices=transform.apply_to_points_values(self.vertices))
|
||||
for attr in set(self.get_serializable_attributes()) - {"vertices"}:
|
||||
orig_val = getattr(self, attr, None)
|
||||
if orig_val:
|
||||
setattr(mesh, attr, orig_val)
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
|
||||
degreeU: int = None
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
from typing import List
|
||||
from specklepy.objects.geometry import Point, Vector
|
||||
from .base import Base
|
||||
|
||||
OTHER = "Objects.Other."
|
||||
|
||||
IDENTITY_TRANSFORM = [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
]
|
||||
|
||||
|
||||
class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
|
||||
name: str = None
|
||||
@@ -10,3 +31,169 @@ class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
|
||||
roughness: float = 1
|
||||
diffuse: int = -2894893 # light gray arbg
|
||||
emissive: int = -16777216 # black arbg
|
||||
|
||||
|
||||
class Transform(
|
||||
Base,
|
||||
speckle_type=OTHER + "Transform",
|
||||
serialize_ignore={"translation", "scaling", "is_identity"},
|
||||
):
|
||||
"""The 4x4 transformation matrix
|
||||
|
||||
The 3x3 sub-matrix determines scaling.
|
||||
The 4th column defines translation, where the last value is a divisor (usually equal to 1).
|
||||
"""
|
||||
|
||||
_value: List[float] = None
|
||||
|
||||
@property
|
||||
def value(self) -> List[float]:
|
||||
"""The transform matrix represented as a flat list of 16 floats"""
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: List[float]) -> None:
|
||||
try:
|
||||
value = [float(x) for x in value]
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(
|
||||
f"Could not create a Transform object with the requested value. Input must be a 16 element list of numbers. Value provided: {value}"
|
||||
)
|
||||
if len(value) != 16:
|
||||
raise ValueError(
|
||||
f"Could not create a Transform object: input list should be 16 floats long, but was {len(value)} long"
|
||||
)
|
||||
|
||||
self._value = value
|
||||
|
||||
@property
|
||||
def translation(self) -> List[float]:
|
||||
"""The final column of the matrix which defines the translation"""
|
||||
return [self._value[i] for i in (3, 7, 11, 15)]
|
||||
|
||||
@property
|
||||
def scaling(self) -> List[float]:
|
||||
"""The 3x3 scaling sub-matrix"""
|
||||
return [self._value[i] for i in (0, 1, 2, 4, 5, 6, 8, 9, 10)]
|
||||
|
||||
@property
|
||||
def is_identity(self) -> bool:
|
||||
return self.value == IDENTITY_TRANSFORM
|
||||
|
||||
def apply_to_point(self, point: Point) -> Point:
|
||||
"""Transform a single speckle Point
|
||||
|
||||
Arguments:
|
||||
point {Point} -- the speckle Point to transform
|
||||
|
||||
Returns:
|
||||
Point -- a new transformed point
|
||||
"""
|
||||
coords = self.apply_to_point_value([point.x, point.y, point.z])
|
||||
return Point(x=coords[0], y=coords[1], z=coords[2], units=point.units)
|
||||
|
||||
def apply_to_point_value(self, point_value: List[float]) -> List[float]:
|
||||
"""Transform a list of three floats representing a point
|
||||
|
||||
Arguments:
|
||||
point_value {List[float]} -- a list of 3 floats
|
||||
|
||||
Returns:
|
||||
List[float] -- the list with the transform applied
|
||||
"""
|
||||
transformed = [
|
||||
point_value[0] * self._value[i]
|
||||
+ point_value[1] * self._value[i + 1]
|
||||
+ point_value[2] * self._value[i + 2]
|
||||
+ self._value[i + 3]
|
||||
for i in range(0, 15, 4)
|
||||
]
|
||||
|
||||
return [transformed[i] / transformed[3] for i in range(3)]
|
||||
|
||||
def apply_to_points(self, points: List[Point]) -> List[Point]:
|
||||
"""Transform a list of speckle Points
|
||||
|
||||
Arguments:
|
||||
points {List[Point]} -- the list of speckle Points to transform
|
||||
|
||||
Returns:
|
||||
List[Point] -- a new list of transformed points
|
||||
"""
|
||||
return [self.apply_to_point(point) for point in points]
|
||||
|
||||
def apply_to_points_values(self, points_value: List[float]) -> List[float]:
|
||||
"""Transform a list of speckle Points
|
||||
|
||||
Arguments:
|
||||
points {List[float]} -- a flat list of floats representing points to transform
|
||||
|
||||
Returns:
|
||||
List[float] -- a new transformed list
|
||||
"""
|
||||
if len(points_value) % 3 != 0:
|
||||
raise ValueError(
|
||||
"Cannot apply transform as the points list is malformed: expected length to be multiple of 3"
|
||||
)
|
||||
transformed = []
|
||||
for i in range(0, len(points_value), 3):
|
||||
transformed.extend(self.apply_to_point_value(points_value[i : i + 3]))
|
||||
|
||||
return transformed
|
||||
|
||||
def apply_to_vector(self, vector: Vector) -> Vector:
|
||||
"""Transform a single speckle Vector
|
||||
|
||||
Arguments:
|
||||
point {Vector} -- the speckle Vector to transform
|
||||
|
||||
Returns:
|
||||
Vector -- a new transformed point
|
||||
"""
|
||||
coords = self.apply_to_vector_value([vector.x, vector.y, vector.z])
|
||||
return Vector(x=coords[0], y=coords[1], z=coords[2], units=vector.units)
|
||||
|
||||
def apply_to_vector_value(self, vector_value: List[float]) -> List[float]:
|
||||
"""Transform a list of three floats representing a vector
|
||||
|
||||
Arguments:
|
||||
vector_value {List[float]} -- a list of 3 floats
|
||||
|
||||
Returns:
|
||||
List[float] -- the list with the transform applied
|
||||
"""
|
||||
return [
|
||||
vector_value[0] * self._value[i]
|
||||
+ vector_value[1] * self._value[i + 1]
|
||||
+ vector_value[2] * self._value[i + 2]
|
||||
for i in range(0, 15, 4)
|
||||
][:3]
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, value: List[float] = None) -> "Transform":
|
||||
"""Returns a Transform object from a list of 16 numbers. If no value is provided, an identity transform will be returned.
|
||||
|
||||
Arguments:
|
||||
value {List[float]} -- the matrix as a flat list of 16 numbers (defaults to the identity transform)
|
||||
|
||||
Returns:
|
||||
Transform -- a complete transform object
|
||||
"""
|
||||
if not value:
|
||||
value = IDENTITY_TRANSFORM
|
||||
return cls(value=value)
|
||||
|
||||
|
||||
class BlockDefinition(
|
||||
Base, speckle_type=OTHER + "BlockDefinition", detachable={"geometry"}
|
||||
):
|
||||
name: str = None
|
||||
basePoint: Point = None
|
||||
geometry: List[Base] = None
|
||||
|
||||
|
||||
class BlockInstance(
|
||||
Base, speckle_type=OTHER + "BlockInstance", detachable={"blockDefinition"}
|
||||
):
|
||||
blockDefinition: BlockDefinition = None
|
||||
transform: Transform = None
|
||||
@@ -58,7 +58,7 @@ class TestBranch:
|
||||
assert isinstance(branches, list)
|
||||
assert len(branches) == 2
|
||||
assert isinstance(branches[0], Branch)
|
||||
assert branches[0].name == branch.name
|
||||
assert branches[1].name == branch.name
|
||||
|
||||
def test_branch_update(self, client, stream, branch, updated_branch):
|
||||
updated = client.branch.update(
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
from typing import List
|
||||
import pytest
|
||||
from specklepy.api import operations
|
||||
from specklepy.objects.geometry import Mesh, Point, Vector
|
||||
from specklepy.objects.other import (
|
||||
Transform,
|
||||
BlockInstance,
|
||||
BlockDefinition,
|
||||
IDENTITY_TRANSFORM,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def point():
|
||||
return Point(x=1, y=10, z=2)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def points():
|
||||
return [Point(x=1 + i, y=10 + i, z=2 + i) for i in range(5)]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def point_value():
|
||||
return [1, 10, 2]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def points_values():
|
||||
coords = []
|
||||
for i in range(5):
|
||||
coords.extend([1 + i, 10 + i, 2 + 1])
|
||||
return coords
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def vector():
|
||||
return Vector(x=1, y=10, z=2)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def vector_value():
|
||||
return [1, 1, 2]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mesh():
|
||||
return Mesh(
|
||||
vertices=[-7, 5, 1, -8, 4, 0, -7, 3, 0, -6, 4, 0],
|
||||
faces=[1, 1, 2, 3, 0],
|
||||
units="feet",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def transform():
|
||||
"""Translates to [1, 2, 0] and scales z by 0.5"""
|
||||
return Transform.from_list(
|
||||
[
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
2.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.5,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_point_transform(point: Point, transform: Transform):
|
||||
new_point = transform.apply_to_point(point)
|
||||
|
||||
assert new_point.x == point.x + 1
|
||||
assert new_point.y == point.y + 2
|
||||
assert new_point.z == point.z * 0.5
|
||||
|
||||
|
||||
def test_points_transform(points: List[Point], transform: Transform):
|
||||
new_points = transform.apply_to_points(points)
|
||||
|
||||
for (i, new_point) in enumerate(new_points):
|
||||
assert new_point.x == points[i].x + 1
|
||||
assert new_point.y == points[i].y + 2
|
||||
assert new_point.z == points[i].z * 0.5
|
||||
|
||||
|
||||
def test_point_value_transform(point_value: List[float], transform: Transform):
|
||||
new_coords = transform.apply_to_point_value(point_value)
|
||||
|
||||
assert new_coords[0] == point_value[0] + 1
|
||||
assert new_coords[1] == point_value[1] + 2
|
||||
assert new_coords[2] == point_value[2] * 0.5
|
||||
|
||||
|
||||
def test_points_values_transform(points_values: List[float], transform: Transform):
|
||||
new_coords = transform.apply_to_points_values(points_values)
|
||||
|
||||
for i in range(0, len(points_values), 3):
|
||||
assert new_coords[i] == points_values[i] + 1
|
||||
assert new_coords[i + 1] == points_values[i + 1] + 2
|
||||
assert new_coords[i + 2] == points_values[i + 2] * 0.5
|
||||
|
||||
|
||||
def test_vector_transform(vector: Vector, transform: Transform):
|
||||
new_vector = transform.apply_to_vector(vector)
|
||||
|
||||
assert new_vector.x == vector.x
|
||||
assert new_vector.y == vector.y
|
||||
assert new_vector.z == vector.z * 0.5
|
||||
|
||||
|
||||
def test_vector_value_transform(vector_value: List[float], transform: Transform):
|
||||
new_coords = transform.apply_to_vector_value(vector_value)
|
||||
|
||||
assert new_coords[0] == vector_value[0]
|
||||
assert new_coords[1] == vector_value[1]
|
||||
assert new_coords[2] == vector_value[2] * 0.5
|
||||
|
||||
|
||||
def test_transform_fails_with_malformed_value():
|
||||
with pytest.raises(ValueError):
|
||||
Transform.from_list("asdf")
|
||||
with pytest.raises(ValueError):
|
||||
Transform.from_list([7, 8, 9])
|
||||
|
||||
|
||||
def test_transform_serialisation(transform: Transform):
|
||||
serialized = operations.serialize(transform)
|
||||
deserialized = operations.deserialize(serialized)
|
||||
|
||||
assert transform.get_id() == deserialized.get_id()
|
||||
|
||||
|
||||
def test_mesh_transform(mesh: Mesh, transform: Transform):
|
||||
new_mesh = mesh.transform_to(transform)
|
||||
|
||||
assert mesh.vertices != new_mesh.vertices
|
||||
|
||||
new_mesh.vertices = mesh.vertices
|
||||
|
||||
assert mesh.get_id() == new_mesh.get_id()
|
||||
@@ -18,6 +18,14 @@ class TestWrapper:
|
||||
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
|
||||
assert wrap.type == "branch"
|
||||
|
||||
def test_parse_nested_branch(self):
|
||||
wrap = StreamWrapper(
|
||||
"https://testing.speckle.dev/streams/4c3ce1459c/branches/izzy/dev"
|
||||
)
|
||||
|
||||
assert wrap.branch_name == "izzy/dev"
|
||||
assert wrap.type == "branch"
|
||||
|
||||
def test_parse_commit(self):
|
||||
wrap = StreamWrapper(
|
||||
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
|
||||
|
||||
Reference in New Issue
Block a user