Compare commits

...

24 Commits

Author SHA1 Message Date
izzy lyseggen 9b5c043029 feat(ui): saved streams & 1-click send (#37)
* feat(ui): saved streams and wip 1 click

* style(connector): remove some puts statements

* feat(accounts): default acct helper

* feat(ui): more 1 click send

need a way to wait for connector launch...

* fix(ui): waiting quick send!

* feat(ui): view in web from toast notif

note to self: the one in frontend is kinda ugly 😓 
went with outine btn for now bc it was better than that big ol dark gray btn

* fix: turn off dev

i always fkn do this i needa just use an env file gdi

* feat(ui): notify ui of oneclick send

* feat(metrics): remove old and use the new!
2022-03-08 10:15:21 +00:00
izzy lyseggen 0eb835bed0 fix(convert): add "cm" to unit strings and skip revit params (#34)
* feat(convert): skip revit params

cant do anything w them so skipping for now for efficiency

* fix(convert): add "cm" to unit strings
2022-02-23 10:11:48 +00:00
Dimitrie Stefanescu 7a7ce8ff3d feat(ui): css-ing around + notifications on actions (#33) 2022-02-14 21:05:03 +00:00
izzy lyseggen 473f0890fe fix(accounts): manually grab user dir (#30)
issue with i assume company managed computers where the "current user"
is pointing to somewhere else (maybe the admin who installed sketchup?)

to solve, I'm manually grabbing the user dir from the pwd

closes Auth error and no streams found #27
2022-01-19 12:01:23 +00:00
izzy lyseggen a8955a435f chore(deps): objectloader 2.3.0 and update others (#29)
closes 📨 Receiving fails if objects have nulls #28
2022-01-13 07:21:33 -08:00
izzylys 80f25eb1a2 fix(batching): simplify slightly
also fix a transition vs transition-group warning
2022-01-12 12:26:37 -08:00
izzylys 5716f92fbf fix(accts): sqlite gem loading 2022-01-06 09:13:45 -08:00
izzylys 63e7aaa661 feat(ui): send progress 2022-01-05 12:15:56 -08:00
izzylys 7234a4be79 feat(auth): log some info to see what's goin on 2022-01-05 12:15:31 -08:00
izzylys e1cceb9bba chore: readme typo and some clenaup 2022-01-05 07:09:53 -08:00
izzy lyseggen 30157b5ac2 feat(ui): animations were TOO FAST 4 ALA 🏃‍♀️ 2021-12-08 15:49:50 +00:00
izzy lyseggen f9b1628c18 Merge pull request #26 from specklesystems/izzy/ui-tweaks
feat(ui): cute lil animations 4 smooth commitmsgs
2021-12-08 15:33:21 +00:00
izzy lyseggen 9429a11fa7 feat(ui): cute lil animations 4 smooth commitmsgs 2021-12-08 15:31:53 +00:00
izzy lyseggen af4c6d9f71 Merge pull request #24 from specklesystems/izzy/nested-def-fix
fix(convert): `to_native` nested block fix
2021-12-03 18:04:22 +00:00
izzy lyseggen a4f23e060c fix(convert): to_native nested block fix
block definitions nested within definitions that also contain meshes
alongside those definitions were not getting picked up

fixes #23
2021-12-03 18:00:53 +00:00
izzy lyseggen d2099c98e0 Merge pull request #22 from specklesystems/izzy/ui
feat(ui): add commit messages
2021-12-03 11:40:01 +00:00
izzy lyseggen c21b51135a feat(ui): add commit messages
closes UI Improvements #4
2021-12-03 11:39:18 +00:00
izzy lyseggen c5c8bf6b6f Merge pull request #21 from specklesystems/izzy/transforms-rework
feat(converter): new transform style
2021-12-02 17:30:32 +00:00
izzy lyseggen d9a92e90ec feat(convert): receive both old and new transform blocsk 2021-12-02 17:28:38 +00:00
izzy lyseggen 79ae201646 feat(convert): new transform to speckle 2021-12-01 17:58:39 +00:00
izzy lyseggen 01b32d2558 Merge pull request #19 from specklesystems/izzy/platform-fix
fix(connector): speckle dir on macos
2021-11-24 11:34:54 +00:00
izzy lyseggen b3067aa346 fix(connector): speckle dir on macos
thanks @ruggieroguida !
addresses #18
2021-11-23 16:13:03 +00:00
izzy lyseggen d1349c5df1 Merge pull request #16 from specklesystems/izzy/kill-insertion-pt
feat(converter): depreciate insertion point
2021-11-17 15:30:03 +00:00
izzy lyseggen d44365a6d4 feat(converter): depreciate insertion point 2021-11-17 15:29:16 +00:00
20 changed files with 8226 additions and 3569 deletions
+3 -3
View File
@@ -77,15 +77,15 @@ This should have also have set up the package installer `gem` and interactive ru
gem -v
irb -v
Let's also install our first gem `bundle` which is a package manager that will help us with development.
Let's also install our first gem `bundler` which is a package manager that will help us with development.
gem install bundle
gem install bundler
### Editor Setup
Clone this repo and run:
bundle install
bundler install
This will install all the necessary packages for the connector.
-59
View File
@@ -1,59 +0,0 @@
require "net/http"
require "json"
require "uri"
server = "https://latest.speckle.dev"
token = "1dc2e3330a56371dc9011e5bed406264c9e65dd355"
limit = 20
streams_list = "
query User {
user {
id
email
name
bio
company
avatar
verified
profiles
role
streams(limit: #{limit}) {
totalCount
cursor
items {
id
name
description
isPublic
createdAt
updatedAt
collaborators {
id
name
role
}
}
}
}
}
"
endpoint = URI("#{server}/graphql")
res =
::Net::HTTP.start(endpoint.host, endpoint.port, use_ssl: true) do |http|
req = ::Net::HTTP::Post.new(endpoint)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
# The body needs to be a JSON string.
req.body = ::JSON[{ query: streams_list }]
puts(req.body)
http.request(req)
end
streams = ::JSON.parse(res.body)["data"]["user"]["streams"]["items"]
puts(streams)
+15 -12
View File
@@ -5,7 +5,6 @@ begin
rescue LoadError
# ty msp-greg! https://github.com/MSP-Greg/SUMisc/releases/tag/sqlite3-mingw-1
Gem.install(File.join(File.dirname(File.expand_path(__FILE__)), "utils/sqlite3-1.4.2.mspgreg-x64-mingw32.gem"))
else
require("sqlite3")
end
@@ -24,6 +23,11 @@ module SpeckleSystems::SpeckleConnector
rows.map { |row| JSON.parse(row[1]) }
end
def self.default_account
accts = load_accounts
accts.select { |acc| acc["isDefault"] }[0] || accts[0]
end
def self.get_suuid
dir = _get_speckle_dir
suuid_path = File.join(dir, "suuid")
@@ -33,22 +37,21 @@ module SpeckleSystems::SpeckleConnector
end
def self._get_speckle_dir
platform = RUBY_PLATFORM.downcase
speckle_dir =
if platform =~ (/mingw/) || platform =~ (/win/)
# win
File.join(Dir.home, "AppData/Roaming/Speckle")
elsif platform =~ /linux/
# linux
File.expand_path("~/.local/share/Speckle")
case Sketchup.platform
# sometimes Dir.home on windows points somewhere else bc I guess it's picking up a higher level user?
when :platform_win then File.join(Dir.pwd[%r{^((?:[^/]*/){3})}], "AppData/Roaming/Speckle")
when :platform_osx then File.join(Dir.home, ".config", "Speckle")
else
# mac
File.expand_path("~/.config/Speckle")
nil
end
return speckle_dir if Dir.exist?(speckle_dir)
raise(IOError, "No Speckle Directory exists. Please read the guide to get Speckle set up on your machine: \nhttps://speckle.guide/user/manager.html")
raise(
IOError,
"No Speckle Directory exists. Please read the guide to get Speckle set up on your machine: \nhttps://speckle.guide/user/manager.html"
)
end
end
end
@@ -3,7 +3,7 @@ require "speckle_connector/converter/to_speckle"
require "speckle_connector/converter/to_native"
module SpeckleSystems::SpeckleConnector
SKETCHUP_UNIT_STRINGS = { "m" => "m", "mm" => "mm", "ft" => "feet", "in" => "inch", "yd" => "yard" }.freeze
SKETCHUP_UNIT_STRINGS = { "m" => "m", "mm" => "mm", "ft" => "feet", "in" => "inch", "yd" => "yard", "cm" => "cm" }.freeze
public_constant :SKETCHUP_UNIT_STRINGS
class ConverterSketchup
include ToNative
+11 -2
View File
@@ -6,6 +6,8 @@ module SpeckleSystems::SpeckleConnector::ToNative
if can_convert_to_native(obj)
convert_to_native(obj, Sketchup.active_model.entities)
elsif obj.is_a?(Hash) && obj.key?("speckle_type")
return if is_ignored_speckle_type(obj)
puts(">>> Found #{obj["speckle_type"]}: #{obj["id"]}")
props = obj.keys.filter_map { |key| key unless key.start_with?("_") }
props.each { |prop| traverse_commit_object(obj[prop]) }
@@ -32,7 +34,12 @@ module SpeckleSystems::SpeckleConnector::ToNative
].include?(obj["speckle_type"])
end
def is_ignored_speckle_type(obj)
["Objects.BuiltElements.Revit.Parameter"].include?(obj["speckle_type"])
end
def convert_to_native(obj, entities = SketchUp.active_model.entities)
puts(">>> Converting #{obj["speckle_type"]}: #{obj["id"]}")
case obj["speckle_type"]
when "Objects.Geometry.Line", "Objects.Geometry.Polyline" then edge_to_native(obj, entities)
when "Objects.Other.BlockInstance" then component_instance_to_native(obj, entities)
@@ -111,8 +118,10 @@ module SpeckleSystems::SpeckleConnector::ToNative
is_group = false
definition = component_definition_to_native(block["blockDefinition"])
transform = transform_to_native(block["transform"], block["units"])
transform = transform_to_native(
block["transform"].is_a?(Hash) ? block["transform"]["value"] : block["transform"],
block["units"]
)
instance =
if is_group
entities.add_group(definition.entities.to_a)
+28 -23
View File
@@ -40,7 +40,6 @@ module SpeckleSystems::SpeckleConnector::ToSpeckle
def component_instance_to_speckle(instance, is_group: false)
transform = instance.transformation
origin = transform.origin
{
speckle_type: "Objects.Other.BlockInstance",
applicationId: instance.guid,
@@ -50,17 +49,19 @@ module SpeckleSystems::SpeckleConnector::ToSpeckle
name: instance.name,
renderMaterial: instance.material.nil? ? nil : material_to_speckle(instance.material),
transform: transform_to_speckle(transform),
insertionPoint: speckle_point(origin[0], origin[1], origin[2]),
"@blockDefinition" => component_definition_to_speckle(instance.definition)
}
end
def group_mesh_to_speckle(component_def)
mat_groups = {}
nested_blocks = []
component_def.entities.each do |face|
next unless face.typename == "Face"
component_def.entities.each do |entity|
nested_blocks.push(component_instance_to_speckle(entity)) if entity.typename == "ComponentInstance"
next unless entity.typename == "Face"
face = entity
# convert material
mat_id = face.material.nil? ? "none" : face.material.entityID
mat_groups[mat_id] = initialise_group_mesh(face, component_def.bounds) unless mat_groups.key?(mat_id)
@@ -76,29 +77,33 @@ module SpeckleSystems::SpeckleConnector::ToSpeckle
end
mat_groups.values.map { |group| group.delete(:pt_count) }
mat_groups.values
mat_groups.values + nested_blocks
end
def transform_to_speckle(transform)
t_arr = transform.to_a
[
t_arr[0],
t_arr[4],
t_arr[8],
length_to_speckle(t_arr[12]),
t_arr[1],
t_arr[5],
t_arr[9],
length_to_speckle(t_arr[13]),
t_arr[2],
t_arr[6],
t_arr[10],
length_to_speckle(t_arr[14]),
t_arr[3],
t_arr[7],
t_arr[11],
t_arr[15]
]
{
speckle_type: "Objects.Other.Transform",
units: @units,
value: [
t_arr[0],
t_arr[4],
t_arr[8],
length_to_speckle(t_arr[12]),
t_arr[1],
t_arr[5],
t_arr[9],
length_to_speckle(t_arr[13]),
t_arr[2],
t_arr[6],
t_arr[10],
length_to_speckle(t_arr[14]),
t_arr[3],
t_arr[7],
t_arr[11],
t_arr[15]
]
}
end
def initialise_group_mesh(face, bounds)
+126 -12
View File
@@ -7,8 +7,23 @@ require "speckle_connector/accounts"
module SpeckleSystems::SpeckleConnector
UNITS = { 0 => "in", 1 => "ft", 2 => "mm", 3 => "cm", 4 => "m", 5 => "yd" }.freeze
public_constant :UNITS
@to_send = {}
@connected = false
def self.create_dialog
def self.queue_send(stream_id, converted)
@to_send = { stream_id: stream_id, converted: converted }
send_from_queue(stream_id) if @connected
end
def self.send_from_queue(stream_id)
return unless @to_send[:stream_id] == stream_id
@dialog.execute_script("convertedFromSketchup('#{@to_send[:stream_id]}',#{@to_send[:converted].to_json})")
@dialog.execute_script("oneClickSend('#{@to_send[:stream_id]}')")
@to_send = {}
end
def self.init_dialog
options = {
dialog_title: "SpeckleSketchUp",
preferences_key: "example.htmldialog.materialinspector",
@@ -21,11 +36,11 @@ module SpeckleSystems::SpeckleConnector
dialog
end
def self.show_dialog
def self.create_dialog(show: true)
if @dialog&.visible?
@dialog.bring_to_front
else
@dialog ||= create_dialog
@dialog ||= init_dialog
@dialog.add_action_callback("send_selection") do |_action_context, stream_id|
send_selection(stream_id)
nil
@@ -37,13 +52,30 @@ module SpeckleSystems::SpeckleConnector
@dialog.add_action_callback("reload_accounts") do |_action_context|
reload_accounts
end
@dialog.add_action_callback("init_local_accounts") do |_action_context|
init_local_accounts
end
@dialog.add_action_callback("load_saved_streams") do |_action_context|
load_saved_streams
end
@dialog.add_action_callback("save_stream") do |_action_context, stream_id|
save_stream(stream_id)
end
@dialog.add_action_callback("remove_stream") do |_action_context, stream_id|
remove_stream(stream_id)
end
@dialog.add_action_callback("notify_connected") do |_action_context, stream_id|
notify_connected(stream_id)
end
# set connected to false when dialog is closed
@dialog.set_can_close do
@connected = false
!@connected
end
if DEV_MODE
puts('Launching Speckle Connector from http://localhost:8081')
puts("Launching Speckle Connector from http://localhost:8081")
@dialog.set_url("http://localhost:8081")
else
html_file = File.join(File.dirname(File.expand_path(__FILE__)), "html", "index.html")
@@ -51,16 +83,23 @@ module SpeckleSystems::SpeckleConnector
@dialog.set_file(html_file)
end
@dialog.show
@dialog
@dialog.show if show
end
@dialog
end
def self.notify_connected(stream_id)
@connected = true
send_from_queue(stream_id)
end
def self.convert_to_speckle(to_convert)
converter = ConverterSketchup.new(UNITS[Sketchup.active_model.options["UnitsOptions"]["LengthUnit"]])
to_convert.map { |entity| converter.convert_to_speckle(entity) }
end
def self.send_selection(stream_id)
model = Sketchup.active_model
converter = ConverterSketchup.new(UNITS[model.options["UnitsOptions"]["LengthUnit"]])
converted = model.selection.map { |entity| converter.convert_to_speckle(entity) }
converted = convert_to_speckle(Sketchup.active_model.selection)
puts("converted #{converted.count} objects for stream #{stream_id}")
# puts(converted.to_json)
@dialog.execute_script("convertedFromSketchup('#{stream_id}',#{converted.to_json})")
@@ -80,11 +119,86 @@ module SpeckleSystems::SpeckleConnector
@dialog.execute_script("sketchupOperationFailed('#{stream_id}')")
end
def self.one_click_send
acct = Accounts.default_account
return puts("No local account found. Please refer to speckle.guide for more information.") if acct.nil?
create_dialog
to_convert = Sketchup.active_model.selection.count > 0 ? Sketchup.active_model.selection : Sketchup.active_model.entities
if first_saved_stream.nil?
create_stream(to_convert)
else
queue_send(first_saved_stream, convert_to_speckle(to_convert))
end
rescue StandardError => e
puts(e)
@dialog.execute_script("sketchupOperationFailed('#{@to_send[:stream_id]}')")
end
def self.first_saved_stream
saved_streams = Sketchup.active_model.attribute_dictionary("speckle", true)["streams"] or []
saved_streams.nil? || saved_streams.empty? ? nil : saved_streams[0]
end
def self.load_saved_streams
saved_streams = Sketchup.active_model.attribute_dictionary("speckle", true)["streams"] or []
@dialog.execute_script("setSavedStreams(#{saved_streams})")
end
def self.init_local_accounts
puts("Initialisation of Speckle accounts requested by plugin")
@dialog.execute_script("loadAccounts(#{Accounts.load_accounts.to_json}, #{Accounts.get_suuid.to_json})")
end
def self.reload_accounts
puts("Reload of Speckle accounts requested by plugin")
@dialog.execute_script("loadAccounts(#{Accounts.load_accounts.to_json})")
load_saved_streams
end
def self.save_stream(stream_id)
speckle_dict = Sketchup.active_model.attribute_dictionary("speckle", true)
saved = speckle_dict["streams"] || []
saved = saved.empty? ? [stream_id] : saved.unshift(stream_id)
speckle_dict["streams"] = saved
load_saved_streams
end
def self.remove_stream(stream_id)
speckle_dict = Sketchup.active_model.attribute_dictionary("speckle", true)
saved = speckle_dict["streams"] || []
saved -= [stream_id]
speckle_dict["streams"] = saved
load_saved_streams
end
def self.create_stream(to_convert)
acct = Accounts.default_account
return if acct.nil?
path = Sketchup.active_model.path
stream_name = path ? File.basename(path, ".*") : "Untitled SketchUp Model"
query = "mutation streamCreate($stream: StreamCreateInput!) {streamCreate(stream: $stream)}"
vars = { stream: { name: stream_name, description: "Stream created from SketchUp" } }
request = Sketchup::Http::Request.new("#{acct["serverInfo"]["url"]}/graphql", Sketchup::Http::POST)
request.headers = { "Authorization" => "Bearer #{acct["token"]}", "Content-Type" => "application/json" }
request.body = { query: query, variables: vars }.to_json
request.start do |_req, res|
res_data = JSON.parse(res.body)["data"]
raise(StandardError) unless res_data
stream_id = res_data["streamCreate"]
save_stream(stream_id)
queue_send(stream_id, convert_to_speckle(to_convert))
# send_selection(stream_id)
end
load_saved_streams
rescue StandardError => e
puts(e)
puts("Could not create a new stream")
end
end
+13 -3
View File
@@ -1,13 +1,13 @@
# frozen_string_literal: true
require "sketchup"
require "speckle_connector/dialog.rb"
require "speckle_connector/debug.rb"
require "speckle_connector/dialog"
require "speckle_connector/debug"
module SpeckleSystems
module SpeckleConnector
unless file_loaded?(__FILE__)
cmd_cube = UI::Command.new("Dialog") { show_dialog }
cmd_cube = UI::Command.new("Dialog") { create_dialog }
cmd_cube.tooltip = "Launch Connector"
cmd_cube.status_bar_text = "Opens the Speckle Connector window"
cmd_cube.small_icon = "icons/s2logo.png"
@@ -16,8 +16,18 @@ module SpeckleSystems
menu = UI.menu("Tools")
menu.add_item(cmd_cube)
cmd_send = UI::Command.new("Send") { one_click_send }
cmd_send.tooltip = "Send to Speckle"
cmd_send.status_bar_text = "Send to Speckle"
cmd_send.small_icon = "icons/Sender.png"
cmd_send.large_icon = "icons/Sender.png"
menu = UI.menu("Tools")
menu.add_item(cmd_send)
toolbar = UI::Toolbar.new("Speckle")
toolbar.add_item(cmd_cube)
toolbar.add_item(cmd_send)
toolbar.restore
file_loaded(__FILE__)
+7505 -3238
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -9,15 +9,16 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@speckle/objectloader": "^2.1.1",
"@speckle/objectloader": "^2.3.0",
"aws-sdk": "^2.981.0",
"core-js": "^3.6.5",
"debounce": "^1.2.1",
"mixpanel-browser": "^2.45.0",
"register-service-worker": "^1.7.1",
"sqlite3": "^5.0.2",
"v-tooltip": "^2.1.3",
"vue": "^2.6.11",
"vue-apollo": "^3.0.0-beta.11",
"vue-matomo": "^4.1.0",
"vue-router": "^3.2.0",
"vue-timeago": "^5.1.3",
"vuetify": "^2.4.0"
@@ -41,4 +42,4 @@
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.7.0"
}
}
}
+11 -5
View File
@@ -85,6 +85,7 @@
<v-container fluid>
<router-view :stream-search-query="streamSearchQuery" />
</v-container>
<global-toast />
</v-main>
</v-app>
</template>
@@ -96,6 +97,7 @@ import userQuery from './graphql/user.gql'
import { onLogin } from './vue-apollo'
global.loadAccounts = function (accounts, suuid) {
console.log('>>> SpeckleSketchup: Loading accounts', accounts, `suuid: ${suuid}`)
localStorage.setItem('localAccounts', JSON.stringify(accounts))
if (suuid) {
localStorage.setItem('suuid', suuid)
@@ -117,7 +119,9 @@ global.setSelectedAccount = function (account) {
export default {
name: 'App',
components: {},
components: {
GlobalToast: () => import('@/components/GlobalToast')
},
props: {
size: {
type: Number,
@@ -143,6 +147,7 @@ export default {
mounted() {
bus.$on('selected-account-reloaded', async () => {
await onLogin(this.$apollo.provider.defaultClient)
this.$refreshMixpanelIds()
this.refresh()
})
bus.$on('streams-loaded', () => {
@@ -158,18 +163,19 @@ export default {
switchTheme() {
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
localStorage.setItem('theme', this.$vuetify.theme.dark ? 'dark' : 'light')
this.$mixpanel.track('Connector Action', { name: 'Toggle Theme' })
},
switchAccount(account) {
this.$matomo && this.$matomo.setCustomUrl(`http://connectors/SketchUp/account/switch`)
this.$matomo && this.$matomo.trackPageView(`account/switch`)
this.$mixpanel.track('Connector Action', { name: 'Account Select' })
global.setSelectedAccount(account)
},
requestRefresh() {
sketchup.reload_accounts()
sketchup.load_saved_streams()
this.refresh()
},
refresh() {
this.$matomo && this.$matomo.setCustomUrl(`http://connectors/SketchUp/stream/list`)
this.$matomo && this.$matomo.trackPageView(`stream/list`)
this.$mixpanel.track('Connector Action', { name: 'Refresh' })
this.$apollo.queries.user.refetch()
bus.$emit('refresh-streams')
}
+48
View File
@@ -0,0 +1,48 @@
<template>
<v-snackbar v-model="snack" app bottom color="primary">
{{ text }}
<template #action="{}">
<v-btn v-if="actionName" small outlined @click="openUrl(url)" @click:append="snack = false">
{{ actionName }}
</v-btn>
<v-btn small icon @click="snack = false">
<v-icon small>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</template>
<script>
export default {
data() {
return {
snack: false,
text: null,
actionName: null,
url: null
}
},
watch: {
snack(newVal) {
if (!newVal) {
this.text = null
this.actionName = null
this.url = null
}
}
},
mounted() {
this.$eventHub.$on('notification', (args) => {
this.snack = true
this.text = args.text
this.actionName = args.action ? args.action.name : null
this.url = args.action ? args.action.url : null
})
},
methods: {
openUrl(link) {
this.$mixpanel.track('Connector Action', { name: 'Open In Web' })
window.open(link)
}
}
}
</script>
+232 -179
View File
@@ -1,164 +1,146 @@
<template>
<v-hover v-slot="{ hover }">
<v-card color="" class="mt-5 mb-5" style="transition: all 0.2s ease-in-out">
<v-row>
<v-col v-if="$apollo.loading">
<v-row>
<v-col><v-skeleton-loader type="article" /></v-col>
</v-row>
</v-col>
<v-col v-else>
<v-toolbar class="transparent elevation-0" dense>
<v-toolbar-title>{{ stream.name }}</v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-card-text class="transparent elevation-0 mt-0 pt-0" dense>
<div class="text-caption">
Updated
<timeago :datetime="stream.updatedAt" />
</div>
<v-toolbar-title>
<v-chip v-if="stream.role" small class="mr-1">
<v-icon small left>mdi-account-key-outline</v-icon>
{{ stream.role.split(':')[1] }}
</v-chip>
<v-menu offset-y>
<template #activator="{ on, attrs }">
<v-chip v-if="stream.branches" small v-bind="attrs" class="mr-1" v-on="on">
<v-icon small class="mr-1 float-left">mdi-source-branch</v-icon>
{{ branchName }}
</v-chip>
</template>
<v-list dense>
<v-list-item
v-for="(branch, index) in stream.branches.items"
:key="index"
link
@click="switchBranch(branch.name)"
>
<v-list-item-title class="text-caption font-weight-regular">
<v-icon v-if="branch.name == branchName" small class="mr-1 float-left">
mdi-check
</v-icon>
<v-icon v-else small class="mr-1 float-left">mdi-source-branch</v-icon>
{{ branch.name }} ({{ branch.commits.totalCount }})
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu offset-y>
<template #activator="{ on, attrs }">
<v-chip v-if="stream.commits" small v-bind="attrs" v-on="on">
<v-icon small class="mr-1 float-left">mdi-source-commit</v-icon>
{{ selectedBranch.commits.items.length ? commitId : 'no commits' }}
</v-chip>
</template>
<v-list dense>
<v-list-item
v-for="(commit, index) in selectedBranch.commits.items"
:key="index"
link
@click="switchCommit(commit.id)"
>
<v-list-item-title class="text-caption font-weight-regular">
<v-icon
v-if="(commitId == 'latest' && index == 0) || commit.id == commitId"
small
class="mr-1 float-left"
>
mdi-check
</v-icon>
<v-icon v-else small class="mr-1 float-left">mdi-source-commit</v-icon>
{{ commit.id }} |
<span class="font-weight-regular">{{ commit.message }} |</span>
<span class="font-weight-light ml-1">
<timeago :datetime="commit.createdAt" />
</span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar-title>
</v-card-text>
</v-col>
<v-col v-if="hover && !$apollo.loading" align="end" justify="center">
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-btn icon class="mr-4 btn-fix" v-bind="attrs" v-on="on" @click="openInWeb">
<v-icon>mdi-open-in-new</v-icon>
</v-btn>
</template>
<span>Open in web</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-btn
fab
:loading="loadingSend"
class="mr-4 elevation-1 btn-fix"
hint="Send"
v-bind="attrs"
v-on="on"
@click="send"
<v-card
v-if="stream"
:class="`mb-3 rounded-lg grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'}`"
@mouseenter="hover = true"
@mouseleave="hover = false"
>
<v-toolbar flat height="70">
<v-toolbar-title class="ml-0" style="position: relative; left: -10px">
<!-- Uncomment when pinning is in place and add style="position: relative; left: -10px" to the element above :) -->
<v-btn
v-tooltip="'Pin this stream - it will be saved to this file.'"
icon
x-small
@click="toggleSavedStream"
>
<v-icon v-if="saved" x-small>mdi-pin</v-icon>
<v-icon v-else x-small>mdi-pin-outline</v-icon>
</v-btn>
{{ stream.name }}
</v-toolbar-title>
<v-spacer />
<v-slide-x-transition>
<div v-show="hover" style="white-space: nowrap">
<v-btn v-tooltip="'View online'" icon small class="mr-3" @click="openInWeb">
<v-icon small>mdi-open-in-new</v-icon>
</v-btn>
<v-btn
v-tooltip="'Send'"
icon
class="mr-3 elevation-2"
:loading="loadingSend"
@click="send"
>
<!-- <v-icon>mdi-upload</v-icon> -->
<v-img v-if="$vuetify.theme.dark" src="@/assets/SenderWhite.png" max-width="30" />
<v-img v-else src="@/assets/Sender.png" max-width="30" />
</v-btn>
<v-btn
v-tooltip="'Receive'"
icon
class="elevation-2"
:loading="loadingReceive"
@click="receive"
>
<!-- <v-icon>mdi-download</v-icon> -->
<v-img v-if="$vuetify.theme.dark" src="@/assets/ReceiverWhite.png" max-width="30" />
<v-img v-else src="@/assets/Receiver.png" max-width="30" />
</v-btn>
</div>
</v-slide-x-transition>
</v-toolbar>
<v-card-text class="caption pt-1 text-truncate" style="white-space: nowrap">
Updated
<timeago class="mr-1" :datetime="stream.updatedAt" />
|
<v-icon class="ml-1" small>mdi-account-key-outline</v-icon>
{{ stream.role.split(':')[1] }}
</v-card-text>
<v-card-text class="d-flex align-center pb-5 mb-5 -mt-2" style="height: 50px">
<v-menu offset-y>
<template #activator="{ on, attrs }">
<v-chip v-if="stream.branches" small v-bind="attrs" class="mr-1" v-on="on">
<v-icon small class="mr-1 float-left">mdi-source-branch</v-icon>
{{ branchName }}
</v-chip>
</template>
<v-list dense>
<v-list-item
v-for="(branch, index) in stream.branches.items"
:key="index"
link
@click="switchBranch(branch.name)"
>
<v-list-item-title class="text-caption font-weight-regular">
<v-icon v-if="branch.name == branchName" small class="mr-1 float-left">
mdi-check
</v-icon>
<v-icon v-else small class="mr-1 float-left">mdi-source-branch</v-icon>
{{ branch.name }} ({{ branch.commits.totalCount }})
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu offset-y>
<template #activator="{ on, attrs }">
<v-chip v-if="stream.commits" small v-bind="attrs" v-on="on">
<v-icon small class="mr-1 float-left">mdi-source-commit</v-icon>
{{ selectedBranch.commits.items.length ? commitId : 'no commits' }}
</v-chip>
</template>
<v-list dense>
<v-list-item
v-for="(commit, index) in selectedBranch.commits.items"
:key="index"
link
@click="switchCommit(commit.id)"
>
<v-list-item-title class="text-caption font-weight-regular">
<v-icon
v-if="(commitId == 'latest' && index == 0) || commit.id == commitId"
small
class="mr-1 float-left"
>
<v-img
v-if="$vuetify.theme.dark"
src="@/assets/SenderWhite.png"
max-width="40"
style="display: inline-block"
/>
<v-img
v-else
src="@/assets/Sender.png"
max-width="40"
style="display: inline-block"
/>
</v-btn>
</template>
<span>Send</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-btn
fab
:loading="loadingReceive"
class="mr-4 elevation-1 btn-fix"
hint="Receive"
v-bind="attrs"
v-on="on"
@click="receive"
>
<v-img
v-if="$vuetify.theme.dark"
src="@/assets/ReceiverWhite.png"
max-width="40"
style="display: inline-block"
/>
<v-img
v-else
src="@/assets/Receiver.png"
max-width="40"
style="display: inline-block"
/>
</v-btn>
</template>
<span>Receive</span>
</v-tooltip>
</v-col>
</v-row>
<v-progress-linear
v-if="(loadingSend || loadingReceive) && loadingStage"
height="14"
indeterminate
>
<div class="text-caption">{{ loadingStage }}</div>
</v-progress-linear>
</v-card>
</v-hover>
mdi-check
</v-icon>
<v-icon v-else small class="mr-1 float-left">mdi-source-commit</v-icon>
{{ commit.id }} |
<span class="font-weight-regular">{{ commit.message }} |</span>
<span class="font-weight-light ml-1">
<timeago :datetime="commit.createdAt" />
</span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<div class="flex-grow-1 px-4">
<v-slide-y-transition>
<div v-show="hover">
<v-text-field
v-model="commitMessage"
xxxclass="small-text-field"
hide-details
dense
flat
placeholder="Write your commit message here"
></v-text-field>
</div>
</v-slide-y-transition>
</div>
</v-card-text>
<v-progress-linear
v-if="(loadingSend || loadingReceive) && loadingStage"
key="progress-bar"
height="14"
indeterminate
>
<div class="text-caption">{{ loadingStage }}</div>
</v-progress-linear>
</v-card>
<v-card v-else class="my-2">
<v-skeleton-loader type="article" />
</v-card>
</template>
<script>
@@ -181,21 +163,31 @@ global.sketchupOperationFailed = function (streamId) {
bus.$emit(`sketchup-fail-${streamId}`)
}
global.oneClickSend = function (streamId) {
bus.$emit(`one-click-send-${streamId}`)
}
export default {
name: 'StreamCard',
props: {
streamId: {
type: String,
default: null
},
saved: {
type: Boolean,
default: false
}
},
data() {
return {
hover: false,
loadingSend: false,
loadingReceive: false,
loadingStage: null,
branchName: 'main',
commitId: 'latest'
commitId: 'latest',
commitMessage: null
}
},
apollo: {
@@ -278,22 +270,26 @@ export default {
},
mounted() {
bus.$on(`sketchup-objects-${this.streamId}`, async (objects) => {
console.log('received objects from sketchup', objects)
console.log('>>> SpeckleSketchUp: Received objects from sketchup')
await this.createCommit(objects)
})
bus.$on(`sketchup-received-${this.streamId}`, () => {
console.log('finished receiving in sketchup', this.streamId)
console.log('>>> SpeckleSketchUp: Finished receiving in sketchup', this.streamId)
this.loadingReceive = false
this.loadingStage = null
})
bus.$on(`sketchup-fail-${this.streamId}`, () => {
this.$matomo && this.$matomo.setCustomUrl(`http://connectors/SketchUp/stream/fail`)
this.$matomo && this.$matomo.trackPageView(`stream/fail`)
console.log('sketchup operation failed', this.streamId)
this.$mixpanel.track('Connector Action', { name: 'Stream Fail' })
console.log('>>> SpeckleSketchUp: operation failed', this.streamId)
this.loadingReceive = this.loadingSend = false
this.loadingStage = null
})
bus.$on(`one-click-send-${this.streamId}`, () => {
this.$mixpanel.track('Send', { oneClick: true })
})
if (this.saved) sketchup.notify_connected(this.streamId)
},
methods: {
sleep(ms) {
@@ -301,21 +297,30 @@ export default {
},
openInWeb() {
window.open(`${localStorage.getItem('serverUrl')}/streams/${this.streamId}`)
this.$matomo && this.$matomo.setCustomUrl(`http://connectors/SketchUp/stream/open-in-web`)
this.$matomo && this.$matomo.trackPageView(`stream/open-in-web`)
this.$mixpanel.track('Connector Action', { name: 'Open In Web' })
},
switchBranch(branchName) {
this.$mixpanel.track('Connector Action', { name: 'Branch Switch' })
this.branchName = branchName
this.commitId = 'latest'
},
switchCommit(commitId) {
this.$mixpanel.track('Connector Action', { name: 'Commit Switch' })
this.commitId = commitId
},
toggleSavedStream() {
if (this.saved) {
sketchup.remove_stream(this.streamId)
this.$mixpanel.track('Connector Action', { name: 'Stream Remove' })
} else {
sketchup.save_stream(this.streamId)
this.$mixpanel.track('Connector Action', { name: 'Stream Save' })
}
},
async receive() {
this.loadingStage = 'requesting'
this.loadingReceive = true
this.$matomo && this.$matomo.setCustomUrl(`http://connectors/SketchUp/receive`)
this.$matomo && this.$matomo.trackPageView(`receive`)
this.$mixpanel.track('Receive')
const refId = this.selectedCommit?.referencedObject
if (!refId) {
this.loadingReceive = false
@@ -358,16 +363,18 @@ export default {
async send() {
this.loadingStage = 'converting'
this.loadingSend = true
this.$matomo && this.$matomo.setCustomUrl(`http://connectors/SketchUp/send`)
this.$matomo && this.$matomo.trackPageView(`send`)
this.$mixpanel.track('Send', { oneClick: false })
sketchup.send_selection(this.streamId)
console.log('request for data sent to sketchup')
console.log('>>> SpeckleSketchUp: Objects requested from SketchUp')
await this.sleep(2000)
},
async createCommit(objects) {
if (objects.length == 0) {
this.loadingSend = false
this.loadingStage = null
this.$eventHub.$emit('notification', {
text: 'No objects selected. Nothing was sent.'
})
return
}
@@ -379,20 +386,25 @@ export default {
this.loadingStage = 'uploading'
this.loadingSend = true
let batches = s.batchObjects()
const totBatches = batches.length
console.log(`>>> SpeckleSketchUp: ${totBatches} batches ready for sending`)
let batchesSent = 0
for (const batch of batches) {
let res = await this.sendBatch(batch)
if (res.status !== 201) throw `Upload request failed: ${res}`
if (res.status !== 201) throw `Upload request failed: ${res.status}`
batchesSent++
this.loadingStage = `uploading: ${Math.round((batchesSent / totBatches) * 100)}%`
}
let commit = {
streamId: this.streamId,
branchName: this.branchName,
objectId: hash,
message: 'sent from sketchup',
message: this.commitMessage ?? 'sent from sketchup',
sourceApplication: 'sketchup',
totalChildrenCount: s.objects[hash].totalChildrenCount
}
await this.$apollo.mutate({
var res = await this.$apollo.mutate({
mutation: gql`
mutation CommitCreate($commit: CommitCreateInput!) {
commitCreate(commit: $commit)
@@ -402,8 +414,17 @@ export default {
commit: commit
}
})
console.log('sent to stream: ' + this.streamId, commit)
console.log('>>> SpeckleSketchUp: Sent to stream: ' + this.streamId, commit)
this.$eventHub.$emit('notification', {
text: 'Model selection sent!',
action: {
name: 'View in Web',
url: `${localStorage.getItem('serverUrl')}/streams/${this.streamId}/commits/${
res.data.commitCreate
}`
}
})
this.$apollo.queries.stream.refetch()
this.loadingSend = false
this.loadingStage = null
} catch (err) {
@@ -414,7 +435,7 @@ export default {
},
async sendBatch(batch) {
let formData = new FormData()
formData.append(`batch-1`, new Blob([JSON.stringify(batch)], { type: 'application/json' }))
formData.append(`batch-1`, new Blob([batch], { type: 'application/json' }))
let token = localStorage.getItem('SpeckleSketchup.AuthToken')
let res = await fetch(`${localStorage.getItem('serverUrl')}/objects/${this.streamId}`, {
method: 'POST',
@@ -427,7 +448,39 @@ export default {
}
</script>
<style>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease-in;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.expand-enter-active {
transition: all 0.2s ease;
max-height: 1200px;
overflow: hidden;
}
.expand-leave-active {
transition: all 0.3s ease;
max-height: 1200px;
overflow: hidden;
}
.expand-enter,
.expand-leave-to {
max-height: 0;
opacity: 0;
}
.v-text-field >>> input {
font-size: 0.9em;
}
.v-text-field >>> label {
font-size: 0.9em;
}
.btn-fix:focus::before {
opacity: 0 !important;
}
-11
View File
@@ -1,11 +0,0 @@
<template>
<v-container>
<v-card>hello there!</v-card>
</v-container>
</template>
<script>
export default {}
</script>
<style></style>
+6 -6
View File
@@ -4,18 +4,18 @@ import router from './router'
import vuetify from './plugins/vuetify'
import { createProvider } from './vue-apollo'
Vue.prototype.$eventHub = new Vue()
Vue.config.productionTip = false
import VueTimeago from 'vue-timeago'
Vue.use(VueTimeago, { locale: 'en' })
import VueMatomo from 'vue-matomo'
import VueTooltip from 'v-tooltip'
Vue.use(VueTooltip)
Vue.use(VueMatomo, {
host: 'https://speckle.matomo.cloud',
siteId: 2,
userId: localStorage.getItem('suuid')
})
import SpeckleMetrics from './plugins/speckle-metrics'
Vue.use(SpeckleMetrics, { token: 'acd87c5a50b56df91a795e999812a3a4' })
export const bus = new Vue()
+42
View File
@@ -0,0 +1,42 @@
var mixpanel = require('mixpanel-browser')
import crypto from 'crypto'
const SpeckleMetrics = {
install(Vue, { token, config }) {
config = config || {
// eslint-disable-next-line camelcase
api_host: 'https://analytics.speckle.systems'
}
Vue.prototype.$mixpanel = mixpanel
Vue.prototype.$mixpanel.init(token, config)
Vue.prototype.$mixpanel.register({ hostApp: 'sketchup', type: 'action' })
Vue.prototype.$refreshMixpanelIds = function () {
let distinctId =
'@' +
crypto
.createHash('md5')
.update(
JSON.parse(localStorage.getItem('selectedAccount'))['userInfo']['email'].toLowerCase()
)
.digest('hex')
.toUpperCase()
let serverId = crypto
.createHash('md5')
.update(localStorage.getItem('serverUrl').toLowerCase())
.digest('hex')
.toUpperCase()
Vue.prototype.$mixpanel.register({
// eslint-disable-next-line camelcase
distinct_id: distinctId,
// eslint-disable-next-line camelcase
server_id: serverId
})
}
}
}
export default SpeckleMetrics
+1 -1
View File
@@ -1,6 +1,6 @@
import Vue from 'vue'
import Vuetify from 'vuetify/lib/framework'
import '@/scss/styles.css'
Vue.use(Vuetify)
export default new Vuetify({
+121
View File
@@ -0,0 +1,121 @@
.background-light {
background: #8e9eab;
background: -webkit-linear-gradient(to top right, #eeeeee, #c8e8ff) !important;
background: linear-gradient(to top right, #ffffff, #c8e8ff) !important;
}
.background-dark {
background: #141e30;
background: -webkit-linear-gradient(to top left, #243b55, #141e30) !important;
background: linear-gradient(to top left, #243b55, #141e30) !important;
}
/* TOOLTIPs */
.tooltip {
display: block !important;
z-index: 10000;
font-family: 'Roboto', sans-serif !important;
font-size: 0.75rem !important;
}
.tooltip .tooltip-inner {
background: rgba(0, 0, 0, 1);
color: white;
border-radius: 16px;
padding: 5px 10px 4px;
}
.tooltip .tooltip-arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
border-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.tooltip[x-placement^='top'] {
margin-bottom: 5px;
}
.tooltip[x-placement^='top'] .tooltip-arrow {
border-width: 5px 5px 0 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
bottom: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.tooltip[x-placement^='bottom'] {
margin-top: 5px;
}
.tooltip[x-placement^='bottom'] .tooltip-arrow {
border-width: 0 5px 5px 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
top: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.tooltip[x-placement^='right'] {
margin-left: 5px;
}
.tooltip[x-placement^='right'] .tooltip-arrow {
border-width: 5px 5px 5px 0;
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
left: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
.tooltip[x-placement^='left'] {
margin-right: 5px;
}
.tooltip[x-placement^='left'] .tooltip-arrow {
border-width: 5px 0 5px 5px;
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
right: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
.tooltip.popover .popover-inner {
background: #f9f9f9;
color: black;
padding: 24px;
border-radius: 5px;
box-shadow: 0 5px 30px rgba(black, 0.1);
}
.tooltip.popover .popover-arrow {
border-color: #f9f9f9;
}
.tooltip[aria-hidden='true'] {
visibility: hidden;
opacity: 0;
transition: opacity 0.15s, visibility 0.15s;
}
.tooltip[aria-hidden='false'] {
visibility: visible;
opacity: 1;
transition: opacity 0.15s;
}
+5 -5
View File
@@ -170,21 +170,21 @@ export class BaseObjectSerializer {
batchObjects(maxBatchSizeMb = 1) {
const maxSize = maxBatchSizeMb * 1000 * 1000
let batches = []
let batch = []
let batch = '['
let batchSize = 0
let objects = Object.values(this.objects)
objects.forEach((obj) => {
let objString = JSON.stringify(obj)
if (batchSize + objString.length < maxSize) {
batch.push(obj)
batch += objString + ','
batchSize += objString.length
} else {
batches.push(batch)
batch = [obj]
batches.push(batch.slice(0, -1) + ']')
batch = '[' + objString + ','
batchSize = objString.length
}
})
batches.push(batch)
batches.push(batch.slice(0, -1) + ']')
return batches
}
+54 -6
View File
@@ -1,5 +1,5 @@
<template>
<v-container>
<div>
<v-row>
<v-col v-if="$apollo.loading && !streams">
<v-row>
@@ -10,8 +10,13 @@
</v-col>
</v-row>
<div v-if="!streamsFound" class="text-subtitle-1 text-center mt-8">No streams found... 👀</div>
<div v-if="streams">
<div v-for="stream in streams.items" :key="stream.id">
<div v-if="savedStreams" class="mt-5">
<div v-for="streamId in savedStreams" :key="streamId">
<stream-card :stream-id="streamId" :saved="true" />
</div>
</div>
<div v-if="allStreamsList" class="mt-5">
<div v-for="stream in allStreamsList" :key="stream.id">
<stream-card :stream-id="stream.id" />
</div>
<div class="actions text-center">
@@ -26,13 +31,19 @@
</v-btn>
</div>
</div>
</v-container>
</div>
</template>
<script>
/*global sketchup*/
import gql from 'graphql-tag'
import { bus } from '../main'
global.setSavedStreams = function (streamIds) {
localStorage.setItem('savedStreams', JSON.stringify(streamIds))
bus.$emit('set-saved-streams', streamIds)
}
const streamLimit = 5
export default {
name: 'Streams',
@@ -44,18 +55,33 @@ export default {
},
data() {
return {
showMoreEnabled: true
showMoreEnabled: true,
savedStreams: []
}
},
computed: {
streamsFound() {
return this.streams && this.streams?.items?.length != 0
return (this.streams && this.streams?.items?.length != 0) || this.savedStreams?.length !== 0
},
isSavedStream(streamId) {
return this.savedStreams?.includes(streamId)
},
allStreamsList() {
if (this.$apollo.loading) return
return this.streams?.items.filter((stream) => !this.savedStreams?.includes(stream.id))
}
},
mounted() {
bus.$on('refresh-streams', () => {
this.$apollo.queries.streams.refetch()
})
bus.$on('set-saved-streams', (streamIds) => {
this.savedStreams = streamIds
})
sketchup.load_saved_streams()
console.log('LAUNCHED')
this.$mixpanel.track('Connector Action', { name: 'Launched' })
},
apollo: {
streams: {
@@ -85,6 +111,28 @@ export default {
this.showMoreEnabled = data.streams?.items.length < data.streams.totalCount
return data.streams
}
},
$subscribe: {
userStreamAdded: {
query: gql`
subscription {
userStreamAdded
}
`,
result() {
this.$apollo.queries.stream.refetch()
}
},
userStreamRemoved: {
query: gql`
subscription {
userStreamRemoved
}
`,
result() {
this.$apollo.queries.stream.refetch()
}
}
}
},
methods: {