# HG changeset patch # User am@osimis.io # Date 1535621796 -7200 # Node ID 2038d76bf13fff3451bd5560770f860aeeab8c1b # Parent 6b3d91857b969a9c0a18f57129158b8c729a8f45 interaction with HTML/JS diff -r 6b3d91857b96 -r 2038d76bf13f Applications/IBasicApplication.h --- a/Applications/IBasicApplication.h Tue Aug 28 15:34:20 2018 +0200 +++ b/Applications/IBasicApplication.h Thu Aug 30 11:36:36 2018 +0200 @@ -24,6 +24,7 @@ #include "BasicApplicationContext.h" #include #include "../Framework/Viewport/WidgetViewport.h" +#include "json/json.h" namespace OrthancStone { @@ -49,7 +50,6 @@ virtual IWidget* GetCentralWidget() = 0; virtual void Finalize() = 0; - }; } diff -r 6b3d91857b96 -r 2038d76bf13f Applications/Samples/SimpleViewerApplication.h --- a/Applications/Samples/SimpleViewerApplication.h Tue Aug 28 15:34:20 2018 +0200 +++ b/Applications/Samples/SimpleViewerApplication.h Thu Aug 30 11:36:36 2018 +0200 @@ -31,6 +31,10 @@ #include "../../Framework/Messages/IObserver.h" #include "../../Framework/SmartLoader.h" +#if ORTHANC_ENABLE_WASM==1 +#include "../../Platforms/Wasm/IStoneApplicationToWebApplicationAdapter.h" +#include "../../Platforms/Wasm/Defaults.h" +#endif #include namespace OrthancStone @@ -39,6 +43,9 @@ { class SimpleViewerApplication : public SampleApplicationBase, +#if ORTHANC_ENABLE_WASM==1 + public IStoneApplicationToWebApplicationAdapter, +#endif public IObserver { private: @@ -178,6 +185,7 @@ LayerWidget* mainWidget_; std::vector thumbnails_; std::map> instancesIdsPerSeriesId_; + std::map seriesTags_; unsigned int currentInstanceIndex_; OrthancStone::WidgetViewport* wasmViewport1_; @@ -297,6 +305,7 @@ { // keep track of all instances IDs const std::string& seriesId = response["ID"].asString(); + seriesTags_[seriesId] = response; instancesIdsPerSeriesId_[seriesId] = std::vector(); for (size_t i = 0; i < response["Instances"].size(); i++) { @@ -351,19 +360,12 @@ } } -#if ORTHANC_ENABLE_WASM==1 - virtual void InitializeWasm() { - - AttachWidgetToWasmViewport("canvas", thumbnailsLayout_); - AttachWidgetToWasmViewport("canvas2", mainWidget_); - } -#endif - - - void SelectSeriesInMainViewport(const std::string& seriesId) { mainWidget_->ReplaceLayer(0, smartLoader_->GetFrame(instancesIdsPerSeriesId_[seriesId][0], 0)); +#if ORTHANC_ENABLE_WASM==1 + NotifyStatusUpdateFromCppToWeb("series-description=" + seriesTags_[seriesId]["MainDicomTags"]["SeriesDescription"].asString()); +#endif } virtual void OnPushButton1Clicked() {} @@ -381,6 +383,33 @@ pushButton1 = "action1"; pushButton2 = "action2"; } + +#if ORTHANC_ENABLE_WASM==1 + virtual void HandleMessageFromWeb(std::string& output, const std::string& input) { + if (input == "select-tool:line-measure") + { + currentTool_ = Tools_LineMeasure; + NotifyStatusUpdateFromCppToWeb("currentTool=line-measure"); + } + else if (input == "select-tool:circle-measure") + { + currentTool_ = Tools_CircleMeasure; + NotifyStatusUpdateFromCppToWeb("currentTool=circle-measure"); + } + + output = "ok"; + } + + virtual void NotifyStatusUpdateFromCppToWeb(const std::string& statusUpdateMessage) { + UpdateStoneApplicationStatusFromCpp(statusUpdateMessage.c_str()); + } + + virtual void InitializeWasm() { + + AttachWidgetToWasmViewport("canvas", thumbnailsLayout_); + AttachWidgetToWasmViewport("canvas2", mainWidget_); + } +#endif }; diff -r 6b3d91857b96 -r 2038d76bf13f Applications/Samples/Web/simple-viewer.html --- a/Applications/Samples/Web/simple-viewer.html Tue Aug 28 15:34:20 2018 +0200 +++ b/Applications/Samples/Web/simple-viewer.html Thu Aug 30 11:36:36 2018 +0200 @@ -13,15 +13,20 @@ +
-
- - - - +
+ line + circle + +
diff -r 6b3d91857b96 -r 2038d76bf13f Applications/Samples/Web/simple-viewer.ts --- a/Applications/Samples/Web/simple-viewer.ts Tue Aug 28 15:34:20 2018 +0200 +++ b/Applications/Samples/Web/simple-viewer.ts Thu Aug 30 11:36:36 2018 +0200 @@ -1,3 +1,39 @@ -/// +/// InitializeWasmApplication("OrthancStoneSimpleViewer", "/orthanc"); + +function SelectTool(toolName: string) { + SendMessageToStoneApplication("select-tool:" + toolName); +} + +function PerformAction(actionName: string) { + SendMessageToStoneApplication("perform-action:" + actionName); +} + +//initializes the buttons +//----------------------- +// install "SelectTool" handlers +document.querySelectorAll("[tool-selector]").forEach((e) => { + console.log(e); + (e as HTMLInputElement).addEventListener("click", () => { + console.log(e); + SelectTool(e.attributes["tool-selector"].value); + }); +}); + +// install "PerformAction" handlers +document.querySelectorAll("[action-trigger]").forEach((e) => { + (e as HTMLInputElement).addEventListener("click", () => { + PerformAction(e.attributes["action-trigger"].value); + }); +}); + +// this method is called "from the C++ code" when the StoneApplication is updated. +// it can be used to update the UI of the application +function UpdateWebApplication(statusUpdateMessage: string) { + console.log(statusUpdateMessage); + + if (statusUpdateMessage.startsWith("series-description=")) { + document.getElementById("series-description").innerText = statusUpdateMessage.split("=")[1]; + } +} diff -r 6b3d91857b96 -r 2038d76bf13f Applications/Samples/Web/tsconfig-samples.json --- a/Applications/Samples/Web/tsconfig-samples.json Tue Aug 28 15:34:20 2018 +0200 +++ b/Applications/Samples/Web/tsconfig-samples.json Thu Aug 30 11:36:36 2018 +0200 @@ -4,7 +4,8 @@ "sourceMap": false, "lib" : [ "es2017", - "dom" + "dom", + "dom.iterable" ] } } diff -r 6b3d91857b96 -r 2038d76bf13f Framework/Layers/CircleMeasureTracker.cpp --- a/Framework/Layers/CircleMeasureTracker.cpp Tue Aug 28 15:34:20 2018 +0200 +++ b/Framework/Layers/CircleMeasureTracker.cpp Thu Aug 30 11:36:36 2018 +0200 @@ -76,8 +76,10 @@ if (fontSize_ != 0) { cairo_move_to(cr, x, y); +#if ORTHANC_ENABLE_NATIVE==1 // text rendering currently fails in wasm CairoFont font("sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); font.Draw(context, FormatRadius(), static_cast(fontSize_) / zoom); +#endif } } diff -r 6b3d91857b96 -r 2038d76bf13f Framework/Layers/LineMeasureTracker.cpp --- a/Framework/Layers/LineMeasureTracker.cpp Tue Aug 28 15:34:20 2018 +0200 +++ b/Framework/Layers/LineMeasureTracker.cpp Thu Aug 30 11:36:36 2018 +0200 @@ -63,8 +63,10 @@ if (fontSize_ != 0) { cairo_move_to(cr, x2_, y2_ - static_cast(fontSize_) / zoom); +#if ORTHANC_ENABLE_NATIVE==1 // text rendering currently fails in wasm CairoFont font("sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); font.Draw(context, FormatLength(), static_cast(fontSize_) / zoom); +#endif } } diff -r 6b3d91857b96 -r 2038d76bf13f Platforms/Wasm/Defaults.cpp --- a/Platforms/Wasm/Defaults.cpp Tue Aug 28 15:34:20 2018 +0200 +++ b/Platforms/Wasm/Defaults.cpp Thu Aug 30 11:36:36 2018 +0200 @@ -7,6 +7,7 @@ #include #include #include "Applications/Wasm/StartupParametersBuilder.h" +#include "Platforms/Wasm/IStoneApplicationToWebApplicationAdapter.h" static unsigned int width_ = 0; static unsigned int height_ = 0; @@ -14,6 +15,7 @@ /**********************************/ static std::unique_ptr application; +static OrthancStone::IStoneApplicationToWebApplicationAdapter* applicationWebAdapter = NULL; static std::unique_ptr context; static OrthancStone::StartupParametersBuilder startupParametersBuilder; static OrthancStone::MessageBroker broker; @@ -43,7 +45,7 @@ ViewportHandle EMSCRIPTEN_KEEPALIVE CreateCppViewport() { std::shared_ptr viewport(new OrthancStone::WidgetViewport); - printf("viewport %x\n", viewport.get()); + printf("viewport %x\n", (int)viewport.get()); viewports_.push_back(viewport); @@ -67,6 +69,7 @@ printf("CreateWasmApplication\n"); application.reset(CreateUserApplication(broker)); + applicationWebAdapter = dynamic_cast(application.get()); WasmWebService::SetBroker(broker); startupParametersBuilder.Clear(); @@ -254,6 +257,18 @@ viewport->MouseLeave(); } + const char* EMSCRIPTEN_KEEPALIVE SendMessageToStoneApplication(const char* message) + { + static std::string output; // we don't want the string to be deallocated when we return to JS code so we always use the same string (this is fine since JS is single-thread) + + if (applicationWebAdapter != NULL) { + printf("sending message to C++"); + applicationWebAdapter->HandleMessageFromWeb(output, std::string(message)); + return output.c_str(); + } + return "This stone application does not have a Web Adapter"; + } + #ifdef __cplusplus } diff -r 6b3d91857b96 -r 2038d76bf13f Platforms/Wasm/Defaults.h --- a/Platforms/Wasm/Defaults.h Tue Aug 28 15:34:20 2018 +0200 +++ b/Platforms/Wasm/Defaults.h Thu Aug 30 11:36:36 2018 +0200 @@ -16,6 +16,7 @@ // JS methods accessible from C++ extern void ScheduleWebViewportRedrawFromCpp(ViewportHandle cppViewportHandle); + extern void UpdateStoneApplicationStatusFromCpp(const char* statusUpdateMessage); // C++ methods accessible from JS extern void EMSCRIPTEN_KEEPALIVE CreateWasmApplication(ViewportHandle cppViewportHandle); @@ -30,7 +31,7 @@ namespace OrthancStone { - // default Ovserver to trigger Viewport redraw when something changes in the Viewport + // default Observer to trigger Viewport redraw when something changes in the Viewport class ViewportContentChangedObserver : public OrthancStone::IViewport::IObserver { diff -r 6b3d91857b96 -r 2038d76bf13f Platforms/Wasm/IStoneApplicationToWebApplicationAdapter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/IStoneApplicationToWebApplicationAdapter.h Thu Aug 30 11:36:36 2018 +0200 @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace OrthancStone +{ + class IStoneApplicationToWebApplicationAdapter + { + public: + virtual void HandleMessageFromWeb(std::string& output, const std::string& input) = 0; + virtual void NotifyStatusUpdateFromCppToWeb(const std::string& statusUpdateMessage) = 0; + }; +} \ No newline at end of file diff -r 6b3d91857b96 -r 2038d76bf13f Platforms/Wasm/default-library.js --- a/Platforms/Wasm/default-library.js Tue Aug 28 15:34:20 2018 +0200 +++ b/Platforms/Wasm/default-library.js Thu Aug 30 11:36:36 2018 +0200 @@ -6,6 +6,11 @@ }, CreateWasmViewportFromCpp: function(htmlCanvasId) { return CreateWasmViewport(htmlCanvasId); + }, + // each time the StoneApplication updates its status, it may signal it through this method. i.e, to change the status of a button in the web interface + UpdateStoneApplicationStatusFromCpp: function(statusUpdateMessage) { + var statusUpdateMessage_ = UTF8ToString(statusUpdateMessage); + UpdateWebApplication(statusUpdateMessage_); } }); \ No newline at end of file diff -r 6b3d91857b96 -r 2038d76bf13f Platforms/Wasm/tsconfig-stone.json --- a/Platforms/Wasm/tsconfig-stone.json Tue Aug 28 15:34:20 2018 +0200 +++ b/Platforms/Wasm/tsconfig-stone.json Thu Aug 30 11:36:36 2018 +0200 @@ -1,7 +1,7 @@ { "include" : [ "../../../Platforms/Wasm/stone-framework-loader.ts", - "../../../Platforms/Wasm/wasm-application.ts", + "../../../Platforms/Wasm/wasm-application-runner.ts", "../../../Platforms/Wasm/wasm-viewport.ts" ] } diff -r 6b3d91857b96 -r 2038d76bf13f Platforms/Wasm/wasm-application-runner.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/wasm-application-runner.ts Thu Aug 30 11:36:36 2018 +0200 @@ -0,0 +1,112 @@ +/// +/// + +if (!('WebAssembly' in window)) { + alert('Sorry, your browser does not support WebAssembly :('); +} + +declare var StoneFrameworkModule : Stone.Framework; + +// global functions +var WasmWebService_NotifyError: Function = null; +var WasmWebService_NotifySuccess: Function = null; +var WasmWebService_SetBaseUri: Function = null; +var NotifyUpdateContent: Function = null; +var SetStartupParameter: Function = null; +var CreateWasmApplication: Function = null; +var CreateCppViewport: Function = null; +var ReleaseCppViewport: Function = null; +var StartWasmApplication: Function = null; +var SendMessageToStoneApplication: Function = null; + + +function UpdateContentThread() { + if (NotifyUpdateContent != null) { + NotifyUpdateContent(); + } + + setTimeout(UpdateContentThread, 100); // Update the viewport content every 100ms if need be +} + + +function GetUriParameters() { + var parameters = window.location.search.substr(1); + + if (parameters != null && + parameters != '') { + var result = {}; + var tokens = parameters.split('&'); + + for (var i = 0; i < tokens.length; i++) { + var tmp = tokens[i].split('='); + if (tmp.length == 2) { + result[tmp[0]] = decodeURIComponent(tmp[1]); + } + } + + return result; + } + else { + return {}; + } +} + +// function UpdateWebApplication(statusUpdateMessage: string) { +// console.log(statusUpdateMessage); +// } + +function _InitializeWasmApplication(canvasId: string, orthancBaseUrl: string): void { + + /************************************** */ + CreateWasmApplication(); + WasmWebService_SetBaseUri(orthancBaseUrl); + + + // parse uri and transmit the parameters to the app before initializing it + var parameters = GetUriParameters(); + + for (var key in parameters) { + if (parameters.hasOwnProperty(key)) { + SetStartupParameter(key, parameters[key]); + } + } + + StartWasmApplication(); + /************************************** */ + + UpdateContentThread(); +} + +function InitializeWasmApplication(wasmModuleName: string, orthancBaseUrl: string) { + + Stone.Framework.Configure(wasmModuleName); + + // Wait for the Orthanc Framework to be initialized (this initializes + // the WebAssembly environment) and then, create and initialize the Wasm application + Stone.Framework.Initialize(true, function () { + + console.log("Connecting C++ methods to JS methods"); + + SetStartupParameter = StoneFrameworkModule.cwrap('SetStartupParameter', null, ['string', 'string']); + CreateWasmApplication = StoneFrameworkModule.cwrap('CreateWasmApplication', null, ['number']); + CreateCppViewport = StoneFrameworkModule.cwrap('CreateCppViewport', 'number', []); + ReleaseCppViewport = StoneFrameworkModule.cwrap('ReleaseCppViewport', null, ['number']); + StartWasmApplication = StoneFrameworkModule.cwrap('StartWasmApplication', null, ['number']); + + WasmWebService_NotifySuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifySuccess', null, ['number', 'string', 'array', 'number', 'number']); + WasmWebService_NotifyError = StoneFrameworkModule.cwrap('WasmWebService_NotifyError', null, ['number', 'string', 'number']); + WasmWebService_SetBaseUri = StoneFrameworkModule.cwrap('WasmWebService_SetBaseUri', null, ['string']); + NotifyUpdateContent = StoneFrameworkModule.cwrap('NotifyUpdateContent', null, []); + + SendMessageToStoneApplication = StoneFrameworkModule.cwrap('SendMessageToStoneApplication', 'string', ['string']); + + console.log("Connecting C++ methods to JS methods - done"); + + // Prevent scrolling + document.body.addEventListener('touchmove', function (event) { + event.preventDefault(); + }, false); + + _InitializeWasmApplication("canvas", orthancBaseUrl); + }); +} \ No newline at end of file diff -r 6b3d91857b96 -r 2038d76bf13f Platforms/Wasm/wasm-application.ts --- a/Platforms/Wasm/wasm-application.ts Tue Aug 28 15:34:20 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,122 +0,0 @@ -/// -/// - -if (!('WebAssembly' in window)) { - alert('Sorry, your browser does not support WebAssembly :('); -} - -declare var StoneFrameworkModule : Stone.Framework; - -// global functions -var WasmWebService_NotifyError: Function = null; -var WasmWebService_NotifySuccess: Function = null; -var WasmWebService_SetBaseUri: Function = null; -var NotifyUpdateContent: Function = null; -var SetStartupParameter: Function = null; -var CreateWasmApplication: Function = null; -var CreateCppViewport: Function = null; -var ReleaseCppViewport: Function = null; -var StartWasmApplication: Function = null; - - -function UpdateContentThread() { - if (NotifyUpdateContent != null) { - NotifyUpdateContent(); - } - - setTimeout(UpdateContentThread, 100); // Update the viewport content every 100ms if need be -} - - -function GetUriParameters() { - var parameters = window.location.search.substr(1); - - if (parameters != null && - parameters != '') { - var result = {}; - var tokens = parameters.split('&'); - - for (var i = 0; i < tokens.length; i++) { - var tmp = tokens[i].split('='); - if (tmp.length == 2) { - result[tmp[0]] = decodeURIComponent(tmp[1]); - } - } - - return result; - } - else { - return {}; - } -} - -module Stone { - - export class WasmApplication { - - private viewport_: WasmViewport; - private canvasId_: string; - - private pimpl_: any; // Private pointer to the underlying WebAssembly C++ object - - public constructor(canvasId: string) { - this.canvasId_ = canvasId; - //this.module_ = module; - } - } -} - - -function _InitializeWasmApplication(canvasId: string, orthancBaseUrl: string): void { - - /************************************** */ - CreateWasmApplication(); - WasmWebService_SetBaseUri(orthancBaseUrl); - - - // parse uri and transmit the parameters to the app before initializing it - var parameters = GetUriParameters(); - - for (var key in parameters) { - if (parameters.hasOwnProperty(key)) { - SetStartupParameter(key, parameters[key]); - } - } - - StartWasmApplication(); - /************************************** */ - - UpdateContentThread(); -} - -function InitializeWasmApplication(wasmModuleName: string, orthancBaseUrl: string) { - - Stone.Framework.Configure(wasmModuleName); - - // Wait for the Orthanc Framework to be initialized (this initializes - // the WebAssembly environment) and then, create and initialize the Wasm application - Stone.Framework.Initialize(true, function () { - - console.log("Connecting C++ methods to JS methods"); - - SetStartupParameter = StoneFrameworkModule.cwrap('SetStartupParameter', null, ['string', 'string']); - CreateWasmApplication = StoneFrameworkModule.cwrap('CreateWasmApplication', null, ['number']); - CreateCppViewport = StoneFrameworkModule.cwrap('CreateCppViewport', 'number', []); - ReleaseCppViewport = StoneFrameworkModule.cwrap('ReleaseCppViewport', null, ['number']); - StartWasmApplication = StoneFrameworkModule.cwrap('StartWasmApplication', null, ['number']); - - WasmWebService_NotifySuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifySuccess', null, ['number', 'string', 'array', 'number', 'number']); - WasmWebService_NotifyError = StoneFrameworkModule.cwrap('WasmWebService_NotifyError', null, ['number', 'string', 'number']); - WasmWebService_SetBaseUri = StoneFrameworkModule.cwrap('WasmWebService_SetBaseUri', null, ['string']); - NotifyUpdateContent = StoneFrameworkModule.cwrap('NotifyUpdateContent', null, []); - - console.log("Connecting C++ methods to JS methods - done - 2"); - - // Prevent scrolling - document.body.addEventListener('touchmove', function (event) { - event.preventDefault(); - }, false); - - _InitializeWasmApplication("canvas", orthancBaseUrl); - }); -} \ No newline at end of file diff -r 6b3d91857b96 -r 2038d76bf13f Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Tue Aug 28 15:34:20 2018 +0200 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Thu Aug 30 11:36:36 2018 +0200 @@ -199,6 +199,7 @@ ${ORTHANC_STONE_ROOT}/Platforms/Wasm/Defaults.cpp ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmWebService.cpp ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmViewport.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Wasm/IStoneApplicationToWebApplicationAdapter.h ) endif() diff -r 6b3d91857b96 -r 2038d76bf13f TODO --- a/TODO Tue Aug 28 15:34:20 2018 +0200 +++ b/TODO Thu Aug 30 11:36:36 2018 +0200 @@ -9,14 +9,6 @@ * Documentation * Interface with DICOMweb * LayoutPetCtFusionApplication: fix initial view -* Allow Interactor to create Pan/ZoomMouseTracker in IWorldSceneMouseTracker* CreateMouseTracker - (problem: PanMouseTracker is a IMouseTracker and CreateMouseTracker shall return a IWorldSceneMouseTracker). - WorldSceneWidet shall not create Pan/ZoomMouseTracker when the Interactor does not create one -* Update SimpleViewer sample to have 2 buttons to select the measure tracker - -Bugs ----- -* LineMeasureTracker rendering generates "memory access out of bounds" in WASM --------------------------------- @@ -33,7 +25,6 @@ Optimizations ------------- -* Add cache in "SmartLoader" by returning a "OrthancFrameLayerSource" for a frame that has already been loaded * Tune number of loading threads in LayeredSceneWidget * LayoutWidget: Do not update full background if only 1 widget has changed * LayoutWidget: Threads to refresh each child