diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..81dd4f810 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Elements.Playground/Elements.Playground.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Elements.Playground/Elements.Playground.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/Elements.Playground/Elements.Playground.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Elements.Playground/App.razor b/Elements.Playground/App.razor index 651131dea..15ffa5a92 100644 --- a/Elements.Playground/App.razor +++ b/Elements.Playground/App.razor @@ -1,10 +1 @@ - - - - - - -

Sorry, there's nothing at this address.

-
-
-
+ \ No newline at end of file diff --git a/Elements.Playground/Compiler.cs b/Elements.Playground/Compiler.cs index da6ba3011..5dc00166e 100644 --- a/Elements.Playground/Compiler.cs +++ b/Elements.Playground/Compiler.cs @@ -12,6 +12,11 @@ namespace Elements.Playground { + public class Globals + { + public string InputJson { get; set; } + } + public static class Compiler { class BlazorBoot @@ -30,42 +35,39 @@ class Resources public Dictionary pdb { get; set; } public Dictionary runtime { get; set; } } - - private static Task InitializationTask; private static List References; - public static void InitializeMetadataReferences(HttpClient client) - { - async Task InitializeInternal() - { - var model = new Model(); - Console.WriteLine("Initializing the code editor..."); + private static bool _isInitialized = false; - // TODO: This loads every assembly that is available. We should - // see if we can limit this to just the ones that we need. - var response = await client.GetFromJsonAsync("_framework/blazor.boot.json"); - var assemblies = await Task.WhenAll(response.resources.assembly.Keys.Select(x => client.GetAsync("_framework/" + x))); - var references = new List(assemblies.Length); - foreach (var asm in assemblies) - { - using var task = await asm.Content.ReadAsStreamAsync(); - references.Add(MetadataReference.CreateFromStream(task)); - } - References = references; - } - InitializationTask = InitializeInternal(); + public static bool IsReady() + { + return _isInitialized; } - public static Task WhenReady(Func action) + public static async Task InitializeMetadataReferences(HttpClient client) { - if (InitializationTask.Status != TaskStatus.RanToCompletion) + if (_isInitialized) { - return InitializationTask.ContinueWith(x => action()); + return; } - else + var model = new Model(); + Console.WriteLine("Loading metadata references..."); + // TODO: Make this conditional on some build flag, so we can easily + // deploy to prod or dev, or allow others cloning the project to + // spec the URL where they'll be hosting it. + var rootUrl = "https://elements.hypar.io/"; + // TODO: This loads every assembly that is available. We should + // see if we can limit this to just the ones that we need. + var response = await client.GetFromJsonAsync($"{rootUrl}blazor.boot.json"); + var assemblies = await Task.WhenAll(response.resources.assembly.Keys.Select(x => client.GetAsync($"{rootUrl}{x}"))); + var references = new List(assemblies.Length); + foreach (var asm in assemblies) { - return action(); + using var task = await asm.Content.ReadAsStreamAsync(); + references.Add(MetadataReference.CreateFromStream(task)); } + References = references; + _isInitialized = true; } public static (bool success, Assembly asm, Compilation compilation) LoadSource(string source) @@ -83,11 +85,15 @@ public static (bool success, Assembly asm, Compilation compilation) LoadSource(s "System.Collections.Generic", "System.Console", "System.Linq", +"System.Text.Json", +"System.Text.Json.Serialization", "Elements", "Elements.Geometry", +"Elements.Geometry.Solids", +"Elements.Spatial", "Elements.Geometry.Profiles", "Elements.Validators" - }, concurrentBuild: false)); + }, concurrentBuild: false), globalsType: typeof(Globals)); ImmutableArray diagnostics = compilation.GetDiagnostics(); diff --git a/Elements.Playground/Elements.Playground.csproj b/Elements.Playground/Elements.Playground.csproj index 83a14308e..f2a6af55a 100644 --- a/Elements.Playground/Elements.Playground.csproj +++ b/Elements.Playground/Elements.Playground.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 en false false @@ -9,10 +9,8 @@ - - - - + + diff --git a/Elements.Playground/ElementsAPI.razor b/Elements.Playground/ElementsAPI.razor new file mode 100644 index 000000000..dc84e6da0 --- /dev/null +++ b/Elements.Playground/ElementsAPI.razor @@ -0,0 +1,152 @@ +@page "/" + +@inject IJSRuntime JSRuntime + +
+ +@code { + + public class Result + { + public bool Success { get; set; } + + public string Output { get; set; } + + public string ModelJson { get; set; } + } + + // Why do we need a component to call Elements compile and + // run elements code from the browser? + // 1. We need an HttpClient passed in from the framework. We can't + // create it because this code runs sandboxed in wasm. It has to + // come from blazor and the only way to do that is by injecting it + // as a service. + // 2. We need the the IJSRuntime to call methods in javascript. This + // also needs to be injected as a service. + + private static Globals globals = new Globals(); + private static IJSRuntime runtime; + [Inject] private HttpClient Client { get; set; } + private static Func executionDelegate; + + protected override async Task OnInitializedAsync() + { + runtime = JSRuntime; + await Compiler.InitializeMetadataReferences(Client); + await base.OnInitializedAsync(); + await Task.FromResult(0); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + } + + [JSInvokable] + public static async Task Run() + { + var sw = Stopwatch.StartNew(); + var currentOut = Console.Out; + var writer = new StringWriter(); + Console.SetOut(writer); + var success = false; + + if (executionDelegate == null) + { + Console.WriteLine("There is nothing to execute. Try compiling first."); + Console.SetOut(currentOut); + sw.Stop(); + return new Result() + { + Success = success, + Output = writer.ToString() + }; + } + + var model = await (Task)executionDelegate(new object[] { globals, null }) as Elements.Model; + Console.WriteLine($"Run successful in {sw.ElapsedMilliseconds} ms"); + sw.Reset(); + + await Task.Run(async () => + { + sw.Start(); + var glb = model.ToGlTF(); + await runtime.InvokeVoidAsync("elements.loadModel", glb); + Console.WriteLine($"GlTF loaded in {sw.ElapsedMilliseconds} ms"); + success = true; + }); + + Console.SetOut(currentOut); + + sw.Stop(); + + return new Result() + { + Success = success, + Output = writer.ToString(), + ModelJson = model.ToJson() + }; + } + + [JSInvokable] + public static Result Compile(string code) + { + if (!Compiler.IsReady()) + { + return new Result + { + Success = false, + Output = "Compiler not ready" + }; + } + var sw = Stopwatch.StartNew(); + + var currentOut = Console.Out; + var writer = new StringWriter(); + Console.SetOut(writer); + var compileSuccess = false; + + // Redirect debug to the console. + Trace.Listeners.Clear(); + Trace.Listeners.Add( + new TextWriterTraceListener(Console.Out)); + + executionDelegate = null; + + // Compilation warnings will be written to console.out + // which is redirected here to the writer. + var (success, asm, compilation) = Compiler.LoadSource(code); + if (success) + { + var entryPoint = compilation.GetEntryPoint(CancellationToken.None); + var type = asm.GetType($"{entryPoint.ContainingNamespace.MetadataName}.{entryPoint.ContainingType.MetadataName}"); + var entryPointMethod = type.GetMethod(entryPoint.MetadataName); + + executionDelegate = (Func)entryPointMethod.CreateDelegate(typeof(Func)); + Console.WriteLine($"Compilation successful in {sw.ElapsedMilliseconds} ms"); + compileSuccess = true; + } + else + { + Console.WriteLine("Compilation was not successful. The execution will not run."); + compileSuccess = false; + } + + Console.SetOut(currentOut); + + sw.Stop(); + + return new Result() + { + Success = compileSuccess, + Output = writer.ToString() + }; + } + + [JSInvokable] + public static void UpdateInputs(string json) + { + globals.InputJson = json; + return; + } +} \ No newline at end of file diff --git a/Elements.Playground/Pages/Model.razor b/Elements.Playground/Pages/Model.razor deleted file mode 100644 index bc9adafa5..000000000 --- a/Elements.Playground/Pages/Model.razor +++ /dev/null @@ -1,154 +0,0 @@ -@page "/" - -@using Microsoft.CodeAnalysis -@using Microsoft.CodeAnalysis.CSharp -@using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting -@using System.Reflection -@using System.IO -@using System.Threading -@using System.Web - -@inject IJSRuntime JSRuntime -@inject IJSUnmarshalledRuntime JSUnmarshalledRuntime -@inject NavigationManager MyNavigationManager - -
- -
@csharp
-
-
@((MarkupString)Output)
-
- -@code { - bool loading = true; - private static string csharp = @"var model = new Model(); - -var length = 10; - -Validator.DisableValidationOnConstruction = true; -var m = BuiltInMaterials.Concrete; -var wf = new WideFlangeProfileFactory(); -var p = wf.GetProfileByType(WideFlangeProfileType.W10x100); - -for(var i=0; i<1; i++) -{ - var start = new Vector3(i, 0, 0); - var end = new Vector3(i, length, i); - - // The bottom chord - var bottomChord = new Line(start, end); - var bottomChordBeam = new Beam(bottomChord, p, m); - model.AddElement(bottomChordBeam); - - var topChord = new Line(start + new Vector3(0,0,5), end + new Vector3(0,0,5)); - var topChordBeam = new Beam(topChord, p, m); - model.AddElement(topChordBeam); - - Vector3 last = default(Vector3); - for(var j=0.0; j<=1.0; j+=0.1) - { - var pt = bottomChord.PointAt(j); - var top = pt + new Vector3(0,0,5); - var panelLine = new Line(pt, top); - var panelBeam = new Beam(panelLine, p, m); - model.AddElement(panelBeam); - - if(j > 0.0) - { - var braceLine = new Line(top, last); - var braceBeam = new Beam(braceLine, p, m); - model.AddElement(braceBeam); - } - last = pt; - } -} -return model;"; - private static string Output = ""; - private static IJSUnmarshalledRuntime Runtime; - [Inject] private HttpClient Client { get; set; } - - protected override Task OnInitializedAsync() - { - Runtime = JSUnmarshalledRuntime; - - Compiler.InitializeMetadataReferences(Client); - - base.OnInitializedAsync(); - - return Compiler.WhenReady(() => - { - loading = false; - return Task.FromResult(0); - }); - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); - if (firstRender) - { - await JSRuntime.InvokeVoidAsync("model.initializeEditor"); - await JSRuntime.InvokeVoidAsync("model.initialize3D"); - } - } - - [JSInvokable] - public static void SetCodeValue(string code) - { - csharp = code; - } - - [JSInvokable] - public static Task Run() - { - return Compiler.WhenReady(RunInternal); - } - - static async Task RunInternal() - { - Output = ""; - - Console.WriteLine("Compiling and Running code"); - var sw = Stopwatch.StartNew(); - - var currentOut = Console.Out; - var writer = new StringWriter(); - Console.SetOut(writer); - - Exception exception = null; - try - { - var (success, asm, compilation) = Compiler.LoadSource(csharp); - if (success) - { - var entryPoint = compilation.GetEntryPoint(CancellationToken.None); - var type = asm.GetType($"{entryPoint.ContainingNamespace.MetadataName}.{entryPoint.ContainingType.MetadataName}"); - var entryPointMethod = type.GetMethod(entryPoint.MetadataName); - - var submission = (Func)entryPointMethod.CreateDelegate(typeof(Func)); - var model = await (Task)submission(new object[] { null, null }); - Console.WriteLine($"\r\nCompilation successful in {sw.ElapsedMilliseconds} ms"); - - await Task.Run(() => - { - var glb = ((Elements.Model)model).ToGlTF(); - Runtime.InvokeUnmarshalled("model.loadModel", glb); - }); - } - } - catch (Exception ex) - { - exception = ex; - } - Output = writer.ToString(); - if (exception != null) - { - - Output += "\r\n" + exception.ToString(); - } - Console.SetOut(currentOut); - - sw.Stop(); - - } -} \ No newline at end of file diff --git a/Elements.Playground/Pages/Model.razor.css b/Elements.Playground/Pages/Model.razor.css deleted file mode 100644 index 2c5965f69..000000000 --- a/Elements.Playground/Pages/Model.razor.css +++ /dev/null @@ -1,49 +0,0 @@ -.grid { - position: relative; - display: grid; - grid-template-columns: 50% 50%; - grid-template-rows: auto 200px; - grid-template-areas: - "code model" - "output output"; - border: 1px solid lightgray; - height: calc(100vh - 16px); -} - -#editor { - grid-area: code; - border-right: 1px solid lightgray; -} - -#model { - grid-area: model; - background: linear-gradient(#b8b8b8, #ffffff); -} - -.output { - overflow-y: auto; - font-family: monospace; - font-size: 10pt; - color: darkgray; - grid-area: output; - border-top: 1px solid lightgray; - padding: 10px; -} - -.error { - color: red; -} - -.success { - color: green; -} - -.message { - color: gray; -} - -.run { - position: absolute; - right: 10px; - bottom: 10px; -} \ No newline at end of file diff --git a/Elements.Playground/README.md b/Elements.Playground/README.md index 2c84deef2..790ff62c5 100644 --- a/Elements.Playground/README.md +++ b/Elements.Playground/README.md @@ -1,19 +1,28 @@ # Elements Playground -A playground for testing Elements in the browser. +The elements web assembly API, for using Elements in the browser. -This is a minimal example using .NET 5, Blazor webassembly, and Elements. +### Running Locally +`./serve.sh` - to serve Elements wasm assets +Edit `wwwroot/index.html` to use localhost:5001, instead of the `https://elements.hypar.io` url. +Serve `wwroot` (e.g. `python3 -m http.server`). -### Running -`dotnet watch run` +### Publishing +`dotnet publish -c release` -Visit http://localhost:5000. +### Deploy +The elements web assembly assets are served from S3 via cloud front at https://elements.hypar.io. The deploy script will publish the "app" and copy and deploy all necessary files. +``` +./deploy.sh +``` -### Publishing -`dotnet public -c release` +### Building an application with Elements in the browser. +This repo contains a demo application that demonstrates the usage of the Elements web assembly APIs. The demo application has two input elements, a slider and a button. The button compiles some test code which relies on an input value provided by the slider. + +See the `index.html` in the demo application for an example of how to do the following. +- Add a script tag in your `` which loads `elements.js`. `Elements.js` contains the API for elements. +- Add an `` tag which hangs the application in the DOM. +- Add a a script tag which loads `blazor.webassembly.js` in the body, and ensure it is set to `autostart=false`. +- Add a call to `Blazor.start` in a subsequent script to fetch Blazor assets from a preferred location, or from disk. + +**NOTE** The Elements web assembly API does not need to interact with the DOM, but we still need to create a blazor component to inject some of the infrastructure required by the compiler, like the `HttpClient` instance and the `IJSRuntime` instance. You will need to inlcude the `app` element in your application, which puts an empty div in the DOM. We're looking at ways to remove this requirement in the future. -### Notes about hosting on Github Pages -- In the site's repo on github - - Replace the base path in `index.html` with the path of the root of your site. For example, replace `/` with `/ElementsPlaygroundSite/` where `ElementsPlaygroundSite` is the name of the repo where the site is located. - - Include a `.nojekyll` file to avoid the system skipping files and folders that start with `_`. - - Add a `404.html` with contents identical to `index.html` to support redirection. - - Add a `.gitattributes` file with `* binary` to avoid git modifying the assemblies in such a way that their hash becomes invalid. \ No newline at end of file diff --git a/Elements.Playground/_Imports.razor b/Elements.Playground/_Imports.razor index fdfb61996..cf5bfe872 100644 --- a/Elements.Playground/_Imports.razor +++ b/Elements.Playground/_Imports.razor @@ -1,4 +1,9 @@ -@using System.Net.Http +@using System.Diagnostics +@using System.Reflection +@using System.IO +@using System.Threading +@using System.Web +@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @@ -9,6 +14,9 @@ @using Elements.Playground.Shared @using Elements @using Elements.Geometry +@using Elements.Geometry.Profiles @using Elements.Serialization.glTF -@using System.Diagnostics @using Microsoft.CodeAnalysis.CSharp.Scripting +@using Microsoft.CodeAnalysis +@using Microsoft.CodeAnalysis.CSharp +@using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting diff --git a/Elements.Playground/deploy.sh b/Elements.Playground/deploy.sh new file mode 100755 index 000000000..eabc08cc2 --- /dev/null +++ b/Elements.Playground/deploy.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +source='./bin/Release/net6.0/wwwroot/_framework' + +echo "Building the app..." +dotnet publish -c release + +echo "Uploading assets to s3..." +aws s3 sync "$source" s3://elements-wasm/ --exclude "*.gz" --exclude "*.br" + +echo "Creating an invalidation..." +aws cloudfront create-invalidation --distribution-id E19YQ16H2KTDNU --paths "/*" \ No newline at end of file diff --git a/Elements.Playground/serve.py b/Elements.Playground/serve.py new file mode 100644 index 000000000..270e1d628 --- /dev/null +++ b/Elements.Playground/serve.py @@ -0,0 +1,38 @@ + +# !/usr/bin/env python3 + +# Use this to serve the built Elements.Playground locally. + +# Adapted from Francesco Pira's python http server with cors: https://fpira.com/blog/2020/05/python-http-server-with-cors + +from http.server import HTTPServer, SimpleHTTPRequestHandler +import sys +import os + +# app client URL +client = 'http://localhost:8080' + +# move to the framework directory +os.chdir('bin/release/net6.0/publish/wwwroot/_framework') + + +class CORSRequestHandler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header('Access-Control-Allow-Origin', client) + self.send_header('Access-Control-Allow-Methods', client) + self.send_header('Access-Control-Allow-Headers', client) + self.send_header('Access-Control-Allow-Credentials', 'true') + self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate') + return super(CORSRequestHandler, self).end_headers() + + def do_OPTIONS(self): + self.send_response(200) + self.end_headers() + + +host = sys.argv[1] if len(sys.argv) > 2 else '0.0.0.0' +port = int(sys.argv[len(sys.argv) - 1]) if len(sys.argv) > 1 else 5001 + +print("Listening on {}:{}".format(host, port)) +httpd = HTTPServer((host, port), CORSRequestHandler) +httpd.serve_forever() diff --git a/Elements.Playground/serve.sh b/Elements.Playground/serve.sh new file mode 100755 index 000000000..a20e05d15 --- /dev/null +++ b/Elements.Playground/serve.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +echo "Building the app..." +dotnet publish -c release -p:RunAOTCompilation=false + +echo "Serving the build assets at http://localhost:5001" +python3 -m serve.py \ No newline at end of file diff --git a/Elements.Playground/wwwroot/css/app.css b/Elements.Playground/wwwroot/css/app.css deleted file mode 100644 index 0a6d7f46a..000000000 --- a/Elements.Playground/wwwroot/css/app.css +++ /dev/null @@ -1,23 +0,0 @@ -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -a, .btn-link { - color: #0366d6; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -app { - position: relative; - display: flex; - flex-direction: column; -} - -.main { - flex: 1; -} \ No newline at end of file diff --git a/Elements.Playground/wwwroot/favicon.ico b/Elements.Playground/wwwroot/favicon.ico deleted file mode 100644 index a3a799985..000000000 Binary files a/Elements.Playground/wwwroot/favicon.ico and /dev/null differ diff --git a/Elements.Playground/wwwroot/index.html b/Elements.Playground/wwwroot/index.html index 09d3bae76..840e74d17 100644 --- a/Elements.Playground/wwwroot/index.html +++ b/Elements.Playground/wwwroot/index.html @@ -6,19 +6,56 @@ Elements Playground - - - + - Loading... - - - + + + + + +
+

+
\ No newline at end of file diff --git a/Elements.Playground/wwwroot/js/elements.d.ts b/Elements.Playground/wwwroot/js/elements.d.ts new file mode 100644 index 000000000..484a926ee --- /dev/null +++ b/Elements.Playground/wwwroot/js/elements.d.ts @@ -0,0 +1,11 @@ +export interface RunResult { + output: string, + success: boolean, + modelJson?: string, +} +export class Elements { + public testCode: string + compile(code: string): Promise + run(): Promise + updateInputs(inputs: string): Promise +} \ No newline at end of file diff --git a/Elements.Playground/wwwroot/js/elements.js b/Elements.Playground/wwwroot/js/elements.js new file mode 100644 index 000000000..2f34f96d1 --- /dev/null +++ b/Elements.Playground/wwwroot/js/elements.js @@ -0,0 +1,34 @@ +// Note: If updating this file to expose new APIs, please also update the adjacent type definition file at elements.d.ts. +class Elements { + testCode = ` + var model = new Model(); + // This class can be modified to suit your needs. It + // should be a model of the JSON you plan to send through updateInputs. + class InputClass { + public double? Height {get; set;} + } + var input = JsonSerializer.Deserialize(InputJson); + var mass = new Mass(Polygon.Rectangle(1,1), input.Height ?? 5, BuiltInMaterials.Wood); + Console.WriteLine($"The volume of the mass is {mass.Volume()}."); + model.AddElement(mass); + return model;` + + async compile (code) { + return DotNet.invokeMethodAsync('Elements.Playground', 'Compile', code) + } + + async run () { + return DotNet.invokeMethodAsync('Elements.Playground', 'Run'); + } + + async updateInputs (inputs) { + return DotNet.invokeMethod('Elements.Playground', 'UpdateInputs', inputs); + } + + loadModel (bytes) { + // console.debug(bytes); + return true; + } +} + +window.elements = new Elements(); \ No newline at end of file diff --git a/Elements.Playground/wwwroot/js/model.js b/Elements.Playground/wwwroot/js/model.js deleted file mode 100644 index b99318baa..000000000 --- a/Elements.Playground/wwwroot/js/model.js +++ /dev/null @@ -1,122 +0,0 @@ -import * as THREE from 'https://cdn.skypack.dev/three@0.133.0/build/three.module.js' -import { OrbitControls } from 'https://cdn.skypack.dev/three@0.133.0/examples/jsm/controls/OrbitControls.js'; -import { GLTFLoader } from 'https://cdn.skypack.dev/three@0.133.0/examples/jsm/loaders/GLTFLoader.js'; - -window.model = { - initialize3D: () => { initialize3D(); }, - initializeEditor: () => { initializeEditor(); }, - loadModel: (glb) => { loadModel(glb) }, -}; - -const scene = new THREE.Scene(); -var gltfScene = null; -var editor = null; - -function loadModel(glb) { - const contentArray = Blazor.platform.toUint8Array(glb); - const blob = new Blob([new Uint8Array(contentArray)], { type: "application/octet-stream" }); - const blobUrl = URL.createObjectURL(blob); - - const loader = new GLTFLoader(); - - loader.load( - blobUrl, - function (gltf) { - - if (gltfScene != null) { - scene.remove(gltfScene); - } - - scene.add(gltf.scene); - gltfScene = gltf.scene; - - URL.revokeObjectURL(blobUrl); - - return true; - }, - function (xhr) { - console.log((xhr.loaded / xhr.total * 100) + '% loaded'); - }, - function (error) { - console.log('An error happened'); - console.log(error) - } - ); -} - -function initializeEditor() { - ace.config.set("packaged", true) - ace.config.set("basePath", "https://pagecdn.io/lib/ace/1.4.12/") - ace.require("ace/ext/language_tools"); - editor = ace.edit("editor"); - editor.setTheme("ace/theme/tomorrow"); - editor.session.setMode("ace/mode/csharp"); - editor.setOptions({ - fontSize: "10pt", - enableBasicAutocompletion: true, - enableSnippets: true, - enableLiveAutocompletion: true - }); - - let timer; - let runTimer = () => { - timer = setTimeout(() => { - invokeRun(); - }, 2000); - } - - editor.getSession().on('change', function () { - var code = editor.getValue(); - DotNet.invokeMethod('Elements.Playground', 'SetCodeValue', code) - clearTimeout(timer); - runTimer(); - }); -} - -function invokeRun() { - DotNet.invokeMethodAsync('Elements.Playground', 'Run'); -} - -function initialize3D() { - const div = document.getElementById("model"); - const camera = new THREE.PerspectiveCamera(75, div.clientWidth / div.clientHeight, 0.1, 1000); - - const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); - - renderer.setSize(div.clientWidth, div.clientHeight); - div.appendChild(renderer.domElement); - - const controls = new OrbitControls(camera, renderer.domElement); - - const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); - directionalLight.position.set(0.5, 0.5, 0); - scene.add(directionalLight); - - const size = 100; - const divisions = 20; - - const gridHelper = new THREE.GridHelper(size, divisions, "darkgray", "lightgray"); - scene.add(gridHelper); - - const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1.0); - scene.add(light); - - camera.position.z = 5; - camera.position.y = 10; - - controls.update(); - - const animate = function () { - requestAnimationFrame(animate); - controls.update(); - renderer.render(scene, camera); - }; - - window.addEventListener('resize', () => { - camera.aspect = div.clientWidth / div.clientHeight; - camera.updateProjectionMatrix(); - renderer.setSize(div.clientWidth, div.clientHeight); - }, false); - - animate(); -} \ No newline at end of file diff --git a/Elements/src/Geometry/Color.cs b/Elements/src/Geometry/Color.cs index 5569e21d4..9a96a3bf3 100644 --- a/Elements/src/Geometry/Color.cs +++ b/Elements/src/Geometry/Color.cs @@ -292,5 +292,18 @@ public static double LinearToSRGB(double c) { return (c < 0.0031308) ? 12.92 * c : (1.055 * Math.Pow(c, 0.41666)) - 0.055; } + + /// + /// Convert the color to hexadecimal. + /// + /// + public string ToHex() + { + var r = (byte)(Red * 255.0); + var g = (byte)(Green * 255.0); + var b = (byte)(Blue * 255.0); + // var a = (byte)(Alpha * 255.0); + return "#" + r.ToString("X2") + g.ToString("X2") + b.ToString("X2"); // + a.ToString("X2"); + } } } \ No newline at end of file