From 9fc6bb1923882d9b07fe99fb191ae553d997997b Mon Sep 17 00:00:00 2001 From: Alan Rynne Date: Thu, 8 Jul 2021 17:35:42 +0200 Subject: [PATCH] Polish --- CSharpStarter/Program.cs | 120 +++++++++++++++++++++++----- CSharpStarter/Readme.md | 12 +-- PythonStarter/Readme.md | 14 ++++ PythonStarter/test.py | 45 ++++++----- README.md | 4 +- WebStarter/Readme.md | 27 +++++++ WebStarter/app.js | 147 +++++++++++++++++++++++++++++++++-- WebStarter/index.html | 71 ++++++++++++++++- WebStarter/speckleQueries.js | 38 +++++++++ WebStarter/speckleUtils.js | 39 ++++++++++ WebStarter/styles.css | 44 +++++++++-- 11 files changed, 491 insertions(+), 70 deletions(-) create mode 100644 WebStarter/speckleQueries.js create mode 100644 WebStarter/speckleUtils.js diff --git a/CSharpStarter/Program.cs b/CSharpStarter/Program.cs index 05c4895..1c401d1 100644 --- a/CSharpStarter/Program.cs +++ b/CSharpStarter/Program.cs @@ -2,45 +2,127 @@ using Speckle.Core.Kits; using Speckle.Core.Credentials; using Speckle.Core.Api; +using System.Threading; using Objects; using Speckle.Core.Models; using System.Collections.Generic; using System.Threading.Tasks; using Objects.Geometry; using Objects.Primitive; +using Speckle.Core.Transports; +using System.Linq; namespace CSharpStarter { class Program { + // Running this program will pull the latest commit from the main branch + // of the specified stream and duplicate it inside a different branch. + // (branch should exist already or the program will fail) static void Main(string[] args) { - Console.WriteLine("Hello Speckle!"); + // The id of the stream to work with (we're assuming it already exists in your default account's server) + var streamId = "c1800d795b"; + // The name of the branch we'll send data to. + var branchName = "processed"; // Get default account on this machine var defaultAccount = AccountManager.GetDefaultAccount(); + // Or get all the accounts and manually choose the one you want + // var accounts = AccountManager.GetAccounts(); + // var defaultAccount = accounts.ToList().FirstOrDefault(); - // Create a new "base" object - var commitObj = new Base(); - // Base objects are dynamic, so you can assign any properties just like a Dictionary - commitObj["myProp"] = "myPropValue"; - commitObj["myOtherProp"] = new List { "A", "list", "of", "values" }; + // Authenticate using the account + var client = new Client(defaultAccount); - var boxes = new List(); - for (int i = 0; i < 10; i++) + // Get the main branch with it's latest commit reference + var branch = client.BranchGet(streamId, "main", 1).Result; + // Get the id of the object referenced in the commit + var hash = branch.commits.items[0].referencedObject; + + + // Create the server transport for the specified stream. + var transport = new ServerTransport(defaultAccount, streamId); + // Receive the object + var receivedBase = Operations.Receive(hash, transport).Result; + + // Process the object however you'd like + Console.WriteLine("Received object:" + receivedBase); + + // Sending the object will return it's unique identifier. + var newHash = Operations.Send(receivedBase, new List { transport }).Result; + + // Create a commit in `processed` branch (it must previously exist) + var commitId = client.CommitCreate(new CommitCreateInput() { - for (int j = 0; j < 10; j++) - { - boxes.Add(new Point(i, j, 0)); - } - } - commitObj["boxes"] = boxes; - // Send the object to Speckle, get back the commit id - var commitId = Helpers.Send("2d9b814ed6", commitObj, "Upload from my console app", null, 0, defaultAccount, false).Result; + branchName = branchName, + message = "Automatic commit created by AEC Tech Demo C# console app.", + objectId = newHash, + streamId = streamId, + sourceApplication = "AEC Tech C# Script" - // To receive the latest commit - var receivedObj = Helpers.Receive("2d9b814ed6", defaultAccount).Result; - Console.WriteLine(receivedObj["myProp"]); + }).Result; + + Console.WriteLine($"Successfully created commit with id: {commitId}"); + + // Remember to dispose of the client once you've finished with it. + client.Dispose(); + } + + static void ReactToCommit(string[] args) + + { + // Get default account on this machine + var defaultAccount = AccountManager.GetDefaultAccount(); + var client = new Client(defaultAccount); + var streamId = "42c06de34f"; + var branch = client.BranchGet(streamId, "main", 1).Result; + var hash = branch.commits.items[0].referencedObject; + + + var exit = false; + client.OnCommitCreated += (sender, e) => + { + // Ignore commits from any branch other than 'main' + if (e.branchName != "main") return; + + Console.WriteLine("Commit was created in Main! Processing data..."); + + // Create the server transport for the specified stream. + var transport = new ServerTransport(defaultAccount, streamId); + // Receive the object + var receivedBase = Operations.Receive(hash, transport).Result; + + var newHash = Operations.Send(receivedBase, new List { transport }).Result; + + // Create a commit in `processed` branch (it must previously exist) + var commitId = client.CommitCreate(new CommitCreateInput() + { + branchName = "processed", + message = "Automatic commit created by AEC Tech Demo C# console app.", + objectId = newHash, + streamId = streamId, + sourceApplication = "C#" + + }).Result; + Console.WriteLine($"Successfully created commit with id: {commitId}"); + exit = true; + }; + + // Subscribe to commits created on the stream. + client.SubscribeCommitCreated(streamId); + + // HACK: This is a super hacky way to get a C# console app to wait for an event to happen. + // YOU SHOULD NOT DO THIS IN A PRODUCTION APPLICATION 🤣 + Console.WriteLine("Waiting for commit created event..."); + while (!exit) + { + Thread.Sleep(500); + Console.WriteLine("Still waiting..."); + } + + // Remember to dispose of the client once you've finished with it. + client.Dispose(); } } } diff --git a/CSharpStarter/Readme.md b/CSharpStarter/Readme.md index e034242..0729e4d 100644 --- a/CSharpStarter/Readme.md +++ b/CSharpStarter/Readme.md @@ -1,12 +1,4 @@ -# Using Speckle Core +# AEC C# Starter -``` -dotnet new console -f netcoreapp3.1 -``` - -Add Speckle.Core - -``` -dotnet add package Speckle.Core --version 2.1.22 -``` +> \ No newline at end of file diff --git a/PythonStarter/Readme.md b/PythonStarter/Readme.md index e69de29..6d1a09f 100644 --- a/PythonStarter/Readme.md +++ b/PythonStarter/Readme.md @@ -0,0 +1,14 @@ +# AEC Tech Python Starter + +# Usage + +This is a demo script + +# Prerequisites + +Install `specklepy` + +``` +pip install specklepy +``` + diff --git a/PythonStarter/test.py b/PythonStarter/test.py index 8cc71d0..035103f 100644 --- a/PythonStarter/test.py +++ b/PythonStarter/test.py @@ -1,51 +1,56 @@ +# Running this script will pull the latest commit from the main branch +# of the specified stream and duplicate it inside a different branch. +# (branch should exist already or the script will fail + from specklepy.api.client import SpeckleClient from specklepy.api.credentials import get_default_account, get_local_accounts from specklepy.transports.server import ServerTransport from specklepy.api import operations +# The id of the stream to work with (we're assuming it already exists in your default account's server) +streamId = "c1800d795b" +branchName = "processed" # Initialise the Speckle client pointing to your specific server. client = SpeckleClient(host="https://speckle.xyz") # Get the default account -# If you have more than one account, or the account is not the default, use get_local_accounts account = get_default_account() +# If you have more than one account, or the account is not the default, use get_local_accounts +# accounts = get_local_accounts() +# account = accounts[0] # Authenticate using the account token client.authenticate(token=account.token) - -# Your account email -print('Welcome!!',account.userInfo.email, account.serverInfo.url) - - -# use that stream id to get the stream from the server -streamId = "42c06de34f" +# Get the main branch with it's latest commit reference branch = client.branch.get(streamId, "main", 1) +# Get the id of the object referenced in the commit hash = branch.commits.items[0].referencedObject - - -# next create a server transport - this is the vehicle through which you will send and receive +# Create the server transport for the specified stream. transport = ServerTransport(client=client, stream_id=streamId) -# this receives the object back from the transport. -# the received data will be deserialised back into a `Block` +# Receive the object received_base = operations.receive(obj_id=hash, remote_transport=transport) -for list in received_base["@data"]: - for item in list: - item.z += 1 + + + +# The received object, process it as you wish. +print("Received object:", received_base) + # this serialises the modified points and sends it to the transport -hash = operations.send(base=received_base, transports=[transport]) +newHash = operations.send(base=received_base, transports=[transport]) # you can now create a commit on your stream with this object commit_id = client.commit.create( stream_id=streamId, - branch_name="python", + branch_name=branchName, object_id=hash, - message="Points were modified by AECTechDemo.py script", + message="Automatic commit created by AEC Tech Demo by AECTechDemo.py script", + source_application="AEC Tech Python Script" ) -print("Successfully created commit with id:", commit_id) \ No newline at end of file +print("Successfully created commit with id: ", commit_id) diff --git a/README.md b/README.md index 7c0d804..7b82fc8 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# AEC Tech Presentation Repo +# AEC Tech Speckle Repo + +This repo contains all code and files related to diff --git a/WebStarter/Readme.md b/WebStarter/Readme.md index e69de29..03f4351 100644 --- a/WebStarter/Readme.md +++ b/WebStarter/Readme.md @@ -0,0 +1,27 @@ +# Starter Speckle web viewer project + +> This is a basic `html/js/css` webpage that uses our `@speckle/viewer` package and some basic queries through our `graphQL` API. +> If you want to know more about Speckle as a developer platform, check our [Developer Docs](https://speckle.guide/dev) + +The app allows you to view the main geometry of a stream and combine it with different design options (which would exist as different `branches` in the stream) + +![alt](https://link) + +The stream will be organised as follows: + +- `main` branch: Will contain the main geometry. +- Any other existing branches should contain the different design options to visualise along side the `main` geometry. + +![Stream branches screenshot](https://link) + +## Usage + +This demo should run directly in your browser. Just double click the `index.html` file on this folder. + +## Repo structure + +- `index.html`: Main html file +- `app.js`: Main `js` module for the website +- `speckleQueries.js` contains several `graphQL` queries we'll be using. +- `speckleUtils.js` contains the functions to interact with the Speckle API. +- `styles.css`: Styles to used by `index.html` diff --git a/WebStarter/app.js b/WebStarter/app.js index 5acb9d2..031d2aa 100644 --- a/WebStarter/app.js +++ b/WebStarter/app.js @@ -1,22 +1,153 @@ -let token = "096315ee1bf8c961444b270746becd1e8474baa79e" -let objUrl = - "http://localhost:3000/streams/2d9b814ed6/objects/d679e6ff0efb8c9c102b8f8136b0f68a" +import { getStreamCommits, getUserData } from "./speckleUtils.js" + +let token = null +let streamId = null +let stream = null +let mainBranch = null +let beams = null +let columns = null +let selectedBeam = null +let selectedColumn = null +let isDropdownLoaded = false + +document + .getElementById("alternative-selector") + .addEventListener("change", onBeamAlternativeSelected) +document + .getElementById("alternative-selector-2") + .addEventListener("change", onColumnAlternativeSelected) +document.getElementById("reloadButton").addEventListener("click", reload) + +let objUrl = (streamId, objId) => + `https://speckle.xyz/streams/${streamId}/objects/${objId}` + // Create the viewer instance let viewer = new window.Speckle.Viewer({ container: document.getElementById("viewer"), - showStats: true + showStats: false }) -viewer.loadObject(objUrl, token) - -viewer.interactions.zoomExtents(0.95, false) - +// Handle selection events viewer.on("select", objects => { console.info(`Selection event. Current selection count: ${objects.length}.`) console.log(objects) + let el = document.getElementById("selection-area") + let jv = window["w-jsonview-tree"] + jv(objects, el, { expanded: false }) }) +// Handle double click events viewer.on("object-doubleclicked", obj => { console.info("Object double click event.") console.log(obj ? obj : "nothing was doubleckicked.") }) + +async function reload() { + document.getElementById("reloadButton").classList.add("is-loading") + streamId = document.getElementById("stream-input").value // Get the value of the stream input + token = document.getElementById("token-input").value // Get the value of the token input + await fetchStreamData() // Fetch the stream data + if (!isDropdownLoaded) { + populateDropdown("alternative-selector", beams) + populateDropdown("alternative-selector-2", columns) // If it's the first time, populate the dropdown too. + } + await reloadViewer() // Reload the viewer once the stream data has been fetched + document.getElementById("reloadButton").classList.remove("is-loading") +} + +// This will add an option for each branch in the alternatives list, and bind the referenced object id as the value +function populateDropdown(id, data) { + let select = document.getElementById(id) + data.forEach(alt => { + if (alt.commits.items.length == 0) return + var opt = document.createElement("option") + opt.appendChild(document.createTextNode(alt.name)) + opt.value = alt.commits.items[0].referencedObject + select.appendChild(opt) + }) + isDropdownLoaded = true +} + +// Get the stream data and process it +// +async function fetchStreamData() { + await getStreamCommits(streamId, 1, null, token).then(str => { + stream = str.data.stream + + // Split the branches into "main" and everything else + mainBranch = stream.branches.items.find(b => b.name == "main") + beams = stream.branches.items.filter(b => b.name.startsWith("beam")) + columns = stream.branches.items.filter(b => b.name.startsWith("column")) + console.log("main branch", mainBranch) + console.log("beam options", beams) + console.log("column options", columns) + }) +} + +// Reload of the viewer data, remove all objects, reload everything and zoom. +async function reloadViewer() { + viewer.sceneManager.removeAllObjects() + await viewer.loadObject( + objUrl(streamId, mainBranch.commits.items[0].referencedObject) + ) + console.log(`Loaded latest commit from branch "${mainBranch.name}"`) + + // Load beam if selected + if (selectedBeam) { + console.log("alternative is selected, should load", selectedBeam) + await viewer.loadObject(objUrl(streamId, selectedBeam)) + console.log("loaded alternative") + } + + // Load column if selected + if (selectedColumn) { + console.log("alternative is selected, should load", selectedColumn) + await viewer.loadObject(objUrl(streamId, selectedColumn)) + console.log("loaded alternative") + } + + viewer.interactions.zoomExtents(0.95, true) +} + +// Reload viewer every time an alternative is selected in the dropdown. +async function onColumnAlternativeSelected(event) { + const selection = event.target.value.substring( + event.target.selectionStart, + event.target.selectionEnd + ) + selectedColumn = selection + console.log("selected", selectedColumn) + document.getElementById("reloadButton").classList.add("is-loading") + await reloadViewer() + document.getElementById("reloadButton").classList.remove("is-loading") +} + +async function onBeamAlternativeSelected(event) { + const selection = event.target.value.substring( + event.target.selectionStart, + event.target.selectionEnd + ) + selectedBeam = selection + console.log("selected", selectedBeam) + document.getElementById("reloadButton").classList.add("is-loading") + await reloadViewer() + document.getElementById("reloadButton").classList.remove("is-loading") +} + +// Hack to prevent scene from loosing current zoom. +viewer.sceneManager._postLoadFunction = () => { + viewer.reflectionsNeedUpdate = true +} + +document.getElementById("take-screenshot").addEventListener("click", () => { + console.log("screenshot clickd") + let data = viewer.interactions.screenshot() // transparent png. + + let pop = window.open() + pop.document.title = "SpeckleStreamScreenshot" + pop.document.body.style.backgroundColor = "grey" + + let img = new Image() + img.src = data + pop.document.body.appendChild(img) +}) diff --git a/WebStarter/index.html b/WebStarter/index.html index d50182c..1bcc835 100644 --- a/WebStarter/index.html +++ b/WebStarter/index.html @@ -7,14 +7,77 @@ Speckle Web Starter + + -
-
-
- +
+
+ +
+
+

Selected objects +

+

Data from selected objects

+

+
+
+ + + \ No newline at end of file diff --git a/WebStarter/speckleQueries.js b/WebStarter/speckleQueries.js new file mode 100644 index 0000000..158a44e --- /dev/null +++ b/WebStarter/speckleQueries.js @@ -0,0 +1,38 @@ +export const userInfoQuery = () => `query { + user { + name + }, + serverInfo { + name + company + } + }` + +export const streamCommitsQuery = (streamId, itemsPerPage, cursor) => `query { + stream(id: "${streamId}"){ + branches { + totalCount + cursor + items { + name + commits(limit: 1){ + items { + referencedObject + } + } + } + } + } +}` + +export const streamSearchQuery = search => `query { + streams(query: "${search}") { + totalCount + cursor + items { + id + name + updatedAt + } + } + }` diff --git a/WebStarter/speckleUtils.js b/WebStarter/speckleUtils.js new file mode 100644 index 0000000..4933337 --- /dev/null +++ b/WebStarter/speckleUtils.js @@ -0,0 +1,39 @@ +import { + streamCommitsQuery, + streamSearchQuery, + userInfoQuery +} from "./speckleQueries.js" + +export let SERVER_URL = "https://speckle.xyz" + +// Calls the GraphQL endpoint of the Speckle server with a specific query. +export async function speckleFetch(query, token) { + console.log("fetching with token ", token) + if (token) + try { + var res = await fetch(`${SERVER_URL}/graphql`, { + method: "POST", + headers: { + Authorization: "Bearer " + token, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + query: query + }) + }) + return await res.json() + } catch (err) { + console.error("API call failed", err) + } + else return Promise.reject("You are not logged in (token does not exist)") +} + +// Fetch the current user data using the userInfoQuery +export const getUserData = token => speckleFetch(userInfoQuery(), token) + +// Fetch for streams matching the specified text using the streamSearchQuery +export const searchStreams = (e, token) => speckleFetch(streamSearchQuery(e)) + +// Get commits related to a specific stream, allows for pagination by passing a cursor +export const getStreamCommits = (streamId, itemsPerPage, cursor, token) => + speckleFetch(streamCommitsQuery(streamId, itemsPerPage, cursor), token) diff --git a/WebStarter/styles.css b/WebStarter/styles.css index aa32654..df1409f 100644 --- a/WebStarter/styles.css +++ b/WebStarter/styles.css @@ -1,15 +1,43 @@ +@import "https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"; + body { margin: 0; padding: 0; -} - -#viewerParent { - min-width: 100vw; - min-height: 100vh; + background-color: whitesmoke !important; } #viewer { - min-width: 100vw; - min-height: 80vh; - border: 1px solid black; + width: 100vw; + height: 100vh; +} + +#menu { + position: fixed; + display: flex; + flex-grow: 1; + flex-direction: column; + margin: 1em; +} + +#selected-panel { + position: absolute; + top: 0em; + right: 0em; + width: 450px !important; + margin: 1em; + max-height: 50% !important; + overflow-y: auto; +} + +.bottom-left { + position: fixed; + bottom: 0; + margin: 1em; +} + +.bottom-right { + position: fixed; + bottom: 0; + right: 0; + margin: 1em; }