diff --git a/README.md b/README.md index f3f21a3..f8dadd2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,102 @@ -# speckle-aectech-masterclass -web app for comparing Speckle stream commits +

+
+ Speckle | AEC Tech Masterclass +

+

+ A Speckle app to compare different commits and visualize the results +

+

Speckle is data infrastructure for the AEC industry.


+ +

Twitter Follow Community forum users website docs

+ +# About Speckle + +What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social) + +### Features + +- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry +- **Version control:** Speckle is the Git & Hub for geometry and BIM data +- **Collaboration:** share your designs collaborate with others +- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere +- **Interoperability:** get your CAD and BIM models into other software without exporting or importing +- **Real time:** get real time updates and notifications and changes +- **GraphQL API:** get what you need anywhere you want it +- **Webhooks:** the base for a automation and next-gen pipelines +- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack +- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more! + +### Try Speckle now! + +Give Speckle a try in no time by: + +- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at +- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click + +### Resources + +- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum! +- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle +- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality + +![Untitled](https://user-images.githubusercontent.com/2679513/132021739-15140299-624d-4410-98dc-b6ae6d9027ab.png) + +# Repo structure + +This repo is divided into two distinct parts, a **frontend** and a **server** + +## Frontend + +The `frontend/` folder contains all code related to the app any user will be able to access. + +The app was built using `Vue.js`, and communicates with Speckle through our `GraphQL API`. It also communicates with the app's `server` using `REST` calls. + +## Backend + +The `server/` folder contains all code related to the app's backend. + +It's a [FastAPI](https://link) server, written in Python and uses our Python SDK to communicate with our [public server](https://speckle.xyz) + +> `FastAPI` generates automatic documentation on the `/docs` route of the server. + +It is a very basic server composed of two routes: + +## `diff-check/STREAM_ID/CURRENT_COMMIT_ID/PREV_COMMIT_ID` + +Checks for an existing commit in a predefined `diff` branch and returns the _diff commit_ if it does exist. + +## `diff/STREAM_ID/CURRENT_COMMIT_ID/PREV_COMMIT_ID` + +Performs a diff operation against two commits from the same stream. + +Result of the `diff` operation will be commited to the stream on a predefined branch. + +Returns the diff commit if the operation was successfull. + +# Workshop pre-requisites + +You'll need an IDE to follow along with the code. We'll be using VSCode for the class, but any other will work too. You will also need the following extensions: + +- Vetur +- Python (comes preinstalled now, but just to be safe) + +For the **server**, you must have: + +- `python` installed in your computer (at least version `3.6`, `3.9` recommended) [Download it here](https://www.python.org/downloads/) +- Install `FastAPI`: + ```shell + pip install fastapi + # and then + pip install "uvicorn[standard]" + ``` +- Install `specklepy` + ```shell + pip install specklepy + ``` + +For the **Vue app**: + +- Install `node` [Download it here](https://nodejs.org/en/download/) +- Install `vue CLI` - [Instructions here](https://cli.vuejs.org/guide/installation.html) +- Install `vetur` VSCode extension +- Install `vue dev tools` for Chrome diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index a5f235c..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,78 +0,0 @@ -

-
- Speckle | Revit Dashboard -

-

- Speckle App to display Revit commits -

-

Speckle is data infrastructure for the AEC industry.


- -

Twitter Follow Community forum users website docs

-

.NET Core

- -# About Speckle - -What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social) - -### Features - -- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry -- **Version control:** Speckle is the Git & Hub for geometry and BIM data -- **Collaboration:** share your designs collaborate with others -- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere -- **Interoperability:** get your CAD and BIM models into other software without exporting or importing -- **Real time:** get real time updates and notifications and changes -- **GraphQL API:** get what you need anywhere you want it -- **Webhooks:** the base for a automation and next-gen pipelines -- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack -- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more! - -### Try Speckle now! - -Give Speckle a try in no time by: - -- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at -- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click - -### Resources - -- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum! -- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle -- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality - -![Untitled](https://user-images.githubusercontent.com/2679513/132021739-15140299-624d-4410-98dc-b6ae6d9027ab.png) - -# Repo structure - -> TBD!!! - -- `frontend/` -- `backend/` - -### Other repos - -Make sure to also check and ⭐️ these other Speckle repositories: - -- [`speckle-sharp`](https://github.com/specklesystems/speckle-sharp): .NET SDK, tooling, schema and Connectors -- [`speckle-server`](https://github.com/specklesystems/speckle-server): Server and Web packages -- [`specklepy`](https://github.com/specklesystems/specklepy): Python SDK 🐍 -- [`speckle-excel`](https://github.com/specklesystems/speckle-excel): Excel connector -- [`speckle-unity`](https://github.com/specklesystems/speckle-unity): Unity 3D connector -- [`speckle-blender`](https://github.com/specklesystems/speckle-blender): Blender connector -- [`speckle-unreal`](https://github.com/specklesystems/speckle-unreal): Unreal Engine Connector -- [`speckle-qgis`](https://github.com/specklesystems/speckle-qgis): QGIS connectod -- [`speckle-powerbi`](https://github.com/specklesystems/speckle-powerbi): PowerBi connector -- and more [connectos & tooling](https://github.com/specklesystems/)! - -## Developing and Debugging - -This app uses Vue.js 2. In order to run it locally - -### Security - -For any security vulnerabilities or concerns, please contact us directly at security[at]speckle.systems. - -### License - -Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems). diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..b2c0e02 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["./src/**/*"] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba90110..104c857 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "speckle-revit-dashboard-app", + "name": "speckle-aec-tech-masterclass", "version": "0.1.0", "lockfileVersion": 1, "requires": true, @@ -11382,17 +11382,18 @@ } }, "vue-eslint-parser": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.6.0.tgz", - "integrity": "sha512-QXxqH8ZevBrtiZMZK0LpwaMfevQi9UL7lY6Kcp+ogWHC88AuwUPwwCIzkOUc1LR4XsYAt/F9yHXAB/QoD17QXA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.11.0.tgz", + "integrity": "sha512-qh3VhDLeh773wjgNTl7ss0VejY9bMMa0GoDG2fQVyDzRFdiU3L7fw74tWZDHNQXdZqxO3EveQroa9ct39D2nqg==", "dev": true, "requires": { "debug": "^4.1.1", - "eslint-scope": "^5.0.0", + "eslint-scope": "^5.1.1", "eslint-visitor-keys": "^1.1.0", "espree": "^6.2.1", "esquery": "^1.4.0", - "lodash": "^4.17.15" + "lodash": "^4.17.21", + "semver": "^6.3.0" }, "dependencies": { "eslint-scope": { diff --git a/frontend/package.json b/frontend/package.json index 686b529..4ad5082 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "speckle-revit-dashboard-app", + "name": "speckle-aec-tech-masterclass", "version": "0.1.0", "private": true, "scripts": { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ccc2f6f..fe2c5d6 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -83,16 +83,6 @@ mdi-account - - Feedback - - mdi-message-alert-outline - - Log out diff --git a/frontend/src/components/commitSelector/CommitPanel.vue b/frontend/src/components/commitSelector/CommitPanel.vue index 7bc1a5d..e315a26 100644 --- a/frontend/src/components/commitSelector/CommitPanel.vue +++ b/frontend/src/components/commitSelector/CommitPanel.vue @@ -1,137 +1,133 @@ - + + diff --git a/frontend/src/components/viewer/Renderer.vue b/frontend/src/components/viewer/Renderer.vue index 537a74d..0d350eb 100644 --- a/frontend/src/components/viewer/Renderer.vue +++ b/frontend/src/components/viewer/Renderer.vue @@ -221,8 +221,8 @@ export default { type: Boolean, default: false }, - objectUrls: { - type: Array, + objectUrl: { + type: String, default: null }, unloadTrigger: { @@ -277,7 +277,9 @@ export default { this.namedViews.push(...views) } }, - objectUrls() { + objectUrl(newVal, oldVal) { + if (newVal == oldVal) return + console.log("obj url changed", newVal, oldVal) this.unloadData() this.load() } @@ -289,7 +291,6 @@ export default { if (!this.viewer) { this.viewer = new Viewer({ container: this.$refs.renderer }) } - this.viewer.onWindowResize() this.setupEvents() }, @@ -338,12 +339,11 @@ export default { }) }, load() { - if (!this.objectUrls || this.objectUrls.length === 0) return - this.viewer.onWindowResize() - this.objectUrls?.forEach(url => { - this.viewer.loadObject(url, localStorage.getItem(TOKEN)) - this.viewerLastLoadedUrl = url - }) + if (!this.objectUrl) return + + this.viewer.loadObject(this.objectUrl, localStorage.getItem(TOKEN)) + this.viewerLastLoadedUrl = this.objectUrl + this.setupEvents() this.hasLoadedModel = true }, diff --git a/frontend/src/graphql/streamSearch.graphql b/frontend/src/graphql/streamSearch.graphql deleted file mode 100644 index 0b8d563..0000000 --- a/frontend/src/graphql/streamSearch.graphql +++ /dev/null @@ -1,8 +0,0 @@ -query { - user { - avatar - id - name - email - } -} \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 1f7d8b4..69c4dd9 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -14,15 +14,15 @@ const routes = [ component: Home, meta: { requiresAuth: true, - title: "Speckle Revit Dashboard", + title: "Speckle AEC Tech Masterclass", metaTags: [ { name: "description", - content: "The speckle Revit Dashboard homepage" + content: "The Speckle AEC Tech Masterclass homepage" }, { property: "og:description", - content: "The speckle Revit Dashboard homepage" + content: "The Speckle AEC Tech Masterclass homepage" } ] } @@ -33,7 +33,7 @@ const routes = [ component: WelcomeView, meta: { requiresNoAuth: true, - title: "Login | Speckle Revit Dashboard" + title: "Login | Speckle AEC Tech Masterclass" } }, { diff --git a/frontend/src/speckleQueries.js b/frontend/src/speckleQueries.js index 85d658f..1e7b4b0 100644 --- a/frontend/src/speckleQueries.js +++ b/frontend/src/speckleQueries.js @@ -1,5 +1,3 @@ - - export const userInfoQuery = ` query { user { @@ -20,17 +18,20 @@ export const streamCommitsQuery = ` name updatedAt id - commits(limit: $limit, cursor: $cursor) { - totalCount - cursor - items{ - id - message - branchName - sourceApplication - referencedObject - authorName - createdAt + branch(name: "main"){ + commits(limit: $limit, cursor: $cursor) { + totalCount + cursor + items{ + id + message + branchName + sourceApplication + referencedObject + authorName + authorAvatar + createdAt + } } } } @@ -49,17 +50,6 @@ export const streamSearchQuery = ` } }` -export const streamObjectQuery = `query($streamId: String!, $objectId: String!) { - stream(id: $streamId){ - object(id: $objectId){ - totalChildrenCount - id - speckleType - data - } - } -}` - export const latestStreamsQuery = `query { streams(limit: 10){ cursor @@ -72,4 +62,4 @@ export const latestStreamsQuery = `query { updatedAt } } -}` \ No newline at end of file +}` diff --git a/frontend/src/speckleUtils.js b/frontend/src/speckleUtils.js index e2a528b..906ad98 100644 --- a/frontend/src/speckleUtils.js +++ b/frontend/src/speckleUtils.js @@ -88,8 +88,6 @@ export const searchStreams = (e) => speckleFetch(streamSearchQuery, {searchText: // Get commits related to a specific stream, allows for pagination by passing a cursor export const getStreamCommits = (streamId, itemsPerPage, cursor) => speckleFetch(streamCommitsQuery, {id: streamId, cursor, limit: itemsPerPage}) -export const getStreamObject = (streamId, objectId) => speckleFetch(streamObjectQuery, {streamId, objectId}).then(res => res.data?.stream?.object?.data) - export const getObject = (streamId, objectId) => speckleFetch(streamObjectQuery, {streamId, objectId}) export const getStreams = () => speckleFetch(latestStreamsQuery).then(res => res.data?.streams) diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index 7ca368c..e4176a0 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -1,16 +1,29 @@ @@ -51,4 +62,4 @@ export default { .v-data-footer__select { display: none !important; } - \ No newline at end of file + diff --git a/frontend/src/views/StreamView.vue b/frontend/src/views/StreamView.vue index 11727c0..dec511e 100644 --- a/frontend/src/views/StreamView.vue +++ b/frontend/src/views/StreamView.vue @@ -1,20 +1,33 @@ diff --git a/server/main.py b/server/main.py index e39e939..f8f9d19 100644 --- a/server/main.py +++ b/server/main.py @@ -1,7 +1,8 @@ -from typing import Optional -from fastapi import FastAPI, Request -from mesh_diff import compare_meshes +"""FastAPI Backend for the AEC Tech Masterclass""" + +from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware +from mesh_diff import SpeckleMeshDiff app = FastAPI() @@ -19,15 +20,46 @@ app.add_middleware( ) -@app.get("/") -def read_root(): - return {"Hello": "World"} - - @app.get("/diff/{stream_id}/{commit_current}/{commit_previous}") def get_diff(stream_id: str, commit_current: str, commit_previous: str, request: Request): - print(request) - print(request.headers.get("Authorisation")) - token = request.headers.get("Authorisation").split(" ")[1] - print(token) - return compare_meshes(stream_id, commit_current, commit_previous, token) + """Diffing endpoint""" + auth_header = request.headers.get("Authorisation") + + if auth_header is None: + raise HTTPException(405, "No token provided") + + token = auth_header.split(" ")[1] + + try: + mesh_differ = SpeckleMeshDiff(token, "https://latest.speckle.dev") + diff_commit = mesh_differ.process_diff( + stream_id, commit_current, commit_previous) + except Exception as e: + raise HTTPException(500, str(e)) + return {"commit": diff_commit} + + +@app.get("/diff_check/{stream_id}/{commit_current}/{commit_previous}") +def get_diff_check(stream_id: str, commit_current: str, commit_previous: str, request: Request): + """Diffing endpoint""" + auth_header = request.headers.get("Authorisation") + + if auth_header is None: + raise HTTPException(405, "No token provided") + + token = auth_header.split(" ")[1] + + try: + mesh_differ = SpeckleMeshDiff(token, "https://latest.speckle.dev") + mesh_differ.stream_id = stream_id + mesh_differ.commit_current = commit_current + mesh_differ.commit_prev = commit_previous + existing_diff_commit = mesh_differ.check_existing_commits() + if existing_diff_commit is not None: + + return {"exists": True, "commit": existing_diff_commit} + else: + return {"exists": False, "commit": None} + + except Exception as e: + raise HTTPException(500, str(e)) diff --git a/server/mesh_diff.py b/server/mesh_diff.py index eb8a369..6e7d9dd 100644 --- a/server/mesh_diff.py +++ b/server/mesh_diff.py @@ -1,290 +1,301 @@ -from array import array -from hashlib import new -from logging import NullHandler -from types import AsyncGeneratorType -from typing import Any, List -from math import floor +"""SpeckleMeshDiff for AEC Tech Masterclass""" +from typing import List +import math from specklepy.api import operations from specklepy.api.client import SpeckleClient -from specklepy.api.credentials import get_default_account from specklepy.api.models import Branch -from specklepy.api.resources import stream from specklepy.transports.server import ServerTransport -from specklepy.objects.geometry import GEOMETRY, Box, Brep, Point, Mesh, Line +from specklepy.objects.geometry import Brep, Point, Mesh from specklepy.objects import Base from specklepy.objects.other import RenderMaterial -import os - -URL = 'https://speckle.xyz/streams/' -HOST = "latest.speckle.dev" -STREAM_ID = "8325294b8f" -COMMIT_ID = "c207299871" # current commit -PREV_COMMIT_ID = "b9f376d75d" # previous commit -DIFF_BRANCH = "diff" COLORS = [-6426, -13108, -19790, -26215, - 32640, -39322, -45747, -52429, -59111, -65536] WHITE = -1 -def get_authenticated_client(token: str) -> SpeckleClient: - client = SpeckleClient(host=HOST) - client.authenticate(token=token) - return client +class SpeckleMeshDiff: + """Class to handle diffing between commits in a stream.""" + client: SpeckleClient = None + host: str = None + diff_branch: str = None -def receive_data( - client: SpeckleClient, stream_id: str, commit_id: str -) -> Any: - transport = ServerTransport(client, stream_id) + commit_prev: str = None + commit_current: str = None + stream_id: str = None - commit = client.commit.get(stream_id, commit_id) - res = operations.receive(commit.referencedObject, transport) + def __init__(self, token: str, host: str = "https://speckle.xyz", diff_branch: str = "diff"): + self.host = host + self.diff_branch = diff_branch + self.client = SpeckleClient(host=self.host) + self.client.authenticate(token=token) - # if grasshopper, will be nested under data: res["data"] - # if rhino/autocad/revit, will be sent with layers or categories + def process_diff(self, stream_id: str, commit_current: str, commit_previous: str): + """ + Process a diff operation between the specified + 'current' commit and the 'previous' one. + """ + # Set the global variables + self.stream_id = stream_id + self.commit_current = commit_current + self.commit_prev = commit_previous - return res + print("Did not find existing diff, fetching commits now....") + # get meshes from commits + previous_commit = self.receive_data( + self.client, self.stream_id, self.commit_prev) + previous_meshes = self.get_all_meshes(previous_commit) -def get_all_meshes(child: Base): - meshes = [] + current_commit = self.receive_data( + self.client, self.stream_id, self.commit_current) + current_meshes = self.get_all_meshes(current_commit) - names = child.get_dynamic_member_names() - for name in names: - prop = child[name] - if isinstance(prop, Base): - if isinstance(prop, Brep): - if not hasattr(prop, "displayMesh"): + print("Comparing meshes...") + diff_base = self.compare_meshes(current_meshes, previous_meshes) + + print("Diffing was successfull, sending to Speckle") + diff_commit_id = self.send_data( + self.client, + self.stream_id, + self.diff_branch, + diff_base, + self.commit_current + "-" + self.commit_prev) + + print("Successfully sent data to Speckle") + return self.client.commit.get(self.stream_id, diff_commit_id) + + def check_existing_commits(self) -> bool or None: + """Checks if a specific diff commit already exists in the diff_branch""" + branch_commits: Branch = self.client.branch.get( + self.stream_id, self.diff_branch, 50) + + for commit in branch_commits.commits.items: + if commit.message == f"{self.commit_current}-{self.commit_prev}": + return commit + + return None + + def compare_meshes(self, current_meshes: List[Mesh], previous_meshes: List[Mesh]) -> Base: + """ + Compares the meshes from the first commit against the second, and sends the result to the `diff` branch. + It returns the commit url of the diff. + """ + + # pre process meshes in the current commit to check for same object ID (this means obj hasn't changed) - skip these + # if object id has changed, check for application id - if these are the same, compare these objects directly + matched_current_indices = [] + matched_previous_indices = [] + paired_current_indices = [] + paired_previous_indices = [] + for i in range(0, len(current_meshes), 1): + for j in range(0, len(previous_meshes), 1): + if current_meshes[i][1] == previous_meshes[j][1]: + matched_current_indices.append(i) + matched_previous_indices.append(j) + break + elif current_meshes[i][2] == previous_meshes[j][2]: + paired_current_indices.append(i) + paired_previous_indices.append(j) break - meshes.append((prop.displayMesh, prop.id, prop.applicationId, prop)) - elif isinstance(prop, Mesh): - meshes.append((prop, prop.id, prop.applicationId)) - elif isinstance(prop, list): - for p in prop: - if isinstance(p, Brep): - if not hasattr(p, "displayMesh"): - break - meshes.append((p.displayMesh, p.id, p.applicationId, p)) - elif isinstance(p, Mesh): - meshes.append((p, p.id, p.applicationId)) - return meshes - -def get_all_points(meshes: List[Mesh]): - points = [] - for mesh in meshes: - for i in range(2, len(mesh.vertices), 3): - point = Point() - point.x = mesh.vertices[i-2] - point.y = mesh.vertices[i-1] - point.z = mesh.vertices[i] - points.append(point) - return points - - -def find_point(current: Point, points: List[Point]): - for point in points: - if (point.x == current.x and point.y == current.y and point.z == current.z): - return True - return False - - -def find_closest_point(current: Point, points: List[Point]): - smallest_distance = None - for point in points: - d = ((current.x - point.x)**2 + (current.y - point.y) - ** 2 + (current.z - point.z)**2)**0.5 - if smallest_distance is not None: - if d > smallest_distance: + # remove matched previous meshes and matched pairs and get list of all mesh points from processed list + # this will be used as reference for all meshes that have changed and don't have a specific match to compare to + previous_meshes_ref_pool = [] + for i in range(0, len(previous_meshes), 1): + if matched_previous_indices.__contains__(i) or paired_previous_indices.__contains__(i): continue - smallest_distance = d - return smallest_distance + previous_meshes_ref_pool.append(previous_meshes[i][0]) + ref_pool = self.get_all_points(previous_meshes_ref_pool) -def check_existing_commits( - client: SpeckleClient, stream_id: str, commit_current: str, commit_previous: str -) -> Any: - transport = ServerTransport(client, stream_id) + # create a ghosted render material + ghosted = RenderMaterial() + ghosted.diffuse = WHITE + ghosted.opacity = 0.1 - branch_commits: Branch = client.branch.get(stream_id, DIFF_BRANCH, 50) - - for commit in branch_commits.commits.items: - if commit.message == f"{commit_current}-{commit_previous}": - return commit.id + # for each mesh in the current commit, compare mesh vertices with ref pool or matched pair to determine scale of change + diff_meshes = [] + same_meshes = [] + ref_meshes = [] + diff_mesh_pairs = [] + diff_mesh_ref_indices = [] # the corresponding ref pair mesh to diff mesh pairs + for i in range(0, len(current_meshes), 1): + mesh = current_meshes[i][0] - return None - - -def compare_meshes(stream_id: str, commit_current: str, commit_previous: str): - client = get_authenticated_client() - - # see if existing diff commit already exists - # query for latest x commits in diff branch - # read commit message & parse - # return url if found - existing_commit = check_existing_commits(client, stream_id, commit_current, commit_previous) - if existing_commit is not None: - url = URL + stream_id + '/commits/' + existing_commit - return url - - # get meshes from commits - previous_commit = receive_data(client, stream_id, commit_previous) - previous_meshes = get_all_meshes(previous_commit) - current_commit = receive_data(client, stream_id, commit_current) - current_meshes = get_all_meshes(current_commit) - - # pre process meshes in the current commit to check for same object ID (this means obj hasn't changed) - skip these - # if object id has changed, check for application id - if these are the same, compare these objects directly - matched_current_indices = [] - matched_previous_indices = [] - paired_current_indices = [] - paired_previous_indices =[] - for i in range(0, len(current_meshes), 1): - for j in range(0, len(previous_meshes), 1): - if current_meshes[i][1] == previous_meshes[j][1]: - matched_current_indices.append(i) - matched_previous_indices.append(j) - break - elif current_meshes[i][2] == previous_meshes[j][2]: - paired_current_indices.append(i) - paired_previous_indices.append(j) - break - - # remove matched previous meshes and matched pairs and get list of all mesh points from processed list - # this will be used as reference for all meshes that have changed and don't have a specific match to compare to - previous_meshes_ref_pool = [] - for i in range(0, len(previous_meshes), 1): - if matched_previous_indices.__contains__(i) or paired_previous_indices.__contains__(i): - continue - previous_meshes_ref_pool.append(previous_meshes[i][0]) - ref_pool = get_all_points(previous_meshes_ref_pool) - - # create a ghosted render material - ghosted = RenderMaterial() - ghosted.diffuse = WHITE - ghosted.opacity = 0.1 - - # for each mesh in the current commit, compare mesh vertices with ref pool or matched pair to determine scale of change - diff_meshes = [] - same_meshes = [] - ref_meshes = [] - diff_mesh_pairs = [] - diff_mesh_ref_indices = [] # the corresponding ref pair mesh to diff mesh pairs - for i in range(0, len(current_meshes), 1): - mesh = current_meshes[i][0] - - # send matched current meshes with rendermaterial semi-transparent (ghosted) - if matched_current_indices.__contains__(i): - mesh.renderMaterial = ghosted - same_meshes.append(mesh) - continue - - diff_mesh = mesh - vertices = get_all_points([mesh]) - diff_mesh_colors = [WHITE] * (len(vertices)) - diff_values = [] - - # check for pairing - paired_mesh_points = [] - paired_ref_mesh_index = None - is_paired = False - if paired_current_indices.__contains__(i): - paired_ref_mesh_index = paired_previous_indices[paired_current_indices.index(i)] - paired_mesh_points = get_all_points([previous_meshes[paired_ref_mesh_index][0]]) - is_paired = True - - for vertex in vertices: - if is_paired: - diff_values.append(find_closest_point(vertex, paired_mesh_points)) - else: - diff_values.append(find_closest_point(vertex, ref_pool)) - - # determine color value for vertex by remapping domain - changed = False - bin_size = max(diff_values) / len(COLORS) - for i in range(0, len(vertices), 1): - if diff_values[i] == 0: + # send matched current meshes with rendermaterial semi-transparent (ghosted) + if matched_current_indices.__contains__(i): + mesh.renderMaterial = ghosted + same_meshes.append(mesh) continue - else: - index = floor(diff_values[i] / bin_size) - if index == len(COLORS): - index -= 1 - diff_mesh_colors[i] = COLORS[index] - changed = True - - - if not changed: # if hasn't changed, append to same list + + diff_mesh = mesh + vertices = self.get_all_points([mesh]) + diff_mesh_colors = [WHITE] * (len(vertices)) + diff_values = [] + + # check for pairing + paired_mesh_points = [] + paired_ref_mesh_index = None + is_paired = False + if paired_current_indices.__contains__(i): + paired_ref_mesh_index = paired_previous_indices[paired_current_indices.index( + i)] + paired_mesh_points = self.get_all_points( + [previous_meshes[paired_ref_mesh_index][0]]) + is_paired = True + + for vertex in vertices: + if is_paired: + diff_values.append(self.find_closest_point( + vertex, paired_mesh_points)) + else: + diff_values.append( + self.find_closest_point(vertex, ref_pool)) + + # determine color value for vertex by remapping domain + changed = False + bin_size = max(diff_values) / len(COLORS) + for i in range(0, len(vertices), 1): + if diff_values[i] == 0: + continue + else: + index = math.floor(diff_values[i] / bin_size) + if index == len(COLORS): + index -= 1 + diff_mesh_colors[i] = COLORS[index] + changed = True + + if not changed: # if hasn't changed, append to same list + mesh.renderMaterial = ghosted + if is_paired: + matched_previous_indices.append(paired_ref_mesh_index) + same_meshes.append(mesh) + + else: # set colors and add mesh to diff list or paired diff list + diff_mesh.colors = diff_mesh_colors + if is_paired: + diff_mesh_pairs.append(diff_mesh) + diff_mesh_ref_indices.append(paired_ref_mesh_index) + else: + diff_meshes.append(diff_mesh) + + # process reference meshes + diff_mesh_refs = [] + for j in range(0, len(previous_meshes)): + # skip matched reference meshes and paired refs + if matched_previous_indices.__contains__(j) or diff_mesh_ref_indices.__contains__(j): + continue + mesh = previous_meshes[j][0] mesh.renderMaterial = ghosted - if is_paired: - matched_previous_indices.append(paired_ref_mesh_index) - same_meshes.append(mesh) - - else: # set colors and add mesh to diff list or paired diff list - diff_mesh.colors = diff_mesh_colors - if is_paired: - diff_mesh_pairs.append(diff_mesh) - diff_mesh_ref_indices.append(paired_ref_mesh_index) - else: - diff_meshes.append(diff_mesh) - - # process reference meshes - diff_mesh_refs = [] - for j in range(0, len(previous_meshes)): - if matched_previous_indices.__contains__(j) or diff_mesh_ref_indices.__contains__(j): # skip matched reference meshes and paired refs - continue - mesh = previous_meshes[j][0] - mesh.renderMaterial = ghosted - ref_meshes.append(mesh) - for diff_mesh_ref_index in diff_mesh_ref_indices: - mesh = previous_meshes[diff_mesh_ref_index] - if len(mesh) > 3: - diff_mesh_refs.append(mesh[3]) - else: + ref_meshes.append(mesh) + for diff_mesh_ref_index in diff_mesh_ref_indices: + mesh = previous_meshes[diff_mesh_ref_index] diff_mesh_refs.append(mesh[0]) - - # get units from first mesh in current commit - units = current_meshes[0][0].units - # create a new commit with the diff meshes - return send_diff_data(stream_id, commit_current, commit_previous, units, diff_meshes, diff_mesh_pairs, diff_mesh_refs, same_meshes, ref_meshes) + # Construct diff base object to return + base = Base() + base.units = current_meshes[0][0].units + base["changed"] = diff_meshes + for i in range(0, len(diff_mesh_pairs), 1): + layer = f"changed::{i}" + base[f"{layer}::changed"] = [diff_mesh_pairs[i]] + base[f"{layer}::ref"] = [diff_mesh_refs[i]] + base["same"] = same_meshes + base["ref"] = ref_meshes + return base + @staticmethod + def send_data(client: SpeckleClient, stream_id: str, branch: str, diff_object: Base, message: str) -> str: + """Sends a Base object to a specified branch""" + # create a branch if necessary + branches = client.branch.list(stream_id) + has_res_branch = any(b.name == branch for b in branches) -def send_diff_data(stream_id: str, commit_current: str, commit_previous: str, units: str, changed: List[Mesh], changed_pairs: List[Mesh], ref_pairs: List[Base], unchanged: List[Mesh], ref: List[Mesh]): - client = get_authenticated_client() + if not has_res_branch: + client.branch.create( + stream_id, name=branch, description="This branch was created by the AEC Tech Masterclass App" + ) - # create a branch if necessary - branches = client.branch.list(stream_id) - has_res_branch = any(b.name == DIFF_BRANCH for b in branches) - if not has_res_branch: - client.branch.create( - stream_id, name=DIFF_BRANCH, description="all your stream diff results" + transport = ServerTransport( + client=client, stream_id=stream_id) + + object_id = operations.send(base=diff_object, transports=[transport]) + + commit_id = client.commit.create( + stream_id, + object_id, # object id + branch, + message ) - # create a commit with message "current_commit_id - previous_commit_id" - base = Base() - base.units = units - base["changed"] = changed + return commit_id - for i in range(0, len(changed_pairs), 1): - layer = f"changed::{i}" - base[f"{layer}::changed"] = [changed_pairs[i]] - base[f"{layer}::ref"] = [ref_pairs[i]] - base["same"] = unchanged - base["ref"] = ref + @staticmethod + def receive_data(client: SpeckleClient, stream_id: str, commit_id: str) -> Base: + """Get the data from a commit on the Speckle server""" + transport = ServerTransport(client, stream_id) - transport = ServerTransport(client=client, stream_id=stream_id) + commit = client.commit.get(stream_id, commit_id) + res = operations.receive(commit.referencedObject, transport) - hash = operations.send(base=base, transports=[transport]) + # if grasshopper, will be nested under data: res["data"] + # if rhino/autocad/revit, will be sent with layers or categories - commit_id = client.commit.create( - stream_id, - hash, # object id - DIFF_BRANCH, - message=commit_current + "-" + commit_previous - ) + return res - return URL + stream_id + '/commits/' + commit_id + @staticmethod + def get_all_meshes(child: Base) -> List[Mesh]: + """Returns all the meshes from a given Base object.""" + meshes = [] -# uncomment for debug -# compare_meshes(STREAM_ID, COMMIT_ID, PREV_COMMIT_ID) + names = child.get_dynamic_member_names() + for name in names: + prop = child[name] + if isinstance(prop, Base): + if isinstance(prop, Brep): + if not hasattr(prop, "displayMesh"): + break + meshes.append((prop.displayMesh, prop.id, + prop.applicationId, prop)) + elif isinstance(prop, Mesh): + meshes.append((prop, prop.id, prop.applicationId)) + elif isinstance(prop, list): + for p in prop: + if isinstance(p, Brep): + if not hasattr(p, "displayMesh"): + break + meshes.append( + (p.displayMesh, p.id, p.applicationId, p)) + elif isinstance(p, Mesh): + meshes.append((p, p.id, p.applicationId)) + elif hasattr(p, "displayMesh") or hasattr(p, "@displayMesh"): + meshes.append((p.displayMesh, p.id, p.applicationId)) + return meshes + + @staticmethod + def get_all_points(meshes: List[Mesh]) -> List[Point]: + """Returns a flat list of vertices of all the meshes in a list""" + points = [] + for mesh in meshes: + for i in range(2, len(mesh.vertices), 3): + point = Point() + point.x = mesh.vertices[i-2] + point.y = mesh.vertices[i-1] + point.z = mesh.vertices[i] + points.append(point) + return points + + @staticmethod + def find_closest_point(current: Point, points: List[Point]): + """Find the closest point to a target given a list of points""" + smallest_distance = None + for point in points: + d = ((current.x - point.x)**2 + (current.y - point.y) + ** 2 + (current.z - point.z)**2)**0.5 + if smallest_distance is not None: + if d > smallest_distance: + continue + smallest_distance = d + return smallest_distance diff --git a/server/test_mesh_diff.py b/server/test_mesh_diff.py new file mode 100644 index 0000000..e7ac4cb --- /dev/null +++ b/server/test_mesh_diff.py @@ -0,0 +1,10 @@ +from mesh_diff import SpeckleMeshDiff +from specklepy.api.credentials import get_default_account + +STREAM_ID = "8325294b8f" +COMMIT_ID = "c207299871" # current commit +PREV_COMMIT_ID = "b9f376d75d" # previous commit + +account = get_default_account() +md = SpeckleMeshDiff(account.token) +md.process_diff(STREAM_ID, COMMIT_ID, PREV_COMMIT_ID) diff --git a/vetur.config.js b/vetur.config.js new file mode 100644 index 0000000..28aa672 --- /dev/null +++ b/vetur.config.js @@ -0,0 +1,34 @@ +// vetur.config.js +/** @type {import('vls').VeturConfig} */ +module.exports = { + // **optional** default: `{}` + // override vscode settings + // Notice: It only affects the settings used by Vetur. + settings: { + "vetur.useWorkspaceDependencies": true, + "vetur.experimental.templateInterpolationService": true + }, + // **optional** default: `[{ root: './' }]` + // support monorepos + projects: [ + "./packages/frontend", // Shorthand for specifying only the project root location + { + // **required** + // Where is your project? + // It is relative to `vetur.config.js`. + root: "./packages/frontend", + // **optional** default: `'package.json'` + // Where is `package.json` in the project? + // We use it to determine the version of vue. + // It is relative to root property. + package: "./package.json", + + // **optional** default: `[]` + // Register globally Vue component glob. + // If you set it, you can get completion by that components. + // It is relative to root property. + // Notice: It won't actually do it. You need to use `require.context` or `Vue.component` + globalComponents: ["./src/components/**/*.vue"] + } + ] +}