From 68ad624a3a3a659d1155b023bfb3186d652f012f Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Tue, 2 Jul 2024 23:44:47 +0800 Subject: [PATCH 01/18] loading default model --- source/engine/viewer/navigation.js | 1 - source/website/index.js | 13 ++++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/source/engine/viewer/navigation.js b/source/engine/viewer/navigation.js index a4f222dc..76ed373a 100644 --- a/source/engine/viewer/navigation.js +++ b/source/engine/viewer/navigation.js @@ -376,7 +376,6 @@ export class Navigation this.clickDetector.Start (this.mouse.GetPosition ()); if (!this.enableCameraMovement) { - console.log('im here heehee OnMouseDowns') this.isMouseDown = true; } } diff --git a/source/website/index.js b/source/website/index.js index 425640ef..42db7af3 100644 --- a/source/website/index.js +++ b/source/website/index.js @@ -7,6 +7,7 @@ import { SetEventHandler, HandleEvent } from './eventhandler.js'; import { PluginType, RegisterPlugin } from './pluginregistry.js'; import { ButtonDialog, ProgressDialog } from './dialog.js'; import { ShowMessageDialog } from './dialogs.js'; +import { ImportSettings } from '../engine/import/importer.js'; import * as Engine from '../engine/main.js'; export { Engine }; @@ -47,7 +48,7 @@ export function RegisterToolbarPlugin (plugin) } export function StartWebsite (externalLibLocation) -{ +{ window.addEventListener ('load', () => { if (window.self !== window.top) { let noEmbeddingDiv = AddDiv (document.body, 'noembed'); @@ -81,6 +82,16 @@ export function StartWebsite (externalLibLocation) fileInput : document.getElementById ('open_file') }); website.Load (); + + if (!website.hashHandler.HasHash()) { + // Load the default FBX model + let defaultModelUrl = 'assets/models/Y_Bot.fbx'; + let importSettings = new ImportSettings(); + importSettings.defaultLineColor = website.settings.defaultLineColor; + importSettings.defaultColor = website.settings.defaultColor; + HandleEvent('model_load_started', 'default'); + website.LoadModelFromUrlList([defaultModelUrl], importSettings); + } }); } From 06ec6c98972a008264ced71bba3ad9918c7f7378 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Tue, 2 Jul 2024 23:45:05 +0800 Subject: [PATCH 02/18] limiting overlapped highlighted meshes --- source/website/highlighttool.js | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/source/website/highlighttool.js b/source/website/highlighttool.js index 4195e19d..a81a38f6 100644 --- a/source/website/highlighttool.js +++ b/source/website/highlighttool.js @@ -16,7 +16,7 @@ export class HighlightTool { this.button = null; this.eventsInitialized = null; this.activeTouches = 0; - + this.overlappingMeshes = new Map(); } InitEvents() { @@ -188,13 +188,44 @@ export class HighlightTool { } } - ApplyHighlight(intersection) { let highlightMesh = this.GenerateHighlightMesh(intersection); + + // Check for overlapping meshes + let overlappingMeshes = this.GetOverlappingMeshes(highlightMesh); + + if (overlappingMeshes.length >= this.maxOverlappingMeshes) { + // Remove the oldest overlapping mesh + let oldestMesh = overlappingMeshes[0]; + this.RemoveHighlight({ point: oldestMesh.position }); + overlappingMeshes.shift(); + } + + // Add the new mesh this.highlightMeshes.push(highlightMesh); this.viewer.AddExtraObject(highlightMesh); + + // Update overlapping meshes + overlappingMeshes.push(highlightMesh); + this.overlappingMeshes.set(highlightMesh.uuid, overlappingMeshes); + + this.viewer.Render(); } + GetOverlappingMeshes(newMesh) { + let overlapping = []; + let newBoundingBox = new THREE.Box3().setFromObject(newMesh); + + for (let mesh of this.highlightMeshes) { + let meshBoundingBox = new THREE.Box3().setFromObject(mesh); + if (newBoundingBox.intersectsBox(meshBoundingBox)) { + overlapping.push(mesh); + } + } + + return overlapping; + } + RemoveHighlight(intersection) { let meshesToRemove = this.highlightMeshes.filter((mesh) => { return this.IsIntersectionWithinBoundingBox(intersection, mesh); @@ -204,6 +235,12 @@ export class HighlightTool { this.viewer.RemoveExtraObject(mesh); this.highlightMeshes = this.highlightMeshes.filter((m) => m !== mesh); this.DisposeHighlightMesh(mesh); + + // Update overlapping meshes + this.overlappingMeshes.delete(mesh.uuid); + for (let [key, value] of this.overlappingMeshes) { + this.overlappingMeshes.set(key, value.filter(m => m !== mesh)); + } }); if (meshesToRemove.length > 0) { @@ -211,6 +248,15 @@ export class HighlightTool { } } + SetMaxOverlappingMeshes(limit) { + if (typeof limit === 'number' && limit > 0) { + this.maxOverlappingMeshes = Math.floor(limit); + console.log(`Max overlapping meshes set to ${this.maxOverlappingMeshes}`); + } else { + console.error('Invalid overlap limit. Please provide a positive number.'); + } + } + DisposeHighlightMesh(mesh) { DisposeThreeObjects(mesh); this.viewer.scene.remove (mesh); From 855378dbba10c62df4c317830d4eb1376e46792e Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Thu, 4 Jul 2024 11:47:36 +0800 Subject: [PATCH 03/18] remove some tools and panels for use case --- source/website/css/core.css | 12 +++---- source/website/navigator.js | 20 ----------- source/website/website.js | 66 ++----------------------------------- 3 files changed, 9 insertions(+), 89 deletions(-) diff --git a/source/website/css/core.css b/source/website/css/core.css index a33cd780..a6673244 100644 --- a/source/website/css/core.css +++ b/source/website/css/core.css @@ -69,10 +69,10 @@ a:hover @media (max-width: 800px) { - -.only_full_width -{ - display: none; -} - + /* Remove or comment out the following block to ensure elements are not hidden on small screens */ + /* + .only_full_width { + display: none; + } + */ } diff --git a/source/website/navigator.js b/source/website/navigator.js index 36a8534e..2b2c8983 100644 --- a/source/website/navigator.js +++ b/source/website/navigator.js @@ -49,11 +49,9 @@ export class Navigator this.tempSelectedMeshId = null; this.filesPanel = new NavigatorFilesPanel (this.panelSet.GetContentDiv ()); - this.materialsPanel = new NavigatorMaterialsPanel (this.panelSet.GetContentDiv ()); this.meshesPanel = new NavigatorMeshesPanel (this.panelSet.GetContentDiv ()); this.panelSet.AddPanel (this.filesPanel); - this.panelSet.AddPanel (this.materialsPanel); this.panelSet.AddPanel (this.meshesPanel); this.panelSet.ShowPanel (this.meshesPanel); } @@ -87,19 +85,6 @@ export class Navigator } }); - this.materialsPanel.Init ({ - onMaterialSelected : (materialIndex) => { - this.SetSelection (new Selection (SelectionType.Material, materialIndex)); - }, - onMeshTemporarySelected : (meshInstanceId) => { - this.tempSelectedMeshId = meshInstanceId; - this.callbacks.onMeshSelectionChanged (); - }, - onMeshSelected : (meshInstanceId) => { - this.SetSelection (new Selection (SelectionType.Mesh, meshInstanceId)); - } - }); - this.meshesPanel.Init ({ onMeshSelected : (meshId) => { this.SetSelection (new Selection (SelectionType.Mesh, meshId)); @@ -149,7 +134,6 @@ export class Navigator } else { this.panelSet.SetPanelIcon (this.filesPanel, 'missing_files'); } - this.materialsPanel.Fill (importResult); this.meshesPanel.Fill (importResult); this.OnSelectionChanged (); } @@ -277,10 +261,6 @@ export class Navigator meshInstanceId = this.selection.meshInstanceId; } } - - let usedByMeshes = this.callbacks.getMeshesForMaterial (materialIndex); - this.materialsPanel.UpdateMeshList (usedByMeshes); - let usedByMaterials = this.callbacks.getMaterialsForMesh (meshInstanceId); this.meshesPanel.UpdateMaterialList (usedByMaterials); } diff --git a/source/website/website.js b/source/website/website.js index 1092f9c0..91386348 100644 --- a/source/website/website.js +++ b/source/website/website.js @@ -134,12 +134,10 @@ class WebsiteLayouter let windowHeight = window.innerHeight; let headerHeight = this.parameters.headerDiv.offsetHeight; - let leftWidth = 0; - let rightWidth = 0; + let leftWidth = this.parameters.leftContainerDiv.style.display === 'none' ? 0 : GetDomElementOuterWidth(this.parameters.leftContainerDiv); + let rightWidth = this.parameters.rightContainerDiv.style.display === 'none' ? 0 : GetDomElementOuterWidth(this.parameters.rightContainerDiv); let safetyMargin = 0; - if (!IsSmallWidth ()) { - leftWidth = GetDomElementOuterWidth (this.parameters.leftContainerDiv); - rightWidth = GetDomElementOuterWidth (this.parameters.rightContainerDiv); + if (!IsSmallWidth()) { safetyMargin = 1; } @@ -680,16 +678,6 @@ export class Website let navigationModeIndex = (this.cameraSettings.navigationMode === NavigationMode.FixedUpVector ? 0 : 1); let projectionModeIndex = (this.cameraSettings.projectionMode === ProjectionMode.Perspective ? 0 : 1); - AddButton (this.toolbar, 'open', Loc ('Open from your device'), [], () => { - this.OpenFileBrowserDialog (); - }); - AddButton (this.toolbar, 'open_url', Loc ('Open from url'), [], () => { - ShowOpenUrlDialog ((urls) => { - if (urls.length > 0) { - this.hashHandler.SetModelFilesToHash (urls); - } - }); - }); AddSeparator (this.toolbar, ['only_on_model']); AddButton (this.toolbar, 'fit', Loc ('Fit model to window'), ['only_on_model'], () => { this.FitModelToWindow (false); @@ -697,41 +685,6 @@ export class Website AddButton (this.toolbar, 'up_y', Loc ('Set Y axis as up vector'), ['only_on_model'], () => { this.viewer.SetUpVector (Direction.Y, true); }); - AddButton (this.toolbar, 'up_z', Loc ('Set Z axis as up vector'), ['only_on_model'], () => { - this.viewer.SetUpVector (Direction.Z, true); - }); - AddButton (this.toolbar, 'flip', Loc ('Flip up vector'), ['only_on_model'], () => { - this.viewer.FlipUpVector (); - }); - AddSeparator (this.toolbar, ['only_full_width', 'only_on_model']); - AddRadioButton (this.toolbar, ['fix_up_on', 'fix_up_off'], [Loc ('Fixed up vector'), Loc ('Free orbit')], navigationModeIndex, ['only_full_width', 'only_on_model'], (buttonIndex) => { - if (buttonIndex === 0) { - this.cameraSettings.navigationMode = NavigationMode.FixedUpVector; - } else if (buttonIndex === 1) { - this.cameraSettings.navigationMode = NavigationMode.FreeOrbit; - } - this.cameraSettings.SaveToCookies (); - this.viewer.SetNavigationMode (this.cameraSettings.navigationMode); - }); - AddSeparator (this.toolbar, ['only_full_width', 'only_on_model']); - AddRadioButton (this.toolbar, ['camera_perspective', 'camera_orthographic'], [Loc ('Perspective camera'), Loc ('Orthographic camera')], projectionModeIndex, ['only_full_width', 'only_on_model'], (buttonIndex) => { - if (buttonIndex === 0) { - this.cameraSettings.projectionMode = ProjectionMode.Perspective; - } else if (buttonIndex === 1) { - this.cameraSettings.projectionMode = ProjectionMode.Orthographic; - } - this.cameraSettings.SaveToCookies (); - this.viewer.SetProjectionMode (this.cameraSettings.projectionMode); - this.sidebar.UpdateControlsVisibility (); - }); - AddSeparator (this.toolbar, ['only_full_width', 'only_on_model']); - let measureToolButton = AddPushButton (this.toolbar, 'measure', Loc ('Measure'), ['only_full_width', 'only_on_model'], (isSelected) => { - HandleEvent ('measure_tool_activated', isSelected ? 'on' : 'off'); - this.navigator.SetSelection (null); - this.measureTool.SetActive (isSelected); - }); - this.measureTool.SetButton (measureToolButton); - let highlightToolButton = AddPushButton(this.toolbar, 'highlight', Loc('Highlight'), ['only_full_width', 'only_on_model'], (isSelected) => { HandleEvent('highlight_tool_activated', isSelected ? 'on' : 'off'); this.navigator.SetSelection(null); @@ -739,19 +692,6 @@ export class Website }); this.highlightTool.SetButton(highlightToolButton); - AddSeparator (this.toolbar, ['only_full_width', 'only_on_model']); - AddButton (this.toolbar, 'download', Loc ('Download'), ['only_full_width', 'only_on_model'], () => { - HandleEvent ('model_downloaded', ''); - let importer = this.modelLoaderUI.GetImporter (); - DownloadModel (importer); - }); - AddButton (this.toolbar, 'export', Loc ('Export'), ['only_full_width', 'only_on_model'], () => { - ShowExportDialog (this.model, this.viewer, { - isMeshVisible : (meshInstanceId) => { - return this.navigator.IsMeshVisible (meshInstanceId); - } - }); - }); AddButton (this.toolbar, 'share', Loc ('Share'), ['only_full_width', 'only_on_model'], () => { ShowSharingDialog (importer.GetFileList (), this.settings, this.viewer); }); From 6c023d86babb910c1558f801b66ad18e1d6b73f5 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Thu, 4 Jul 2024 17:52:41 +0800 Subject: [PATCH 04/18] modifying sharing dialog interface --- source/website/css/sharingdialog.css | 65 ++++++++ source/website/index.js | 1 + source/website/sharingdialog.js | 213 +++++++++++++-------------- source/website/website.js | 2 +- website/index.html | 2 + 5 files changed, 174 insertions(+), 109 deletions(-) create mode 100644 source/website/css/sharingdialog.css diff --git a/source/website/css/sharingdialog.css b/source/website/css/sharingdialog.css new file mode 100644 index 00000000..ff42fd84 --- /dev/null +++ b/source/website/css/sharingdialog.css @@ -0,0 +1,65 @@ +.ov_dialog_form_container { + padding: 20px; + background-color: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.ov_dialog_step { + display: flex; + flex-direction: column; + gap: 15px; +} + +.ov_dialog_title { + font-size: 1.5em; + margin-bottom: 10px; + color: #333; +} + +.ov_dialog_description { + font-size: 1em; + margin-bottom: 20px; + color: #666; +} + +.ov_dialog_label { + font-size: 1em; + margin-bottom: 5px; + color: #333; +} + +.ov_dialog_input { + padding: 10px; + font-size: 1em; + border: 1px solid #ddd; + border-radius: 4px; + width: 100%; + box-sizing: border-box; +} + +.ov_snapshot_preview_container { + margin: 20px 0; + text-align: center; +} + +.ov_snapshot_preview_image { + max-width: 100%; + height: auto; + border: 1px solid #ddd; + border-radius: 4px; +} + +.ov_button { + padding: 10px 15px; + font-size: 1em; + color: #fff; + background-color: #007bff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.ov_button:hover { + background-color: #0056b3; +} diff --git a/source/website/index.js b/source/website/index.js index 42db7af3..c9bf7837 100644 --- a/source/website/index.js +++ b/source/website/index.js @@ -23,6 +23,7 @@ import './css/navigator.css'; import './css/sidebar.css'; import './css/website.css'; import './css/embed.css'; +import './css/sharingdialog.css'; export const UI = { ButtonDialog, diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 7a7ed137..9c5d815f 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -1,138 +1,135 @@ -import { FileSource } from '../engine/io/fileutils.js'; -import { AddDiv, AddDomElement } from '../engine/viewer/domutils.js'; +import { AddDiv, AddDomElement, CreateDomElement } from '../engine/viewer/domutils.js'; import { AddCheckbox } from '../website/utils.js'; -import { CreateUrlBuilder } from '../engine/parameters/parameterlist.js'; import { ShowMessageDialog } from './dialogs.js'; import { ButtonDialog } from './dialog.js'; -import { CopyToClipboard } from './utils.js'; import { HandleEvent } from './eventhandler.js'; import { Loc } from '../engine/core/localization.js'; -export function ShowSharingDialog (fileList, settings, viewer) -{ - function AddCheckboxLine (parentDiv, text, id, onChange) - { - let line = AddDiv (parentDiv, 'ov_dialog_row'); - let checkbox = AddCheckbox (line, id, text, true, () => { - onChange (checkbox.checked); +export function ShowSharingDialog(settings, viewer) { + function AddCheckboxLine(parentDiv, text, id, onChange) { + let line = AddDiv(parentDiv, 'ov_dialog_row'); + let checkbox = AddCheckbox(line, id, text, true, () => { + onChange(checkbox.checked); }); } - function AddCopyableTextInput (parentDiv, getText) - { - let copyText = Loc ('Copy'); - let copiedText = Loc ('Copied'); - let container = AddDiv (parentDiv, 'ov_dialog_copyable_input'); - let input = AddDomElement (container, 'input', null); - input.setAttribute ('type', 'text'); - input.readOnly = true; - let button = AddDiv (container, 'ov_button outline ov_dialog_copyable_input_button', copyText); - button.addEventListener ('click', () => { - CopyToClipboard (getText ()); - button.innerHTML = copiedText; - setTimeout (() => { - button.innerHTML = copyText; - }, 2000); - }); - return input; + function GetImageUrl(viewer, width, height, isTransparent) { + return viewer.GetImageAsDataUrl(width, height, isTransparent); } - function AddSharingLinkTab (parentDiv, modelFiles) - { - function GetSharingLink (modelFiles) - { - let builder = CreateUrlBuilder (); - builder.AddModelUrls (modelFiles); - let hashParameters = builder.GetParameterList (); - return 'https://3dviewer.net/#' + hashParameters; + function UpdatePreview(viewer, previewImage, width, height, isTransparent) { + let url = GetImageUrl(viewer, width, height, isTransparent); + previewImage.src = url; + } + + function AddPainSnapshotSharingTab(parentDiv) { + function SendEmail(recipients, subject, body) { + console.log('Sending email to:', recipients); + // Implement email sending functionality + // This might involve integrating with an email service provider API } - let section = AddDiv (parentDiv, 'ov_dialog_section'); - AddDiv (section, 'ov_dialog_inner_title', Loc ('Sharing Link')); - let sharingLinkInput = AddCopyableTextInput (section, () => { - HandleEvent ('model_shared', 'sharing_link'); - return GetSharingLink (modelFiles); - }); - sharingLinkInput.value = GetSharingLink (modelFiles); - } + function DownloadSnapshotAndInfo(snapshot, info) { + console.log('Downloading snapshot and info') + // Implement download functionality + // This can involve creating a Blob from the snapshot and info, and triggering a download + } - function AddEmbeddingCodeTab (parentDiv, modelFiles, settings, viewer) - { - function GetEmbeddingCode (modelFiles, useCurrentSettings, settings, viewer) - { - let builder = CreateUrlBuilder (); - builder.AddModelUrls (modelFiles); - if (useCurrentSettings) { - builder.AddCamera (viewer.GetCamera ()); - builder.AddProjectionMode (viewer.GetProjectionMode ()); - let environmentSettings = { - environmentMapName : settings.environmentMapName, - backgroundIsEnvMap : settings.backgroundIsEnvMap - }; - builder.AddEnvironmentSettings (environmentSettings); - builder.AddBackgroundColor (settings.backgroundColor); - builder.AddDefaultColor (settings.defaultColor); - builder.AddDefaultLineColor (settings.defaultLineColor); - builder.AddEdgeSettings (settings.edgeSettings); + function CreateMultiStepForm(parentDiv) { + let formContainer = AddDiv(parentDiv, 'ov_dialog_form_container'); + let step1 = AddDiv(formContainer, 'ov_dialog_step'); + AddDiv(step1, 'ov_dialog_title', Loc('Share Snapshot')); + + let description = AddDiv(step1, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); + + // Create email fields + let emailFields = []; + for (let i = 0; i < 3; i++) { + let emailLabel = AddDiv(step1, 'ov_dialog_label', Loc(`Email ${i + 1}`)); + let emailInput = AddDomElement(step1, 'input', `email${i}`); + emailInput.setAttribute('type', 'email'); + emailInput.setAttribute('class', 'ov_dialog_input'); + emailInput.setAttribute('placeholder', Loc('Enter email address')); + emailFields.push(emailInput); } - let hashParameters = builder.GetParameterList (); - - let embeddingCode = ''; - embeddingCode += ''; - embeddingCode += ''; - return embeddingCode; - } - let useCurrentSettings = true; - let section = AddDiv (parentDiv, 'ov_dialog_section'); - section.style.marginTop = '20px'; - AddDiv (section, 'ov_dialog_inner_title', Loc ('Embedding Code')); - let optionsSection = AddDiv (section, 'ov_dialog_section'); - let embeddingCodeInput = AddCopyableTextInput (section, () => { - HandleEvent ('model_shared', 'embedding_code'); - return GetEmbeddingCode (modelFiles, useCurrentSettings, settings, viewer); - }); - AddCheckboxLine (optionsSection, Loc ('Use customized settings'), 'embed_current_settings', (checked) => { - useCurrentSettings = checked; - embeddingCodeInput.value = GetEmbeddingCode (modelFiles, useCurrentSettings, settings, viewer); - }); + // Add snapshot preview + let snapshotPreviewContainer = AddDiv(step1, 'ov_snapshot_preview_container'); + let previewImage = CreateDomElement('img', 'ov_snapshot_preview_image'); + snapshotPreviewContainer.appendChild(previewImage); - embeddingCodeInput.value = GetEmbeddingCode (modelFiles, useCurrentSettings, settings, viewer); - } + // Set initial preview + UpdatePreview(viewer, previewImage, 1920, 1080, false); - if (!fileList.IsOnlyUrlSource ()) { - return ShowMessageDialog ( - Loc ('Sharing Failed'), - Loc ('Sharing works only if you load files by url. Please upload your model files to a web server, open them by url, and try embedding again.'), - null - ); - } + let nextButton = AddDiv(step1, 'ov_button', Loc('Next')); + nextButton.addEventListener('click', () => { + let emails = emailFields.map(input => input.value.trim()).filter(email => email.length > 0); + if (emails.length > 3) { + ShowMessageDialog(Loc('Error'), Loc('You can only send to up to 3 recipients.')); + } else { + step1.style.display = 'none'; + step2.style.display = 'block'; + } + }); + + let step2 = AddDiv(formContainer, 'ov_dialog_step'); + step2.style.display = 'none'; + AddDiv(step2, 'ov_dialog_title', Loc('Additional Options')); + + let sendToSelfCheckbox = AddCheckbox(step2, 'send_to_self', Loc('Send to myself'), false, () => {}); + let downloadCheckbox = AddCheckbox(step2, 'download_snapshot', Loc('Download snapshot and info'), false, () => {}); + + let intensityLabel = AddDiv(step2, 'ov_dialog_label', Loc('Pain Intensity')); + let intensityInput = AddDomElement(step2, 'input', null); + intensityInput.setAttribute('type', 'number'); + intensityInput.setAttribute('min', '1'); + intensityInput.setAttribute('max', '10'); + intensityInput.setAttribute('class', 'ov_dialog_input'); + intensityInput.setAttribute('placeholder', Loc('Enter pain intensity (1-10)')); - let files = fileList.GetFiles (); - let modelFiles = []; - for (let fileIndex = 0; fileIndex < files.length; fileIndex++) { - let file = files[fileIndex]; - if (file.source === FileSource.Url) { - modelFiles.push (file.data); + let durationLabel = AddDiv(step2, 'ov_dialog_label', Loc('Pain Duration')); + let durationInput = AddDomElement(step2, 'input', null); + durationInput.setAttribute('type', 'text'); + durationInput.setAttribute('class', 'ov_dialog_input'); + durationInput.setAttribute('placeholder', Loc('Enter pain duration (e.g., 2 hours, 3 days)')); + + let submitButton = AddDiv(step2, 'ov_button', Loc('Submit')); + submitButton.addEventListener('click', () => { + let snapshot = previewImage.src; // Use the preview image as the snapshot + let info = { + intensity: intensityInput.value, + duration: durationInput.value, + }; + + let emails = emailFields.map(input => input.value.trim()).filter(email => email.length > 0); + if (sendToSelfCheckbox.checked) { + emails.push('self@example.com'); // Replace with actual user's email + } + + SendEmail(emails, 'Pain Snapshot', `Snapshot: ${snapshot}\nInfo: ${JSON.stringify(info)}`); + if (downloadCheckbox.checked) { + DownloadSnapshotAndInfo(snapshot, info); + } + + ShowMessageDialog(Loc('Success'), Loc('Your snapshot and information have been shared.')); + }); } + + CreateMultiStepForm(parentDiv); } - let dialog = new ButtonDialog (); - let contentDiv = dialog.Init (Loc ('Share'), [ + let dialog = new ButtonDialog(); + let contentDiv = dialog.Init(Loc('Share'), [ { - name : Loc ('Close'), - onClick () { - dialog.Close (); + name: Loc('Close'), + onClick() { + dialog.Close(); } } ]); - AddSharingLinkTab (contentDiv, modelFiles); - AddEmbeddingCodeTab (contentDiv, modelFiles, settings, viewer); + AddPainSnapshotSharingTab(contentDiv); - dialog.Open (); + dialog.Open(); return dialog; } diff --git a/source/website/website.js b/source/website/website.js index 91386348..6ce8aa4f 100644 --- a/source/website/website.js +++ b/source/website/website.js @@ -693,7 +693,7 @@ export class Website this.highlightTool.SetButton(highlightToolButton); AddButton (this.toolbar, 'share', Loc ('Share'), ['only_full_width', 'only_on_model'], () => { - ShowSharingDialog (importer.GetFileList (), this.settings, this.viewer); + ShowSharingDialog (this.settings, this.viewer); }); AddSeparator (this.toolbar, ['only_full_width', 'only_on_model']); AddButton (this.toolbar, 'snapshot', Loc ('Create snapshot'), ['only_full_width', 'only_on_model'], () => { diff --git a/website/index.html b/website/index.html index f08b53ea..512deeb0 100644 --- a/website/index.html +++ b/website/index.html @@ -14,6 +14,8 @@ + + From 861bfdb2a836940e8a274fb0ed53c96db2f167f1 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Thu, 4 Jul 2024 21:12:41 +0800 Subject: [PATCH 05/18] Zooming feature in the dialog box --- source/website/sharingdialog.js | 60 +++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 9c5d815f..2d4a0b42 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -6,6 +6,10 @@ import { HandleEvent } from './eventhandler.js'; import { Loc } from '../engine/core/localization.js'; export function ShowSharingDialog(settings, viewer) { + const snapshotWidth = 1920; + const snapshotHeight = 1080; + const initialZoomLevel = settings.snapshotZoomLevel || 1.5; // Default zoom level + function AddCheckboxLine(parentDiv, text, id, onChange) { let line = AddDiv(parentDiv, 'ov_dialog_row'); let checkbox = AddCheckbox(line, id, text, true, () => { @@ -13,13 +17,43 @@ export function ShowSharingDialog(settings, viewer) { }); } - function GetImageUrl(viewer, width, height, isTransparent) { - return viewer.GetImageAsDataUrl(width, height, isTransparent); + function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel) { + const camera = viewer.navigation.GetCamera(); + const originalEyePosition = { ...camera.eye }; + const originalCenterPosition = { ...camera.center }; + + // Calculate the direction vector from center to eye + const direction = { + x: camera.eye.x - camera.center.x, + y: camera.eye.y - camera.center.y, + z: camera.eye.z - camera.center.z + }; + + // Scale the direction vector by the zoom level + direction.x *= zoomLevel; + direction.y *= zoomLevel; + direction.z *= zoomLevel; + + // Adjust the camera eye position + camera.eye.x = camera.center.x + direction.x; + camera.eye.y = camera.center.y + direction.y; + camera.eye.z = camera.center.z + direction.z; + viewer.navigation.MoveCamera(camera, 0); // Ensure the viewer updates the camera position + + // Capture the image as a Data URL + const imageDataUrl = viewer.GetImageAsDataUrl(width, height, isTransparent); + + // Restore the original camera position + camera.eye = originalEyePosition; + camera.center = originalCenterPosition; + viewer.navigation.MoveCamera(camera, 0); + + return imageDataUrl; } - function UpdatePreview(viewer, previewImage, width, height, isTransparent) { - let url = GetImageUrl(viewer, width, height, isTransparent); - previewImage.src = url; + function UpdatePreview(viewer, previewImage, width, height, isTransparent, zoomLevel) { + let imageUrl = CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel); + previewImage.src = imageUrl; } function AddPainSnapshotSharingTab(parentDiv) { @@ -30,7 +64,7 @@ export function ShowSharingDialog(settings, viewer) { } function DownloadSnapshotAndInfo(snapshot, info) { - console.log('Downloading snapshot and info') + console.log('Downloading snapshot and info'); // Implement download functionality // This can involve creating a Blob from the snapshot and info, and triggering a download } @@ -58,8 +92,20 @@ export function ShowSharingDialog(settings, viewer) { let previewImage = CreateDomElement('img', 'ov_snapshot_preview_image'); snapshotPreviewContainer.appendChild(previewImage); + // Add zoom slider + let zoomSliderLabel = AddDiv(step1, 'ov_dialog_label', Loc('Zoom Level')); + let zoomSlider = AddDomElement(step1, 'input', 'zoomSlider'); + zoomSlider.setAttribute('type', 'range'); + zoomSlider.setAttribute('min', '0.1'); + zoomSlider.setAttribute('max', '3'); + zoomSlider.setAttribute('step', '0.1'); + zoomSlider.setAttribute('value', initialZoomLevel); + zoomSlider.addEventListener('input', () => { + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, zoomSlider.value); + }); + // Set initial preview - UpdatePreview(viewer, previewImage, 1920, 1080, false); + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, initialZoomLevel); let nextButton = AddDiv(step1, 'ov_button', Loc('Next')); nextButton.addEventListener('click', () => { From d3483f1f73aa031a73ffdb4d9e7e976ecdff54cb Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Fri, 5 Jul 2024 00:39:36 +0800 Subject: [PATCH 06/18] rotate fix before and after dialog box --- source/website/sharingdialog.js | 70 ++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 2d4a0b42..6cc60b57 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -9,6 +9,16 @@ export function ShowSharingDialog(settings, viewer) { const snapshotWidth = 1920; const snapshotHeight = 1080; const initialZoomLevel = settings.snapshotZoomLevel || 1.5; // Default zoom level + let isPanning = false; + let isOrbiting = false; + let startMousePosition = { x: 0, y: 0 }; + let previewImage; + + // Log the camera object before opening the dialog + const camera = viewer.navigation.GetCamera(); + const originalRotate = camera.eye.Rotate; + console.log('Camera before opening dialog:', camera); + console.log('Rotate method before opening dialog:', camera.eye.Rotate); function AddCheckboxLine(parentDiv, text, id, onChange) { let line = AddDiv(parentDiv, 'ov_dialog_row'); @@ -56,6 +66,44 @@ export function ShowSharingDialog(settings, viewer) { previewImage.src = imageUrl; } + function HandleMouseDown(event) { + startMousePosition = { x: event.clientX, y: event.clientY }; + if (event.button === 0) { // Left button + isOrbiting = true; + } else if (event.button === 1) { // Middle button + isPanning = true; + } + window.addEventListener('mousemove', HandleMouseMove); + window.addEventListener('mouseup', HandleMouseUp); + } + + function HandleMouseMove(event) { + if (!isPanning && !isOrbiting) return; + + const currentMousePosition = { x: event.clientX, y: event.clientY }; + const deltaX = currentMousePosition.x - startMousePosition.x; + const deltaY = currentMousePosition.y - startMousePosition.y; + + if (isOrbiting) { + const orbitRatio = 0.5; + viewer.navigation.Orbit(deltaX * orbitRatio, deltaY * orbitRatio); + } else if (isPanning) { + const panRatio = 0.001; + viewer.navigation.Pan(deltaX * panRatio, deltaY * panRatio); + } + + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, initialZoomLevel); + + startMousePosition = currentMousePosition; + } + + function HandleMouseUp(event) { + isPanning = false; + isOrbiting = false; + window.removeEventListener('mousemove', HandleMouseMove); + window.removeEventListener('mouseup', HandleMouseUp); + } + function AddPainSnapshotSharingTab(parentDiv) { function SendEmail(recipients, subject, body) { console.log('Sending email to:', recipients); @@ -89,7 +137,7 @@ export function ShowSharingDialog(settings, viewer) { // Add snapshot preview let snapshotPreviewContainer = AddDiv(step1, 'ov_snapshot_preview_container'); - let previewImage = CreateDomElement('img', 'ov_snapshot_preview_image'); + previewImage = CreateDomElement('img', 'ov_snapshot_preview_image'); snapshotPreviewContainer.appendChild(previewImage); // Add zoom slider @@ -107,6 +155,9 @@ export function ShowSharingDialog(settings, viewer) { // Set initial preview UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, initialZoomLevel); + // Add mouse event listeners for panning and orbiting + previewImage.addEventListener('mousedown', HandleMouseDown); + let nextButton = AddDiv(step1, 'ov_button', Loc('Next')); nextButton.addEventListener('click', () => { let emails = emailFields.map(input => input.value.trim()).filter(email => email.length > 0); @@ -176,6 +227,23 @@ export function ShowSharingDialog(settings, viewer) { AddPainSnapshotSharingTab(contentDiv); + const originalClose = dialog.Close.bind(dialog); + dialog.Close = function() { + previewImage.removeEventListener('mousedown', HandleMouseDown); + window.removeEventListener('mousemove', HandleMouseMove); + window.removeEventListener('mouseup', HandleMouseUp); + + // Reassign the original Rotate method after closing the dialog + camera.eye.Rotate = originalRotate; + + originalClose(); + + // Log the camera object after closing the dialog + console.log('Camera after closing dialog:', viewer.navigation.GetCamera()); + console.log('Rotate method after closing dialog:', camera.eye.Rotate); + }; + dialog.Open(); + return dialog; } From db610c59165c4d492077ab5508ad82029ad47dbf Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Fri, 5 Jul 2024 01:46:33 +0800 Subject: [PATCH 07/18] bug of offset not working after CaptureSnapshot used --- source/website/sharingdialog.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 6cc60b57..b3db817d 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -29,8 +29,10 @@ export function ShowSharingDialog(settings, viewer) { function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel) { const camera = viewer.navigation.GetCamera(); - const originalEyePosition = { ...camera.eye }; - const originalCenterPosition = { ...camera.center }; + + // Store original positions directly + const originalEyePosition = { x: camera.eye.x, y: camera.eye.y, z: camera.eye.z }; + const originalCenterPosition = { x: camera.center.x, y: camera.center.y, z: camera.center.z }; // Calculate the direction vector from center to eye const direction = { @@ -53,15 +55,22 @@ export function ShowSharingDialog(settings, viewer) { // Capture the image as a Data URL const imageDataUrl = viewer.GetImageAsDataUrl(width, height, isTransparent); - // Restore the original camera position - camera.eye = originalEyePosition; - camera.center = originalCenterPosition; + // Restore the original camera position directly + camera.eye.x = originalEyePosition.x; + camera.eye.y = originalEyePosition.y; + camera.eye.z = originalEyePosition.z; + camera.center.x = originalCenterPosition.x; + camera.center.y = originalCenterPosition.y; + camera.center.z = originalCenterPosition.z; viewer.navigation.MoveCamera(camera, 0); return imageDataUrl; } + + function UpdatePreview(viewer, previewImage, width, height, isTransparent, zoomLevel) { + console.log('Updating preview'); let imageUrl = CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel); previewImage.src = imageUrl; } From 20e24c0a42687de0b83112900b36916a752b5c3f Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Sat, 6 Jul 2024 00:45:13 +0800 Subject: [PATCH 08/18] rotate and orbit in preview --- source/website/sharingdialog.js | 170 +++++++++++++++++++------------- 1 file changed, 100 insertions(+), 70 deletions(-) diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index b3db817d..7fe42ea9 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -13,104 +13,133 @@ export function ShowSharingDialog(settings, viewer) { let isOrbiting = false; let startMousePosition = { x: 0, y: 0 }; let previewImage; - + let panOffset = { x: 0, y: 0 }; + let orbitOffset = { x: 0, y: 0 }; + let zoomSlider; + // Log the camera object before opening the dialog const camera = viewer.navigation.GetCamera(); const originalRotate = camera.eye.Rotate; console.log('Camera before opening dialog:', camera); console.log('Rotate method before opening dialog:', camera.eye.Rotate); - function AddCheckboxLine(parentDiv, text, id, onChange) { - let line = AddDiv(parentDiv, 'ov_dialog_row'); - let checkbox = AddCheckbox(line, id, text, true, () => { - onChange(checkbox.checked); - }); - } - - function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel) { + function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset) { const camera = viewer.navigation.GetCamera(); - // Store original positions directly - const originalEyePosition = { x: camera.eye.x, y: camera.eye.y, z: camera.eye.z }; - const originalCenterPosition = { x: camera.center.x, y: camera.center.y, z: camera.center.z }; - - // Calculate the direction vector from center to eye + // Store original camera state + const originalCamera = { + eye: { x: camera.eye.x, y: camera.eye.y, z: camera.eye.z }, + center: { x: camera.center.x, y: camera.center.y, z: camera.center.z }, + up: { x: camera.up.x, y: camera.up.y, z: camera.up.z } + }; + + // Apply zoom const direction = { x: camera.eye.x - camera.center.x, y: camera.eye.y - camera.center.y, z: camera.eye.z - camera.center.z }; - // Scale the direction vector by the zoom level - direction.x *= zoomLevel; - direction.y *= zoomLevel; - direction.z *= zoomLevel; + const distance = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z); + const zoomedDistance = distance * zoomLevel; + const zoomFactor = zoomedDistance / distance; - // Adjust the camera eye position - camera.eye.x = camera.center.x + direction.x; - camera.eye.y = camera.center.y + direction.y; - camera.eye.z = camera.center.z + direction.z; - viewer.navigation.MoveCamera(camera, 0); // Ensure the viewer updates the camera position + const zoomedEye = { + x: camera.center.x + direction.x * zoomFactor, + y: camera.center.y + direction.y * zoomFactor, + z: camera.center.z + direction.z * zoomFactor + }; + + // Apply pan + const panScale = distance * 0.001; // Adjust this value as needed + const pannedCenter = { + x: camera.center.x + camera.up.x * panOffset.y * panScale, + y: camera.center.y + camera.up.y * panOffset.y * panScale, + z: camera.center.z + camera.up.z * panOffset.y * panScale + }; + const pannedEye = { + x: zoomedEye.x + camera.up.x * panOffset.y * panScale, + y: zoomedEye.y + camera.up.y * panOffset.y * panScale, + z: zoomedEye.z + camera.up.z * panOffset.y * panScale + }; + + // Set temporary camera for snapshot + camera.eye.x = pannedEye.x; + camera.eye.y = pannedEye.y; + camera.eye.z = pannedEye.z; + camera.center.x = pannedCenter.x; + camera.center.y = pannedCenter.y; + camera.center.z = pannedCenter.z; + viewer.navigation.MoveCamera(camera, 0); + + // Apply orbit + viewer.navigation.Orbit(orbitOffset.x, orbitOffset.y); - // Capture the image as a Data URL + // Capture the image const imageDataUrl = viewer.GetImageAsDataUrl(width, height, isTransparent); - // Restore the original camera position directly - camera.eye.x = originalEyePosition.x; - camera.eye.y = originalEyePosition.y; - camera.eye.z = originalEyePosition.z; - camera.center.x = originalCenterPosition.x; - camera.center.y = originalCenterPosition.y; - camera.center.z = originalCenterPosition.z; + // Restore original camera state + camera.eye.x = originalCamera.eye.x; + camera.eye.y = originalCamera.eye.y; + camera.eye.z = originalCamera.eye.z; + camera.center.x = originalCamera.center.x; + camera.center.y = originalCamera.center.y; + camera.center.z = originalCamera.center.z; + camera.up.x = originalCamera.up.x; + camera.up.y = originalCamera.up.y; + camera.up.z = originalCamera.up.z; viewer.navigation.MoveCamera(camera, 0); - + return imageDataUrl; } - - - function UpdatePreview(viewer, previewImage, width, height, isTransparent, zoomLevel) { + function UpdatePreview(viewer, previewImage, width, height, isTransparent, zoomLevel, panOffset, orbitOffset) { console.log('Updating preview'); - let imageUrl = CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel); + let imageUrl = CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset); previewImage.src = imageUrl; } - function HandleMouseDown(event) { - startMousePosition = { x: event.clientX, y: event.clientY }; - if (event.button === 0) { // Left button - isOrbiting = true; - } else if (event.button === 1) { // Middle button - isPanning = true; - } - window.addEventListener('mousemove', HandleMouseMove); - window.addEventListener('mouseup', HandleMouseUp); - } - - function HandleMouseMove(event) { + function HandlePreviewMouseMove(event) { if (!isPanning && !isOrbiting) return; - + const currentMousePosition = { x: event.clientX, y: event.clientY }; const deltaX = currentMousePosition.x - startMousePosition.x; const deltaY = currentMousePosition.y - startMousePosition.y; - + if (isOrbiting) { - const orbitRatio = 0.5; - viewer.navigation.Orbit(deltaX * orbitRatio, deltaY * orbitRatio); + const orbitRatio = 0.1; // Adjust this value as needed + orbitOffset.x += deltaX * orbitRatio; + orbitOffset.y += deltaY * orbitRatio; } else if (isPanning) { - const panRatio = 0.001; - viewer.navigation.Pan(deltaX * panRatio, deltaY * panRatio); + const panRatio = 0.1; // Adjust this value as needed + panOffset.y -= deltaY * panRatio; // Only vertical panning for now } - - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, initialZoomLevel); - + + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, zoomSlider.value, panOffset, orbitOffset); + startMousePosition = currentMousePosition; + event.preventDefault(); } - function HandleMouseUp(event) { + function HandlePreviewMouseUp(event) { + console.log('HandlePreviewMouseUp'); isPanning = false; isOrbiting = false; - window.removeEventListener('mousemove', HandleMouseMove); - window.removeEventListener('mouseup', HandleMouseUp); + document.removeEventListener('mousemove', HandlePreviewMouseMove, true); + document.removeEventListener('mouseup', HandlePreviewMouseUp, true); + event.preventDefault(); + } + + function HandlePreviewMouseDown(event) { + startMousePosition = { x: event.clientX, y: event.clientY }; + if (event.button === 0) { // Left button + isOrbiting = true; + } else if (event.button === 1 || event.button === 2) { // Middle button or right button + isPanning = true; + } + document.addEventListener('mousemove', HandlePreviewMouseMove, true); + document.addEventListener('mouseup', HandlePreviewMouseUp, true); + event.preventDefault(); } function AddPainSnapshotSharingTab(parentDiv) { @@ -129,6 +158,7 @@ export function ShowSharingDialog(settings, viewer) { function CreateMultiStepForm(parentDiv) { let formContainer = AddDiv(parentDiv, 'ov_dialog_form_container'); let step1 = AddDiv(formContainer, 'ov_dialog_step'); + AddDiv(step1, 'ov_dialog_title', Loc('Share Snapshot')); let description = AddDiv(step1, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); @@ -151,21 +181,22 @@ export function ShowSharingDialog(settings, viewer) { // Add zoom slider let zoomSliderLabel = AddDiv(step1, 'ov_dialog_label', Loc('Zoom Level')); - let zoomSlider = AddDomElement(step1, 'input', 'zoomSlider'); + zoomSlider = AddDomElement(step1, 'input', 'zoomSlider'); zoomSlider.setAttribute('type', 'range'); zoomSlider.setAttribute('min', '0.1'); zoomSlider.setAttribute('max', '3'); zoomSlider.setAttribute('step', '0.1'); zoomSlider.setAttribute('value', initialZoomLevel); zoomSlider.addEventListener('input', () => { - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, zoomSlider.value); + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, zoomSlider.value, panOffset, orbitOffset); }); - // Set initial preview - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, initialZoomLevel); - // Add mouse event listeners for panning and orbiting - previewImage.addEventListener('mousedown', HandleMouseDown); + previewImage.addEventListener('mousedown', HandlePreviewMouseDown, true); + + // Set initial preview + console.log('Initial preview'); + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, zoomSlider.value, panOffset, orbitOffset); let nextButton = AddDiv(step1, 'ov_button', Loc('Next')); nextButton.addEventListener('click', () => { @@ -238,10 +269,9 @@ export function ShowSharingDialog(settings, viewer) { const originalClose = dialog.Close.bind(dialog); dialog.Close = function() { - previewImage.removeEventListener('mousedown', HandleMouseDown); - window.removeEventListener('mousemove', HandleMouseMove); - window.removeEventListener('mouseup', HandleMouseUp); - + previewImage.removeEventListener('mousedown', HandlePreviewMouseDown, true); + document.removeEventListener('mousemove', HandlePreviewMouseMove, true); + document.removeEventListener('mouseup', HandlePreviewMouseUp, true); // Reassign the original Rotate method after closing the dialog camera.eye.Rotate = originalRotate; From 2f46236709c1793ab4cd4c110c44c93c051a89e1 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Sat, 6 Jul 2024 01:13:12 +0800 Subject: [PATCH 09/18] Removed zoom slider to replace with mouse wheel. Added panning to sharing preview --- source/website/css/sharingdialog.css | 10 +++ source/website/sharingdialog.js | 95 +++++++++++++--------------- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/source/website/css/sharingdialog.css b/source/website/css/sharingdialog.css index ff42fd84..9adfe127 100644 --- a/source/website/css/sharingdialog.css +++ b/source/website/css/sharingdialog.css @@ -63,3 +63,13 @@ .ov_button:hover { background-color: #0056b3; } + +.ov_dialog_form_container { + max-height: 80vh; /* Ensure the form doesn't extend beyond 80% of the viewport height */ + overflow-y: auto; /* Enable scrolling if content overflows */ +} + +.ov_dialog_step { + position: relative; /* Ensure steps are correctly positioned */ + overflow: hidden; /* Prevent unintended scrolling */ +} diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 7fe42ea9..cefe650c 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -15,17 +15,14 @@ export function ShowSharingDialog(settings, viewer) { let previewImage; let panOffset = { x: 0, y: 0 }; let orbitOffset = { x: 0, y: 0 }; - let zoomSlider; + let currentZoomLevel = initialZoomLevel; - // Log the camera object before opening the dialog const camera = viewer.navigation.GetCamera(); const originalRotate = camera.eye.Rotate; - console.log('Camera before opening dialog:', camera); - console.log('Rotate method before opening dialog:', camera.eye.Rotate); function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset) { const camera = viewer.navigation.GetCamera(); - + // Store original camera state const originalCamera = { eye: { x: camera.eye.x, y: camera.eye.y, z: camera.eye.z }, @@ -39,11 +36,11 @@ export function ShowSharingDialog(settings, viewer) { y: camera.eye.y - camera.center.y, z: camera.eye.z - camera.center.z }; - + const distance = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z); const zoomedDistance = distance * zoomLevel; const zoomFactor = zoomedDistance / distance; - + const zoomedEye = { x: camera.center.x + direction.x * zoomFactor, y: camera.center.y + direction.y * zoomFactor, @@ -51,16 +48,28 @@ export function ShowSharingDialog(settings, viewer) { }; // Apply pan - const panScale = distance * 0.001; // Adjust this value as needed + const panScale = distance * 0.005; // Adjust this value as needed + const right = { + x: direction.y * camera.up.z - direction.z * camera.up.y, + y: direction.z * camera.up.x - direction.x * camera.up.z, + z: direction.x * camera.up.y - direction.y * camera.up.x + }; + const rightLength = Math.sqrt(right.x * right.x + right.y * right.y + right.z * right.z); + const normalizedRight = { + x: right.x / rightLength, + y: right.y / rightLength, + z: right.z / rightLength + }; + const pannedCenter = { - x: camera.center.x + camera.up.x * panOffset.y * panScale, - y: camera.center.y + camera.up.y * panOffset.y * panScale, - z: camera.center.z + camera.up.z * panOffset.y * panScale + x: camera.center.x + normalizedRight.x * panOffset.x * panScale + camera.up.x * panOffset.y * panScale, + y: camera.center.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panOffset.y * panScale, + z: camera.center.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panOffset.y * panScale }; const pannedEye = { - x: zoomedEye.x + camera.up.x * panOffset.y * panScale, - y: zoomedEye.y + camera.up.y * panOffset.y * panScale, - z: zoomedEye.z + camera.up.z * panOffset.y * panScale + x: zoomedEye.x + normalizedRight.x * panOffset.x * panScale + camera.up.x * panOffset.y * panScale, + y: zoomedEye.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panOffset.y * panScale, + z: zoomedEye.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panOffset.y * panScale }; // Set temporary camera for snapshot @@ -71,13 +80,13 @@ export function ShowSharingDialog(settings, viewer) { camera.center.y = pannedCenter.y; camera.center.z = pannedCenter.z; viewer.navigation.MoveCamera(camera, 0); - + // Apply orbit viewer.navigation.Orbit(orbitOffset.x, orbitOffset.y); - + // Capture the image const imageDataUrl = viewer.GetImageAsDataUrl(width, height, isTransparent); - + // Restore original camera state camera.eye.x = originalCamera.eye.x; camera.eye.y = originalCamera.eye.y; @@ -92,9 +101,9 @@ export function ShowSharingDialog(settings, viewer) { return imageDataUrl; } + function UpdatePreview(viewer, previewImage, width, height, isTransparent, zoomLevel, panOffset, orbitOffset) { - console.log('Updating preview'); let imageUrl = CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset); previewImage.src = imageUrl; } @@ -111,18 +120,18 @@ export function ShowSharingDialog(settings, viewer) { orbitOffset.x += deltaX * orbitRatio; orbitOffset.y += deltaY * orbitRatio; } else if (isPanning) { - const panRatio = 0.1; // Adjust this value as needed - panOffset.y -= deltaY * panRatio; // Only vertical panning for now + const panRatio = 0.075; // Adjust this value as needed + panOffset.x -= deltaX * panRatio; // Horizontal panning + panOffset.y -= deltaY * panRatio; // Vertical panning } - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, zoomSlider.value, panOffset, orbitOffset); + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, currentZoomLevel, panOffset, orbitOffset); startMousePosition = currentMousePosition; event.preventDefault(); } function HandlePreviewMouseUp(event) { - console.log('HandlePreviewMouseUp'); isPanning = false; isOrbiting = false; document.removeEventListener('mousemove', HandlePreviewMouseMove, true); @@ -142,17 +151,21 @@ export function ShowSharingDialog(settings, viewer) { event.preventDefault(); } + function HandleMouseWheel(event) { + const zoomSpeed = 0.001; + currentZoomLevel += event.deltaY * zoomSpeed; + currentZoomLevel = Math.min(Math.max(currentZoomLevel, 0.1), 3); + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, currentZoomLevel, panOffset, orbitOffset); + event.preventDefault(); + } + function AddPainSnapshotSharingTab(parentDiv) { function SendEmail(recipients, subject, body) { console.log('Sending email to:', recipients); - // Implement email sending functionality - // This might involve integrating with an email service provider API } function DownloadSnapshotAndInfo(snapshot, info) { console.log('Downloading snapshot and info'); - // Implement download functionality - // This can involve creating a Blob from the snapshot and info, and triggering a download } function CreateMultiStepForm(parentDiv) { @@ -163,7 +176,6 @@ export function ShowSharingDialog(settings, viewer) { let description = AddDiv(step1, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); - // Create email fields let emailFields = []; for (let i = 0; i < 3; i++) { let emailLabel = AddDiv(step1, 'ov_dialog_label', Loc(`Email ${i + 1}`)); @@ -174,29 +186,16 @@ export function ShowSharingDialog(settings, viewer) { emailFields.push(emailInput); } - // Add snapshot preview let snapshotPreviewContainer = AddDiv(step1, 'ov_snapshot_preview_container'); previewImage = CreateDomElement('img', 'ov_snapshot_preview_image'); snapshotPreviewContainer.appendChild(previewImage); - // Add zoom slider - let zoomSliderLabel = AddDiv(step1, 'ov_dialog_label', Loc('Zoom Level')); - zoomSlider = AddDomElement(step1, 'input', 'zoomSlider'); - zoomSlider.setAttribute('type', 'range'); - zoomSlider.setAttribute('min', '0.1'); - zoomSlider.setAttribute('max', '3'); - zoomSlider.setAttribute('step', '0.1'); - zoomSlider.setAttribute('value', initialZoomLevel); - zoomSlider.addEventListener('input', () => { - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, zoomSlider.value, panOffset, orbitOffset); - }); + // Remove zoom slider and add mouse wheel event listener for zoom + previewImage.addEventListener('wheel', HandleMouseWheel, true); - // Add mouse event listeners for panning and orbiting previewImage.addEventListener('mousedown', HandlePreviewMouseDown, true); - // Set initial preview - console.log('Initial preview'); - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, zoomSlider.value, panOffset, orbitOffset); + UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, currentZoomLevel, panOffset, orbitOffset); let nextButton = AddDiv(step1, 'ov_button', Loc('Next')); nextButton.addEventListener('click', () => { @@ -232,7 +231,7 @@ export function ShowSharingDialog(settings, viewer) { let submitButton = AddDiv(step2, 'ov_button', Loc('Submit')); submitButton.addEventListener('click', () => { - let snapshot = previewImage.src; // Use the preview image as the snapshot + let snapshot = previewImage.src; let info = { intensity: intensityInput.value, duration: durationInput.value, @@ -240,7 +239,7 @@ export function ShowSharingDialog(settings, viewer) { let emails = emailFields.map(input => input.value.trim()).filter(email => email.length > 0); if (sendToSelfCheckbox.checked) { - emails.push('self@example.com'); // Replace with actual user's email + emails.push('self@example.com'); } SendEmail(emails, 'Pain Snapshot', `Snapshot: ${snapshot}\nInfo: ${JSON.stringify(info)}`); @@ -272,14 +271,10 @@ export function ShowSharingDialog(settings, viewer) { previewImage.removeEventListener('mousedown', HandlePreviewMouseDown, true); document.removeEventListener('mousemove', HandlePreviewMouseMove, true); document.removeEventListener('mouseup', HandlePreviewMouseUp, true); - // Reassign the original Rotate method after closing the dialog + previewImage.removeEventListener('wheel', HandleMouseWheel, true); camera.eye.Rotate = originalRotate; originalClose(); - - // Log the camera object after closing the dialog - console.log('Camera after closing dialog:', viewer.navigation.GetCamera()); - console.log('Rotate method after closing dialog:', camera.eye.Rotate); }; dialog.Open(); From 3f0d80182c43f2bc545640a78a7f38978157a193 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Sat, 6 Jul 2024 02:16:35 +0800 Subject: [PATCH 10/18] refactored --- source/website/sharingdialog.js | 419 +++++++++++++++++--------------- 1 file changed, 219 insertions(+), 200 deletions(-) diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index cefe650c..ae3acd33 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -6,9 +6,16 @@ import { HandleEvent } from './eventhandler.js'; import { Loc } from '../engine/core/localization.js'; export function ShowSharingDialog(settings, viewer) { + const SnapshotManager = createSnapshotManager(viewer, settings); + const DialogManager = createDialogManager(SnapshotManager); + DialogManager.showDialog(); +} + +function createSnapshotManager(viewer, settings) { const snapshotWidth = 1920; const snapshotHeight = 1080; - const initialZoomLevel = settings.snapshotZoomLevel || 1.5; // Default zoom level + const initialZoomLevel = settings.snapshotZoomLevel || 1.5; + let isPanning = false; let isOrbiting = false; let startMousePosition = { x: 0, y: 0 }; @@ -20,95 +27,16 @@ export function ShowSharingDialog(settings, viewer) { const camera = viewer.navigation.GetCamera(); const originalRotate = camera.eye.Rotate; - function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset) { - const camera = viewer.navigation.GetCamera(); - - // Store original camera state - const originalCamera = { - eye: { x: camera.eye.x, y: camera.eye.y, z: camera.eye.z }, - center: { x: camera.center.x, y: camera.center.y, z: camera.center.z }, - up: { x: camera.up.x, y: camera.up.y, z: camera.up.z } - }; - - // Apply zoom - const direction = { - x: camera.eye.x - camera.center.x, - y: camera.eye.y - camera.center.y, - z: camera.eye.z - camera.center.z - }; - - const distance = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z); - const zoomedDistance = distance * zoomLevel; - const zoomFactor = zoomedDistance / distance; - - const zoomedEye = { - x: camera.center.x + direction.x * zoomFactor, - y: camera.center.y + direction.y * zoomFactor, - z: camera.center.z + direction.z * zoomFactor - }; - - // Apply pan - const panScale = distance * 0.005; // Adjust this value as needed - const right = { - x: direction.y * camera.up.z - direction.z * camera.up.y, - y: direction.z * camera.up.x - direction.x * camera.up.z, - z: direction.x * camera.up.y - direction.y * camera.up.x - }; - const rightLength = Math.sqrt(right.x * right.x + right.y * right.y + right.z * right.z); - const normalizedRight = { - x: right.x / rightLength, - y: right.y / rightLength, - z: right.z / rightLength - }; - - const pannedCenter = { - x: camera.center.x + normalizedRight.x * panOffset.x * panScale + camera.up.x * panOffset.y * panScale, - y: camera.center.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panOffset.y * panScale, - z: camera.center.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panOffset.y * panScale - }; - const pannedEye = { - x: zoomedEye.x + normalizedRight.x * panOffset.x * panScale + camera.up.x * panOffset.y * panScale, - y: zoomedEye.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panOffset.y * panScale, - z: zoomedEye.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panOffset.y * panScale - }; - - // Set temporary camera for snapshot - camera.eye.x = pannedEye.x; - camera.eye.y = pannedEye.y; - camera.eye.z = pannedEye.z; - camera.center.x = pannedCenter.x; - camera.center.y = pannedCenter.y; - camera.center.z = pannedCenter.z; - viewer.navigation.MoveCamera(camera, 0); - - // Apply orbit - viewer.navigation.Orbit(orbitOffset.x, orbitOffset.y); - - // Capture the image - const imageDataUrl = viewer.GetImageAsDataUrl(width, height, isTransparent); - - // Restore original camera state - camera.eye.x = originalCamera.eye.x; - camera.eye.y = originalCamera.eye.y; - camera.eye.z = originalCamera.eye.z; - camera.center.x = originalCamera.center.x; - camera.center.y = originalCamera.center.y; - camera.center.z = originalCamera.center.z; - camera.up.x = originalCamera.up.x; - camera.up.y = originalCamera.up.y; - camera.up.z = originalCamera.up.z; - viewer.navigation.MoveCamera(camera, 0); - - return imageDataUrl; + function captureSnapshot(isTransparent) { + return CaptureSnapshot(viewer, snapshotWidth, snapshotHeight, isTransparent, currentZoomLevel, panOffset, orbitOffset); } - - function UpdatePreview(viewer, previewImage, width, height, isTransparent, zoomLevel, panOffset, orbitOffset) { - let imageUrl = CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset); + function updatePreview() { + let imageUrl = captureSnapshot(false); previewImage.src = imageUrl; } - function HandlePreviewMouseMove(event) { + function handlePreviewMouseMove(event) { if (!isPanning && !isOrbiting) return; const currentMousePosition = { x: event.clientX, y: event.clientY }; @@ -116,168 +44,259 @@ export function ShowSharingDialog(settings, viewer) { const deltaY = currentMousePosition.y - startMousePosition.y; if (isOrbiting) { - const orbitRatio = 0.1; // Adjust this value as needed + const orbitRatio = 0.1; orbitOffset.x += deltaX * orbitRatio; orbitOffset.y += deltaY * orbitRatio; } else if (isPanning) { - const panRatio = 0.075; // Adjust this value as needed - panOffset.x -= deltaX * panRatio; // Horizontal panning - panOffset.y -= deltaY * panRatio; // Vertical panning + const panRatio = 0.075; + panOffset.x -= deltaX * panRatio; + panOffset.y -= deltaY * panRatio; } - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, currentZoomLevel, panOffset, orbitOffset); + updatePreview(); startMousePosition = currentMousePosition; event.preventDefault(); } - function HandlePreviewMouseUp(event) { + function handlePreviewMouseUp(event) { isPanning = false; isOrbiting = false; - document.removeEventListener('mousemove', HandlePreviewMouseMove, true); - document.removeEventListener('mouseup', HandlePreviewMouseUp, true); + document.removeEventListener('mousemove', handlePreviewMouseMove, true); + document.removeEventListener('mouseup', handlePreviewMouseUp, true); event.preventDefault(); } - function HandlePreviewMouseDown(event) { + function handlePreviewMouseDown(event) { startMousePosition = { x: event.clientX, y: event.clientY }; - if (event.button === 0) { // Left button + if (event.button === 0) { isOrbiting = true; - } else if (event.button === 1 || event.button === 2) { // Middle button or right button + } else if (event.button === 1 || event.button === 2) { isPanning = true; } - document.addEventListener('mousemove', HandlePreviewMouseMove, true); - document.addEventListener('mouseup', HandlePreviewMouseUp, true); + document.addEventListener('mousemove', handlePreviewMouseMove, true); + document.addEventListener('mouseup', handlePreviewMouseUp, true); event.preventDefault(); } - function HandleMouseWheel(event) { + function handleMouseWheel(event) { const zoomSpeed = 0.001; currentZoomLevel += event.deltaY * zoomSpeed; currentZoomLevel = Math.min(Math.max(currentZoomLevel, 0.1), 3); - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, currentZoomLevel, panOffset, orbitOffset); + updatePreview(); event.preventDefault(); } - function AddPainSnapshotSharingTab(parentDiv) { - function SendEmail(recipients, subject, body) { - console.log('Sending email to:', recipients); - } + function initializePreviewImage(container) { + previewImage = CreateDomElement('img', 'ov_snapshot_preview_image'); + container.appendChild(previewImage); + previewImage.addEventListener('wheel', handleMouseWheel, true); + previewImage.addEventListener('mousedown', handlePreviewMouseDown, true); + updatePreview(); + } - function DownloadSnapshotAndInfo(snapshot, info) { - console.log('Downloading snapshot and info'); - } + function cleanup() { + previewImage.removeEventListener('mousedown', handlePreviewMouseDown, true); + document.removeEventListener('mousemove', handlePreviewMouseMove, true); + document.removeEventListener('mouseup', handlePreviewMouseUp, true); + previewImage.removeEventListener('wheel', handleMouseWheel, true); + camera.eye.Rotate = originalRotate; + } + + return { + initializePreviewImage, + cleanup, + captureSnapshot + }; +} - function CreateMultiStepForm(parentDiv) { - let formContainer = AddDiv(parentDiv, 'ov_dialog_form_container'); - let step1 = AddDiv(formContainer, 'ov_dialog_step'); +function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset) { + const camera = viewer.navigation.GetCamera(); - AddDiv(step1, 'ov_dialog_title', Loc('Share Snapshot')); + // Store original camera state + const originalCamera = { + eye: { x: camera.eye.x, y: camera.eye.y, z: camera.eye.z }, + center: { x: camera.center.x, y: camera.center.y, z: camera.center.z }, + up: { x: camera.up.x, y: camera.up.y, z: camera.up.z } + }; - let description = AddDiv(step1, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); + // Apply zoom + const direction = { + x: camera.eye.x - camera.center.x, + y: camera.eye.y - camera.center.y, + z: camera.eye.z - camera.center.z + }; - let emailFields = []; - for (let i = 0; i < 3; i++) { - let emailLabel = AddDiv(step1, 'ov_dialog_label', Loc(`Email ${i + 1}`)); - let emailInput = AddDomElement(step1, 'input', `email${i}`); - emailInput.setAttribute('type', 'email'); - emailInput.setAttribute('class', 'ov_dialog_input'); - emailInput.setAttribute('placeholder', Loc('Enter email address')); - emailFields.push(emailInput); - } + const distance = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z); + const zoomedDistance = distance * zoomLevel; + const zoomFactor = zoomedDistance / distance; - let snapshotPreviewContainer = AddDiv(step1, 'ov_snapshot_preview_container'); - previewImage = CreateDomElement('img', 'ov_snapshot_preview_image'); - snapshotPreviewContainer.appendChild(previewImage); + const zoomedEye = { + x: camera.center.x + direction.x * zoomFactor, + y: camera.center.y + direction.y * zoomFactor, + z: camera.center.z + direction.z * zoomFactor + }; - // Remove zoom slider and add mouse wheel event listener for zoom - previewImage.addEventListener('wheel', HandleMouseWheel, true); + // Apply pan + const panScale = distance * 0.005; // Adjust this value as needed + const right = { + x: direction.y * camera.up.z - direction.z * camera.up.y, + y: direction.z * camera.up.x - direction.x * camera.up.z, + z: direction.x * camera.up.y - direction.y * camera.up.x + }; + const rightLength = Math.sqrt(right.x * right.x + right.y * right.y + right.z * right.z); + const normalizedRight = { + x: right.x / rightLength, + y: right.y / rightLength, + z: right.z / rightLength + }; - previewImage.addEventListener('mousedown', HandlePreviewMouseDown, true); + const pannedCenter = { + x: camera.center.x + normalizedRight.x * panOffset.x * panScale + camera.up.x * panOffset.y * panScale, + y: camera.center.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panOffset.y * panScale, + z: camera.center.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panOffset.y * panScale + }; + const pannedEye = { + x: zoomedEye.x + normalizedRight.x * panOffset.x * panScale + camera.up.x * panOffset.y * panScale, + y: zoomedEye.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panOffset.y * panScale, + z: zoomedEye.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panOffset.y * panScale + }; - UpdatePreview(viewer, previewImage, snapshotWidth, snapshotHeight, false, currentZoomLevel, panOffset, orbitOffset); + // Set temporary camera for snapshot + camera.eye.x = pannedEye.x; + camera.eye.y = pannedEye.y; + camera.eye.z = pannedEye.z; + camera.center.x = pannedCenter.x; + camera.center.y = pannedCenter.y; + camera.center.z = pannedCenter.z; + viewer.navigation.MoveCamera(camera, 0); + + // Apply orbit + viewer.navigation.Orbit(orbitOffset.x, orbitOffset.y); + + // Capture the image + const imageDataUrl = viewer.GetImageAsDataUrl(width, height, isTransparent); + + // Restore original camera state + camera.eye.x = originalCamera.eye.x; + camera.eye.y = originalCamera.eye.y; + camera.eye.z = originalCamera.eye.z; + camera.center.x = originalCamera.center.x; + camera.center.y = originalCamera.center.y; + camera.center.z = originalCamera.center.z; + camera.up.x = originalCamera.up.x; + camera.up.y = originalCamera.up.y; + camera.up.z = originalCamera.up.z; + viewer.navigation.MoveCamera(camera, 0); + + return imageDataUrl; +} - let nextButton = AddDiv(step1, 'ov_button', Loc('Next')); - nextButton.addEventListener('click', () => { - let emails = emailFields.map(input => input.value.trim()).filter(email => email.length > 0); - if (emails.length > 3) { - ShowMessageDialog(Loc('Error'), Loc('You can only send to up to 3 recipients.')); - } else { - step1.style.display = 'none'; - step2.style.display = 'block'; - } - }); - - let step2 = AddDiv(formContainer, 'ov_dialog_step'); - step2.style.display = 'none'; - AddDiv(step2, 'ov_dialog_title', Loc('Additional Options')); - - let sendToSelfCheckbox = AddCheckbox(step2, 'send_to_self', Loc('Send to myself'), false, () => {}); - let downloadCheckbox = AddCheckbox(step2, 'download_snapshot', Loc('Download snapshot and info'), false, () => {}); - - let intensityLabel = AddDiv(step2, 'ov_dialog_label', Loc('Pain Intensity')); - let intensityInput = AddDomElement(step2, 'input', null); - intensityInput.setAttribute('type', 'number'); - intensityInput.setAttribute('min', '1'); - intensityInput.setAttribute('max', '10'); - intensityInput.setAttribute('class', 'ov_dialog_input'); - intensityInput.setAttribute('placeholder', Loc('Enter pain intensity (1-10)')); - - let durationLabel = AddDiv(step2, 'ov_dialog_label', Loc('Pain Duration')); - let durationInput = AddDomElement(step2, 'input', null); - durationInput.setAttribute('type', 'text'); - durationInput.setAttribute('class', 'ov_dialog_input'); - durationInput.setAttribute('placeholder', Loc('Enter pain duration (e.g., 2 hours, 3 days)')); - - let submitButton = AddDiv(step2, 'ov_button', Loc('Submit')); - submitButton.addEventListener('click', () => { - let snapshot = previewImage.src; - let info = { - intensity: intensityInput.value, - duration: durationInput.value, - }; - - let emails = emailFields.map(input => input.value.trim()).filter(email => email.length > 0); - if (sendToSelfCheckbox.checked) { - emails.push('self@example.com'); - } +function createDialogManager(SnapshotManager) { + function createMultiStepForm(parentDiv) { + let formContainer = AddDiv(parentDiv, 'ov_dialog_form_container'); + let step1 = createStep1(formContainer); + let step2 = createStep2(formContainer); - SendEmail(emails, 'Pain Snapshot', `Snapshot: ${snapshot}\nInfo: ${JSON.stringify(info)}`); - if (downloadCheckbox.checked) { - DownloadSnapshotAndInfo(snapshot, info); - } + return { step1, step2 }; + } - ShowMessageDialog(Loc('Success'), Loc('Your snapshot and information have been shared.')); - }); + function createStep1(container) { + let step1 = AddDiv(container, 'ov_dialog_step'); + + AddDiv(step1, 'ov_dialog_title', Loc('Share Snapshot')); + AddDiv(step1, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); + + let emailFields = []; + for (let i = 0; i < 3; i++) { + AddDiv(step1, 'ov_dialog_label', Loc(`Email ${i + 1}`)); + let emailInput = AddDomElement(step1, 'input', `email${i}`); + emailInput.setAttribute('type', 'email'); + emailInput.setAttribute('class', 'ov_dialog_input'); + emailInput.setAttribute('placeholder', Loc('Enter email address')); + emailFields.push(emailInput); } - CreateMultiStepForm(parentDiv); + let snapshotPreviewContainer = AddDiv(step1, 'ov_snapshot_preview_container'); + SnapshotManager.initializePreviewImage(snapshotPreviewContainer); + + let nextButton = AddDiv(step1, 'ov_button', Loc('Next')); + nextButton.addEventListener('click', () => { + let emails = emailFields.map(input => input.value.trim()).filter(email => email.length > 0); + if (emails.length > 3) { + ShowMessageDialog(Loc('Error'), Loc('You can only send to up to 3 recipients.')); + } else { + step1.style.display = 'none'; + step2.style.display = 'block'; + } + }); + + return step1; } - let dialog = new ButtonDialog(); - let contentDiv = dialog.Init(Loc('Share'), [ - { - name: Loc('Close'), - onClick() { - dialog.Close(); + function createStep2(container) { + let step2 = AddDiv(container, 'ov_dialog_step'); + step2.style.display = 'none'; + AddDiv(step2, 'ov_dialog_title', Loc('Additional Options')); + + let sendToSelfCheckbox = AddCheckbox(step2, 'send_to_self', Loc('Send to myself'), false, () => {}); + let downloadCheckbox = AddCheckbox(step2, 'download_snapshot', Loc('Download snapshot and info'), false, () => {}); + + let intensityLabel = AddDiv(step2, 'ov_dialog_label', Loc('Pain Intensity')); + let intensityInput = AddDomElement(step2, 'input', null); + intensityInput.setAttribute('type', 'number'); + intensityInput.setAttribute('min', '1'); + intensityInput.setAttribute('max', '10'); + intensityInput.setAttribute('class', 'ov_dialog_input'); + intensityInput.setAttribute('placeholder', Loc('Enter pain intensity (1-10)')); + + let durationLabel = AddDiv(step2, 'ov_dialog_label', Loc('Pain Duration')); + let durationInput = AddDomElement(step2, 'input', null); + durationInput.setAttribute('type', 'text'); + durationInput.setAttribute('class', 'ov_dialog_input'); + durationInput.setAttribute('placeholder', Loc('Enter pain duration (e.g., 2 hours, 3 days)')); + + let submitButton = AddDiv(step2, 'ov_button', Loc('Submit')); + submitButton.addEventListener('click', () => { + let snapshot = SnapshotManager.captureSnapshot(false); + let info = { + intensity: intensityInput.value, + duration: durationInput.value, + }; + + // Here you would implement the actual sharing logic + console.log('Sharing snapshot:', snapshot); + console.log('Sharing info:', info); + + ShowMessageDialog(Loc('Success'), Loc('Your snapshot and information have been shared.')); + }); + + return step2; + } + + function showDialog() { + let dialog = new ButtonDialog(); + let contentDiv = dialog.Init(Loc('Share'), [ + { + name: Loc('Close'), + onClick() { + dialog.Close(); + } } - } - ]); + ]); - AddPainSnapshotSharingTab(contentDiv); + createMultiStepForm(contentDiv); - const originalClose = dialog.Close.bind(dialog); - dialog.Close = function() { - previewImage.removeEventListener('mousedown', HandlePreviewMouseDown, true); - document.removeEventListener('mousemove', HandlePreviewMouseMove, true); - document.removeEventListener('mouseup', HandlePreviewMouseUp, true); - previewImage.removeEventListener('wheel', HandleMouseWheel, true); - camera.eye.Rotate = originalRotate; - - originalClose(); - }; + const originalClose = dialog.Close.bind(dialog); + dialog.Close = function() { + SnapshotManager.cleanup(); + originalClose(); + }; - dialog.Open(); + dialog.Open(); + } - return dialog; + return { + showDialog + }; } From b81904e582bea8cd69eb186bbacd0627d2cdaf67 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Sat, 6 Jul 2024 23:13:05 +0800 Subject: [PATCH 11/18] making the sharing form have 3 snapshot previews --- source/website/css/dialogs.css | 15 +- source/website/css/sharingdialog.css | 94 ++++++- source/website/sharingdialog.js | 357 +++++++++++++++++++-------- 3 files changed, 350 insertions(+), 116 deletions(-) diff --git a/source/website/css/dialogs.css b/source/website/css/dialogs.css index ff8f27f9..39e8a42a 100644 --- a/source/website/css/dialogs.css +++ b/source/website/css/dialogs.css @@ -12,14 +12,13 @@ div.ov_modal_overlay position: absolute; } -div.ov_dialog -{ - color: var(--ov_dialog_foreground_color); - background: var(--ov_dialog_background_color); - width: 400px; - padding: 20px; - box-shadow: var(--ov_shadow); - border-radius: 5px; +div.ov_dialog { + color: var(--ov_dialog_foreground_color); + background: var(--ov_dialog_background_color); + width: 80vw; /* Adjust width */ + padding: 20px; + box-shadow: var(--ov_shadow); + border-radius: 5px; } div.ov_dialog div.ov_dialog_title diff --git a/source/website/css/sharingdialog.css b/source/website/css/sharingdialog.css index 9adfe127..2646a9ec 100644 --- a/source/website/css/sharingdialog.css +++ b/source/website/css/sharingdialog.css @@ -3,11 +3,16 @@ background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + max-height: 80vh; /* Ensure the form doesn't extend beyond 80% of the viewport height */ + overflow-y: auto; /* Enable scrolling if content overflows */ + width: 90vw; /* Make the form wider */ + max-width: 1200px; /* Limit the max width */ + margin: auto; /* Center the form horizontally */ } .ov_dialog_step { display: flex; - flex-direction: column; + flex-direction: row; /* Change direction to row to place elements side by side */ gap: 15px; } @@ -44,10 +49,11 @@ } .ov_snapshot_preview_image { - max-width: 100%; + width: 100%; height: auto; - border: 1px solid #ddd; - border-radius: 4px; + border: 1px solid #ddd; /* Add border to preview images */ + border-radius: 4px; /* Match the border radius of other elements */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Add a subtle shadow for better visual separation */ } .ov_button { @@ -58,18 +64,86 @@ border: none; border-radius: 4px; cursor: pointer; + text-align: center; + width: 100%; + box-sizing: border-box; } .ov_button:hover { background-color: #0056b3; } -.ov_dialog_form_container { - max-height: 80vh; /* Ensure the form doesn't extend beyond 80% of the viewport height */ - overflow-y: auto; /* Enable scrolling if content overflows */ +.ov_left_container { + width: 40%; + padding: 20px; } -.ov_dialog_step { - position: relative; /* Ensure steps are correctly positioned */ - overflow: hidden; /* Prevent unintended scrolling */ +.ov_right_container { + width: 60%; + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; /* Add space between preview containers */ +} + +.ov_step1, .ov_step2 { + padding: 20px; +} + +.ov_email_fields_container { + margin-bottom: 20px; +} + +.ov_preview_container { + display: flex; + flex-direction: column; /* Ensure images are stacked correctly */ + height: 100%; + gap: 20px; /* Add space between preview containers */ +} + +.ov_preview1_container { + width: 100%; + height: 300px; /* Adjust height for better layout */ + display: flex; + justify-content: center; /* Center the image horizontally */ + align-items: center; /* Center the image vertically */ +} + +.ov_preview_row { + display: flex; + width: 100%; + gap: 10px; /* Add space between side-by-side previews */ +} + +.ov_preview2_container, .ov_preview3_container { + flex: 1; /* Each takes up equal space in the row */ + height: 150px; /* Adjust height for better layout */ + display: flex; + justify-content: center; /* Center the image horizontally */ + align-items: center; /* Center the image vertically */ + overflow: hidden; /* Ensure no overflow */ +} + +.ov_preview_container img { + width: 100%; + height: 100%; + object-fit: contain; + border: 1px solid #ddd; /* Add border to preview images */ + border-radius: 4px; /* Match the border radius of other elements */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Add a subtle shadow for better visual separation */ +} + +.ov_next_button, .ov_submit_button { + text-align: center; + padding: 10px; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} + +.ov_next_button:hover, .ov_submit_button:hover { + background-color: #0056b3; } diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index ae3acd33..ba2d4fb3 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -11,108 +11,257 @@ export function ShowSharingDialog(settings, viewer) { DialogManager.showDialog(); } -function createSnapshotManager(viewer, settings) { - const snapshotWidth = 1920; - const snapshotHeight = 1080; - const initialZoomLevel = settings.snapshotZoomLevel || 1.5; - - let isPanning = false; - let isOrbiting = false; - let startMousePosition = { x: 0, y: 0 }; - let previewImage; - let panOffset = { x: 0, y: 0 }; - let orbitOffset = { x: 0, y: 0 }; - let currentZoomLevel = initialZoomLevel; - - const camera = viewer.navigation.GetCamera(); - const originalRotate = camera.eye.Rotate; +function createSnapshotManager(viewer, settings, snapshotWidth1 = 2000, snapshotHeight1 = 1080, snapshotWidth2 = 1080, snapshotHeight2 = 540) { + const initialZoomLevel = settings.snapshotZoomLevel || 0.5; + + let isPanning1 = false, isPanning2 = false, isPanning3 = false; + let isOrbiting1 = false, isOrbiting2 = false, isOrbiting3 = false; + let startMousePosition1 = { x: 0, y: 0 }, startMousePosition2 = { x: 0, y: 0 }, startMousePosition3 = { x: 0, y: 0 }; + let previewImage1, previewImage2, previewImage3; + let panOffset1 = { x: 0, y: 0 }, panOffset2 = { x: 0, y: 0 }, panOffset3 = { x: 0, y: 0 }; + let orbitOffset1 = { x: 0, y: 0 }, orbitOffset2 = { x: 0, y: 0 }, orbitOffset3 = { x: 0, y: 0 }; + let currentZoomLevel1 = initialZoomLevel, currentZoomLevel2 = initialZoomLevel, currentZoomLevel3 = initialZoomLevel; + + const camera1 = viewer.navigation.GetCamera(); + const camera2 = Object.assign({}, camera1); // Clone the initial camera for the second preview + const camera3 = Object.assign({}, camera1); // Clone the initial camera for the third preview + + const originalRotate1 = camera1.eye.Rotate; + const originalRotate2 = camera2.eye.Rotate; + const originalRotate3 = camera3.eye.Rotate; + + function captureSnapshot(isTransparent, camera, zoomLevel, panOffset, orbitOffset, width, height) { + // Adjust camera for better framing + camera.zoom = zoomLevel; + camera.panOffset = panOffset; + camera.orbitOffset = orbitOffset; + + // Set the aspect ratio + camera.aspectRatio = width / height; + + return CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset, camera); + } - function captureSnapshot(isTransparent) { - return CaptureSnapshot(viewer, snapshotWidth, snapshotHeight, isTransparent, currentZoomLevel, panOffset, orbitOffset); + function updatePreview1() { + let imageUrl = captureSnapshot(false, camera1, currentZoomLevel1, panOffset1, orbitOffset1, snapshotWidth1, snapshotHeight1); // Default size for preview 1 + previewImage1.src = imageUrl; } - function updatePreview() { - let imageUrl = captureSnapshot(false); - previewImage.src = imageUrl; + function updatePreview2() { + let imageUrl = captureSnapshot(false, camera2, currentZoomLevel2, panOffset2, orbitOffset2, snapshotWidth2, snapshotHeight2); // Half size for preview 2 + previewImage2.src = imageUrl; } - function handlePreviewMouseMove(event) { - if (!isPanning && !isOrbiting) return; - + function updatePreview3() { + let imageUrl = captureSnapshot(false, camera3, currentZoomLevel3, panOffset3, orbitOffset3, snapshotWidth2, snapshotHeight2); // Half size for preview 3 + previewImage3.src = imageUrl; + } + + function handlePreviewMouseMove1(event) { + if (!isPanning1 && !isOrbiting1) return; + const currentMousePosition = { x: event.clientX, y: event.clientY }; - const deltaX = currentMousePosition.x - startMousePosition.x; - const deltaY = currentMousePosition.y - startMousePosition.y; - - if (isOrbiting) { + const deltaX = currentMousePosition.x - startMousePosition1.x; + const deltaY = currentMousePosition.y - startMousePosition1.y; + + if (isOrbiting1) { const orbitRatio = 0.1; - orbitOffset.x += deltaX * orbitRatio; - orbitOffset.y += deltaY * orbitRatio; - } else if (isPanning) { + orbitOffset1.x += deltaX * orbitRatio; + orbitOffset1.y += deltaY * orbitRatio; + } else if (isPanning1) { const panRatio = 0.075; - panOffset.x -= deltaX * panRatio; - panOffset.y -= deltaY * panRatio; + panOffset1.x -= deltaX * panRatio; + panOffset1.y -= deltaY * panRatio; } - - updatePreview(); - - startMousePosition = currentMousePosition; + + updatePreview1(); + + startMousePosition1 = currentMousePosition; + event.preventDefault(); + } + + function handlePreviewMouseMove2(event) { + if (!isPanning2 && !isOrbiting2) return; + + const currentMousePosition = { x: event.clientX, y: event.clientY }; + const deltaX = currentMousePosition.x - startMousePosition2.x; + const deltaY = currentMousePosition.y - startMousePosition2.y; + + if (isOrbiting2) { + const orbitRatio = 0.1; + orbitOffset2.x += deltaX * orbitRatio; + orbitOffset2.y += deltaY * orbitRatio; + } else if (isPanning2) { + const panRatio = 0.075; + panOffset2.x -= deltaX * panRatio; + panOffset2.y -= deltaY * panRatio; + } + + updatePreview2(); + + startMousePosition2 = currentMousePosition; + event.preventDefault(); + } + + function handlePreviewMouseMove3(event) { + if (!isPanning3 && !isOrbiting3) return; + + const currentMousePosition = { x: event.clientX, y: event.clientY }; + const deltaX = currentMousePosition.x - startMousePosition3.x; + const deltaY = currentMousePosition.y - startMousePosition3.y; + + if (isOrbiting3) { + const orbitRatio = 0.1; + orbitOffset3.x += deltaX * orbitRatio; + orbitOffset3.y += deltaY * orbitRatio; + } else if (isPanning3) { + const panRatio = 0.075; + panOffset3.x -= deltaX * panRatio; + panOffset3.y -= deltaY * panRatio; + } + + updatePreview3(); + + startMousePosition3 = currentMousePosition; + event.preventDefault(); + } + + function handlePreviewMouseUp1(event) { + isPanning1 = false; + isOrbiting1 = false; + document.removeEventListener('mousemove', handlePreviewMouseMove1, true); + document.removeEventListener('mouseup', handlePreviewMouseUp1, true); + event.preventDefault(); + } + + function handlePreviewMouseUp2(event) { + isPanning2 = false; + isOrbiting2 = false; + document.removeEventListener('mousemove', handlePreviewMouseMove2, true); + document.removeEventListener('mouseup', handlePreviewMouseUp2, true); + event.preventDefault(); + } + + function handlePreviewMouseUp3(event) { + isPanning3 = false; + isOrbiting3 = false; + document.removeEventListener('mousemove', handlePreviewMouseMove3, true); + document.removeEventListener('mouseup', handlePreviewMouseUp3, true); event.preventDefault(); } - function handlePreviewMouseUp(event) { - isPanning = false; - isOrbiting = false; - document.removeEventListener('mousemove', handlePreviewMouseMove, true); - document.removeEventListener('mouseup', handlePreviewMouseUp, true); + function handlePreviewMouseDown1(event) { + startMousePosition1 = { x: event.clientX, y: event.clientY }; + if (event.button === 0) { + isOrbiting1 = true; + } else if (event.button === 1 || event.button === 2) { + isPanning1 = true; + } + document.addEventListener('mousemove', handlePreviewMouseMove1, true); + document.addEventListener('mouseup', handlePreviewMouseUp1, true); event.preventDefault(); } - function handlePreviewMouseDown(event) { - startMousePosition = { x: event.clientX, y: event.clientY }; + function handlePreviewMouseDown2(event) { + startMousePosition2 = { x: event.clientX, y: event.clientY }; if (event.button === 0) { - isOrbiting = true; + isOrbiting2 = true; } else if (event.button === 1 || event.button === 2) { - isPanning = true; + isPanning2 = true; } - document.addEventListener('mousemove', handlePreviewMouseMove, true); - document.addEventListener('mouseup', handlePreviewMouseUp, true); + document.addEventListener('mousemove', handlePreviewMouseMove2, true); + document.addEventListener('mouseup', handlePreviewMouseUp2, true); + event.preventDefault(); + } + + function handlePreviewMouseDown3(event) { + startMousePosition3 = { x: event.clientX, y: event.clientY }; + if (event.button === 0) { + isOrbiting3 = true; + } else if (event.button === 1 || event.button === 2) { + isPanning3 = true; + } + document.addEventListener('mousemove', handlePreviewMouseMove3, true); + document.addEventListener('mouseup', handlePreviewMouseUp3, true); + event.preventDefault(); + } + + function handleMouseWheel1(event) { + const zoomSpeed = 0.001; + currentZoomLevel1 += event.deltaY * zoomSpeed; + currentZoomLevel1 = Math.min(Math.max(currentZoomLevel1, 0.1), 3); + updatePreview1(); + event.preventDefault(); + } + + function handleMouseWheel2(event) { + const zoomSpeed = 0.001; + currentZoomLevel2 += event.deltaY * zoomSpeed; + currentZoomLevel2 = Math.min(Math.max(currentZoomLevel2, 0.1), 3); + updatePreview2(); event.preventDefault(); } - function handleMouseWheel(event) { + function handleMouseWheel3(event) { const zoomSpeed = 0.001; - currentZoomLevel += event.deltaY * zoomSpeed; - currentZoomLevel = Math.min(Math.max(currentZoomLevel, 0.1), 3); - updatePreview(); + currentZoomLevel3 += event.deltaY * zoomSpeed; + currentZoomLevel3 = Math.min(Math.max(currentZoomLevel3, 0.1), 3); + updatePreview3(); event.preventDefault(); } - function initializePreviewImage(container) { - previewImage = CreateDomElement('img', 'ov_snapshot_preview_image'); - container.appendChild(previewImage); - previewImage.addEventListener('wheel', handleMouseWheel, true); - previewImage.addEventListener('mousedown', handlePreviewMouseDown, true); - updatePreview(); + function initializePreviewImages(preview1Container, preview2Container, preview3Container) { + previewImage1 = CreateDomElement('img', 'ov_snapshot_preview_image'); + previewImage2 = CreateDomElement('img', 'ov_snapshot_preview_image'); + previewImage3 = CreateDomElement('img', 'ov_snapshot_preview_image'); + + preview1Container.appendChild(previewImage1); + preview2Container.appendChild(previewImage2); + preview3Container.appendChild(previewImage3); + + previewImage1.addEventListener('wheel', handleMouseWheel1, true); + previewImage1.addEventListener('mousedown', handlePreviewMouseDown1, true); + + previewImage2.addEventListener('wheel', handleMouseWheel2, true); + previewImage2.addEventListener('mousedown', handlePreviewMouseDown2, true); + + previewImage3.addEventListener('wheel', handleMouseWheel3, true); + previewImage3.addEventListener('mousedown', handlePreviewMouseDown3, true); + + updatePreview1(); + updatePreview2(); + updatePreview3(); } function cleanup() { - previewImage.removeEventListener('mousedown', handlePreviewMouseDown, true); - document.removeEventListener('mousemove', handlePreviewMouseMove, true); - document.removeEventListener('mouseup', handlePreviewMouseUp, true); - previewImage.removeEventListener('wheel', handleMouseWheel, true); - camera.eye.Rotate = originalRotate; + previewImage1.removeEventListener('mousedown', handlePreviewMouseDown1, true); + document.removeEventListener('mousemove', handlePreviewMouseMove1, true); + document.removeEventListener('mouseup', handlePreviewMouseUp1, true); + previewImage1.removeEventListener('wheel', handleMouseWheel1, true); + + previewImage2.removeEventListener('mousedown', handlePreviewMouseDown2, true); + document.removeEventListener('mousemove', handlePreviewMouseMove2, true); + document.removeEventListener('mouseup', handlePreviewMouseUp2, true); + previewImage2.removeEventListener('wheel', handleMouseWheel2, true); + + previewImage3.removeEventListener('mousedown', handlePreviewMouseDown3, true); + document.removeEventListener('mousemove', handlePreviewMouseMove3, true); + document.removeEventListener('mouseup', handlePreviewMouseUp3, true); + previewImage3.removeEventListener('wheel', handleMouseWheel3, true); + + camera1.eye.Rotate = originalRotate1; + camera2.eye.Rotate = originalRotate2; + camera3.eye.Rotate = originalRotate3; } return { - initializePreviewImage, + initializePreviewImages, cleanup, captureSnapshot }; } -function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset) { - const camera = viewer.navigation.GetCamera(); - +function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset, camera) { // Store original camera state const originalCamera = { eye: { x: camera.eye.x, y: camera.eye.y, z: camera.eye.z }, @@ -154,12 +303,12 @@ function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOff const pannedCenter = { x: camera.center.x + normalizedRight.x * panOffset.x * panScale + camera.up.x * panOffset.y * panScale, y: camera.center.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panOffset.y * panScale, - z: camera.center.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panOffset.y * panScale + z: camera.center.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panScale }; const pannedEye = { x: zoomedEye.x + normalizedRight.x * panOffset.x * panScale + camera.up.x * panOffset.y * panScale, - y: zoomedEye.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panOffset.y * panScale, - z: zoomedEye.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panOffset.y * panScale + y: zoomedEye.y + normalizedRight.y * panOffset.x * panScale + camera.up.y * panScale, + z: zoomedEye.z + normalizedRight.z * panOffset.x * panScale + camera.up.z * panScale }; // Set temporary camera for snapshot @@ -174,6 +323,11 @@ function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOff // Apply orbit viewer.navigation.Orbit(orbitOffset.x, orbitOffset.y); + // Set aspect ratio and resize renderer + viewer.renderer.setSize(width, height); + viewer.camera.aspect = width / height; + viewer.camera.updateProjectionMatrix(); + // Capture the image const imageDataUrl = viewer.GetImageAsDataUrl(width, height, isTransparent); @@ -202,41 +356,43 @@ function createDialogManager(SnapshotManager) { } function createStep1(container) { - let step1 = AddDiv(container, 'ov_dialog_step'); - - AddDiv(step1, 'ov_dialog_title', Loc('Share Snapshot')); - AddDiv(step1, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); - - let emailFields = []; + let step1 = AddDiv(container, 'ov_dialog_step ov_step1'); + + let leftContainer = AddDiv(step1, 'ov_left_container'); + AddDiv(leftContainer, 'ov_dialog_title', Loc('Share Snapshot')); + AddDiv(leftContainer, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); + + let emailFieldsContainer = AddDiv(leftContainer, 'ov_email_fields_container'); for (let i = 0; i < 3; i++) { - AddDiv(step1, 'ov_dialog_label', Loc(`Email ${i + 1}`)); - let emailInput = AddDomElement(step1, 'input', `email${i}`); + let emailLabel = AddDiv(emailFieldsContainer, 'ov_dialog_label', Loc(`Email ${i + 1}`)); + let emailInput = AddDomElement(emailFieldsContainer, 'input', `email${i}`); emailInput.setAttribute('type', 'email'); emailInput.setAttribute('class', 'ov_dialog_input'); emailInput.setAttribute('placeholder', Loc('Enter email address')); - emailFields.push(emailInput); } - - let snapshotPreviewContainer = AddDiv(step1, 'ov_snapshot_preview_container'); - SnapshotManager.initializePreviewImage(snapshotPreviewContainer); - - let nextButton = AddDiv(step1, 'ov_button', Loc('Next')); + + let rightContainer = AddDiv(step1, 'ov_right_container'); + let previewContainer = AddDiv(rightContainer, 'ov_preview_container'); + let preview1Container = AddDiv(previewContainer, 'ov_preview1_container'); + let previewRow = AddDiv(previewContainer, 'ov_preview_row'); // New row container for side-by-side previews + let preview2Container = AddDiv(previewRow, 'ov_preview2_container'); + let preview3Container = AddDiv(previewRow, 'ov_preview3_container'); + + SnapshotManager.initializePreviewImages(preview1Container, preview2Container, preview3Container); + + let nextButton = AddDiv(leftContainer, 'ov_button ov_next_button', Loc('Next')); nextButton.addEventListener('click', () => { - let emails = emailFields.map(input => input.value.trim()).filter(email => email.length > 0); - if (emails.length > 3) { - ShowMessageDialog(Loc('Error'), Loc('You can only send to up to 3 recipients.')); - } else { - step1.style.display = 'none'; - step2.style.display = 'block'; - } + step1.style.display = 'none'; + step2.style.display = 'block'; }); - + return step1; } function createStep2(container) { - let step2 = AddDiv(container, 'ov_dialog_step'); + let step2 = AddDiv(container, 'ov_dialog_step ov_step2'); step2.style.display = 'none'; + AddDiv(step2, 'ov_dialog_title', Loc('Additional Options')); let sendToSelfCheckbox = AddCheckbox(step2, 'send_to_self', Loc('Send to myself'), false, () => {}); @@ -256,17 +412,22 @@ function createDialogManager(SnapshotManager) { durationInput.setAttribute('class', 'ov_dialog_input'); durationInput.setAttribute('placeholder', Loc('Enter pain duration (e.g., 2 hours, 3 days)')); - let submitButton = AddDiv(step2, 'ov_button', Loc('Submit')); + let submitButton = AddDiv(step2, 'ov_button ov_submit_button', Loc('Submit')); submitButton.addEventListener('click', () => { - let snapshot = SnapshotManager.captureSnapshot(false); + let snapshot1 = SnapshotManager.captureSnapshot(false, camera1, currentZoomLevel1, panOffset1, orbitOffset1); + let snapshot2 = SnapshotManager.captureSnapshot(false, camera2, currentZoomLevel2, panOffset2, orbitOffset2); + let snapshot3 = SnapshotManager.captureSnapshot(false, camera3, currentZoomLevel3, panOffset3, orbitOffset3); + let info = { intensity: intensityInput.value, duration: durationInput.value, }; - // Here you would implement the actual sharing logic - console.log('Sharing snapshot:', snapshot); - console.log('Sharing info:', info); + // Here you would imsplement the actual sharing logic + // console.log('Sharing snapshot1:', snapshot1); + // console.log('Sharing snapshot2:', snapshot2); + // console.log('Sharing snapshot3:', snapshot3); + // console.log('Sharing info:', info); ShowMessageDialog(Loc('Success'), Loc('Your snapshot and information have been shared.')); }); @@ -299,4 +460,4 @@ function createDialogManager(SnapshotManager) { return { showDialog }; -} +} \ No newline at end of file From c46ba629ef3f14fa4b9586ba01bb7bf61e624ad5 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Sun, 7 Jul 2024 22:37:13 +0800 Subject: [PATCH 12/18] refactor --- source/website/css/sharingdialog.css | 13 +- source/website/sharingdialog.js | 497 +++++++++++---------------- 2 files changed, 200 insertions(+), 310 deletions(-) diff --git a/source/website/css/sharingdialog.css b/source/website/css/sharingdialog.css index 2646a9ec..bfb4e925 100644 --- a/source/website/css/sharingdialog.css +++ b/source/website/css/sharingdialog.css @@ -49,9 +49,9 @@ } .ov_snapshot_preview_image { - width: 100%; + border: 1px solid #ccc; + max-width: 100%; height: auto; - border: 1px solid #ddd; /* Add border to preview images */ border-radius: 4px; /* Match the border radius of other elements */ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Add a subtle shadow for better visual separation */ } @@ -97,13 +97,11 @@ .ov_preview_container { display: flex; flex-direction: column; /* Ensure images are stacked correctly */ - height: 100%; - gap: 20px; /* Add space between preview containers */ + gap: 10px; /* Add space between preview containers */ } .ov_preview1_container { width: 100%; - height: 300px; /* Adjust height for better layout */ display: flex; justify-content: center; /* Center the image horizontally */ align-items: center; /* Center the image vertically */ @@ -124,6 +122,11 @@ overflow: hidden; /* Ensure no overflow */ } +.ov_preview2_container, +.ov_preview3_container { + width: calc(50% - 5px); +} + .ov_preview_container img { width: 100%; height: 100%; diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index ba2d4fb3..713f2fbe 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -5,260 +5,153 @@ import { ButtonDialog } from './dialog.js'; import { HandleEvent } from './eventhandler.js'; import { Loc } from '../engine/core/localization.js'; + +const CONFIG = { + SNAPSHOT_SIZES: { + LARGE: { width: 2000, height: 1080 }, + SMALL: { width: 1080, height: 540 } + }, + INITIAL_ZOOM: 0.5, + MAX_ZOOM: 3, + MIN_ZOOM: 0.1, + ZOOM_SPEED: 0.001, + ORBIT_RATIO: 0.1, + PAN_RATIO: 0.075 +}; + export function ShowSharingDialog(settings, viewer) { const SnapshotManager = createSnapshotManager(viewer, settings); const DialogManager = createDialogManager(SnapshotManager); DialogManager.showDialog(); } -function createSnapshotManager(viewer, settings, snapshotWidth1 = 2000, snapshotHeight1 = 1080, snapshotWidth2 = 1080, snapshotHeight2 = 540) { - const initialZoomLevel = settings.snapshotZoomLevel || 0.5; - - let isPanning1 = false, isPanning2 = false, isPanning3 = false; - let isOrbiting1 = false, isOrbiting2 = false, isOrbiting3 = false; - let startMousePosition1 = { x: 0, y: 0 }, startMousePosition2 = { x: 0, y: 0 }, startMousePosition3 = { x: 0, y: 0 }; - let previewImage1, previewImage2, previewImage3; - let panOffset1 = { x: 0, y: 0 }, panOffset2 = { x: 0, y: 0 }, panOffset3 = { x: 0, y: 0 }; - let orbitOffset1 = { x: 0, y: 0 }, orbitOffset2 = { x: 0, y: 0 }, orbitOffset3 = { x: 0, y: 0 }; - let currentZoomLevel1 = initialZoomLevel, currentZoomLevel2 = initialZoomLevel, currentZoomLevel3 = initialZoomLevel; - - const camera1 = viewer.navigation.GetCamera(); - const camera2 = Object.assign({}, camera1); // Clone the initial camera for the second preview - const camera3 = Object.assign({}, camera1); // Clone the initial camera for the third preview +function createSnapshotManager(viewer, settings) { + const cameras = Array(3).fill().map(() => ({ ...viewer.navigation.GetCamera() })); + const states = Array(3).fill().map(() => ({ + isPanning: false, + isOrbiting: false, + startMousePosition: { x: 0, y: 0 }, + panOffset: { x: 0, y: 0 }, + orbitOffset: { x: 0, y: 0 }, + currentZoomLevel: CONFIG.INITIAL_ZOOM + })); + let previewImages = []; + + function captureSnapshot(index) { + if (index < 0 || index >= cameras.length) { + console.error(`Invalid index: ${index}`); + return null; + } - const originalRotate1 = camera1.eye.Rotate; - const originalRotate2 = camera2.eye.Rotate; - const originalRotate3 = camera3.eye.Rotate; + const { width, height } = index === 0 ? CONFIG.SNAPSHOT_SIZES.LARGE : CONFIG.SNAPSHOT_SIZES.SMALL; + const { currentZoomLevel, panOffset, orbitOffset } = states[index]; + const camera = cameras[index]; - function captureSnapshot(isTransparent, camera, zoomLevel, panOffset, orbitOffset, width, height) { - // Adjust camera for better framing - camera.zoom = zoomLevel; + camera.zoom = currentZoomLevel; camera.panOffset = panOffset; camera.orbitOffset = orbitOffset; - - // Set the aspect ratio camera.aspectRatio = width / height; - return CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset, camera); - } - - function updatePreview1() { - let imageUrl = captureSnapshot(false, camera1, currentZoomLevel1, panOffset1, orbitOffset1, snapshotWidth1, snapshotHeight1); // Default size for preview 1 - previewImage1.src = imageUrl; - } - - function updatePreview2() { - let imageUrl = captureSnapshot(false, camera2, currentZoomLevel2, panOffset2, orbitOffset2, snapshotWidth2, snapshotHeight2); // Half size for preview 2 - previewImage2.src = imageUrl; - } - - function updatePreview3() { - let imageUrl = captureSnapshot(false, camera3, currentZoomLevel3, panOffset3, orbitOffset3, snapshotWidth2, snapshotHeight2); // Half size for preview 3 - previewImage3.src = imageUrl; + return CaptureSnapshot(viewer, width, height, false, currentZoomLevel, panOffset, orbitOffset, camera); } - function handlePreviewMouseMove1(event) { - if (!isPanning1 && !isOrbiting1) return; - - const currentMousePosition = { x: event.clientX, y: event.clientY }; - const deltaX = currentMousePosition.x - startMousePosition1.x; - const deltaY = currentMousePosition.y - startMousePosition1.y; - - if (isOrbiting1) { - const orbitRatio = 0.1; - orbitOffset1.x += deltaX * orbitRatio; - orbitOffset1.y += deltaY * orbitRatio; - } else if (isPanning1) { - const panRatio = 0.075; - panOffset1.x -= deltaX * panRatio; - panOffset1.y -= deltaY * panRatio; + function updatePreview(index) { + if (index < 0 || index >= previewImages.length) { + console.error(`Invalid preview index: ${index}`); + return; } - updatePreview1(); - - startMousePosition1 = currentMousePosition; - event.preventDefault(); - } - - function handlePreviewMouseMove2(event) { - if (!isPanning2 && !isOrbiting2) return; - - const currentMousePosition = { x: event.clientX, y: event.clientY }; - const deltaX = currentMousePosition.x - startMousePosition2.x; - const deltaY = currentMousePosition.y - startMousePosition2.y; - - if (isOrbiting2) { - const orbitRatio = 0.1; - orbitOffset2.x += deltaX * orbitRatio; - orbitOffset2.y += deltaY * orbitRatio; - } else if (isPanning2) { - const panRatio = 0.075; - panOffset2.x -= deltaX * panRatio; - panOffset2.y -= deltaY * panRatio; + const snapshotData = captureSnapshot(index); + if (snapshotData) { + previewImages[index].src = snapshotData; + } else { + console.error(`Failed to capture snapshot for index: ${index}`); } - - updatePreview2(); - - startMousePosition2 = currentMousePosition; - event.preventDefault(); } - function handlePreviewMouseMove3(event) { - if (!isPanning3 && !isOrbiting3) return; - - const currentMousePosition = { x: event.clientX, y: event.clientY }; - const deltaX = currentMousePosition.x - startMousePosition3.x; - const deltaY = currentMousePosition.y - startMousePosition3.y; - - if (isOrbiting3) { - const orbitRatio = 0.1; - orbitOffset3.x += deltaX * orbitRatio; - orbitOffset3.y += deltaY * orbitRatio; - } else if (isPanning3) { - const panRatio = 0.075; - panOffset3.x -= deltaX * panRatio; - panOffset3.y -= deltaY * panRatio; - } - - updatePreview3(); - - startMousePosition3 = currentMousePosition; - event.preventDefault(); - } - - function handlePreviewMouseUp1(event) { - isPanning1 = false; - isOrbiting1 = false; - document.removeEventListener('mousemove', handlePreviewMouseMove1, true); - document.removeEventListener('mouseup', handlePreviewMouseUp1, true); - event.preventDefault(); - } - - function handlePreviewMouseUp2(event) { - isPanning2 = false; - isOrbiting2 = false; - document.removeEventListener('mousemove', handlePreviewMouseMove2, true); - document.removeEventListener('mouseup', handlePreviewMouseUp2, true); - event.preventDefault(); - } - - function handlePreviewMouseUp3(event) { - isPanning3 = false; - isOrbiting3 = false; - document.removeEventListener('mousemove', handlePreviewMouseMove3, true); - document.removeEventListener('mouseup', handlePreviewMouseUp3, true); - event.preventDefault(); - } - - function handlePreviewMouseDown1(event) { - startMousePosition1 = { x: event.clientX, y: event.clientY }; - if (event.button === 0) { - isOrbiting1 = true; - } else if (event.button === 1 || event.button === 2) { - isPanning1 = true; - } - document.addEventListener('mousemove', handlePreviewMouseMove1, true); - document.addEventListener('mouseup', handlePreviewMouseUp1, true); - event.preventDefault(); - } + function initializePreviewImages(containers) { + previewImages = containers.map((container, index) => { + const img = CreateDomElement('img', 'ov_snapshot_preview_image'); + container.appendChild(img); + ['wheel', 'mousedown'].forEach(eventType => + img.addEventListener(eventType, (e) => handleMouseEvent(index, eventType, e), true) + ); + return img; + }); - function handlePreviewMouseDown2(event) { - startMousePosition2 = { x: event.clientX, y: event.clientY }; - if (event.button === 0) { - isOrbiting2 = true; - } else if (event.button === 1 || event.button === 2) { - isPanning2 = true; - } - document.addEventListener('mousemove', handlePreviewMouseMove2, true); - document.addEventListener('mouseup', handlePreviewMouseUp2, true); - event.preventDefault(); + // Update previews after initialization + previewImages.forEach((_, index) => updatePreview(index)); } + + function handleMouseEvent(index, eventType, event) { + const state = states[index]; + switch (eventType) { + case 'mousemove': + if (!state.isPanning && !state.isOrbiting) return; + const currentMousePosition = { x: event.clientX, y: event.clientY }; + const deltaX = currentMousePosition.x - state.startMousePosition.x; + const deltaY = currentMousePosition.y - state.startMousePosition.y; + + if (state.isOrbiting) { + state.orbitOffset.x += deltaX * CONFIG.ORBIT_RATIO; + state.orbitOffset.y += deltaY * CONFIG.ORBIT_RATIO; + } else if (state.isPanning) { + state.panOffset.x -= deltaX * CONFIG.PAN_RATIO; + state.panOffset.y -= deltaY * CONFIG.PAN_RATIO; + } - function handlePreviewMouseDown3(event) { - startMousePosition3 = { x: event.clientX, y: event.clientY }; - if (event.button === 0) { - isOrbiting3 = true; - } else if (event.button === 1 || event.button === 2) { - isPanning3 = true; + updatePreview(index); + state.startMousePosition = currentMousePosition; + break; + case 'mousedown': + state.startMousePosition = { x: event.clientX, y: event.clientY }; + if (event.button === 0) { + state.isOrbiting = true; + } else if (event.button === 1 || event.button === 2) { + state.isPanning = true; + } + document.addEventListener('mousemove', (e) => handleMouseEvent(index, 'mousemove', e), true); + document.addEventListener('mouseup', (e) => handleMouseEvent(index, 'mouseup', e), true); + break; + case 'mouseup': + state.isPanning = false; + state.isOrbiting = false; + document.removeEventListener('mousemove', (e) => handleMouseEvent(index, 'mousemove', e), true); + document.removeEventListener('mouseup', (e) => handleMouseEvent(index, 'mouseup', e), true); + break; + case 'wheel': + state.currentZoomLevel += event.deltaY * CONFIG.ZOOM_SPEED; + state.currentZoomLevel = Math.min(Math.max(state.currentZoomLevel, CONFIG.MIN_ZOOM), CONFIG.MAX_ZOOM); + updatePreview(index); + break; } - document.addEventListener('mousemove', handlePreviewMouseMove3, true); - document.addEventListener('mouseup', handlePreviewMouseUp3, true); event.preventDefault(); } - function handleMouseWheel1(event) { - const zoomSpeed = 0.001; - currentZoomLevel1 += event.deltaY * zoomSpeed; - currentZoomLevel1 = Math.min(Math.max(currentZoomLevel1, 0.1), 3); - updatePreview1(); - event.preventDefault(); - } - - function handleMouseWheel2(event) { - const zoomSpeed = 0.001; - currentZoomLevel2 += event.deltaY * zoomSpeed; - currentZoomLevel2 = Math.min(Math.max(currentZoomLevel2, 0.1), 3); - updatePreview2(); - event.preventDefault(); - } - - function handleMouseWheel3(event) { - const zoomSpeed = 0.001; - currentZoomLevel3 += event.deltaY * zoomSpeed; - currentZoomLevel3 = Math.min(Math.max(currentZoomLevel3, 0.1), 3); - updatePreview3(); - event.preventDefault(); - } + function initializePreviewImages(containers) { + previewImages = containers.map((container, index) => { + const img = CreateDomElement('img', 'ov_snapshot_preview_image'); + container.appendChild(img); + img.addEventListener('wheel', (e) => handleMouseEvent(index, 'wheel', e), { passive: false }); + img.addEventListener('mousedown', (e) => handleMouseEvent(index, 'mousedown', e)); + img.addEventListener('contextmenu', (e) => e.preventDefault()); + return img; + }); - function initializePreviewImages(preview1Container, preview2Container, preview3Container) { - previewImage1 = CreateDomElement('img', 'ov_snapshot_preview_image'); - previewImage2 = CreateDomElement('img', 'ov_snapshot_preview_image'); - previewImage3 = CreateDomElement('img', 'ov_snapshot_preview_image'); - - preview1Container.appendChild(previewImage1); - preview2Container.appendChild(previewImage2); - preview3Container.appendChild(previewImage3); - - previewImage1.addEventListener('wheel', handleMouseWheel1, true); - previewImage1.addEventListener('mousedown', handlePreviewMouseDown1, true); - - previewImage2.addEventListener('wheel', handleMouseWheel2, true); - previewImage2.addEventListener('mousedown', handlePreviewMouseDown2, true); - - previewImage3.addEventListener('wheel', handleMouseWheel3, true); - previewImage3.addEventListener('mousedown', handlePreviewMouseDown3, true); - - updatePreview1(); - updatePreview2(); - updatePreview3(); + // Update previews after initialization + previewImages.forEach((_, index) => updatePreview(index)); } function cleanup() { - previewImage1.removeEventListener('mousedown', handlePreviewMouseDown1, true); - document.removeEventListener('mousemove', handlePreviewMouseMove1, true); - document.removeEventListener('mouseup', handlePreviewMouseUp1, true); - previewImage1.removeEventListener('wheel', handleMouseWheel1, true); - - previewImage2.removeEventListener('mousedown', handlePreviewMouseDown2, true); - document.removeEventListener('mousemove', handlePreviewMouseMove2, true); - document.removeEventListener('mouseup', handlePreviewMouseUp2, true); - previewImage2.removeEventListener('wheel', handleMouseWheel2, true); - - previewImage3.removeEventListener('mousedown', handlePreviewMouseDown3, true); - document.removeEventListener('mousemove', handlePreviewMouseMove3, true); - document.removeEventListener('mouseup', handlePreviewMouseUp3, true); - previewImage3.removeEventListener('wheel', handleMouseWheel3, true); - - camera1.eye.Rotate = originalRotate1; - camera2.eye.Rotate = originalRotate2; - camera3.eye.Rotate = originalRotate3; + previewImages.forEach((img, index) => { + img.removeEventListener('wheel', (e) => handleMouseEvent(index, 'wheel', e)); + img.removeEventListener('mousedown', (e) => handleMouseEvent(index, 'mousedown', e)); + }); + document.removeEventListener('mousemove', handleMouseEvent); + document.removeEventListener('mouseup', handleMouseEvent); } - return { - initializePreviewImages, - cleanup, - captureSnapshot - }; + return { initializePreviewImages, cleanup, captureSnapshot, updatePreview }; } function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOffset, orbitOffset, camera) { @@ -346,118 +239,112 @@ function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOff return imageDataUrl; } -function createDialogManager(SnapshotManager) { +function createDialogManager(snapshotManager) { function createMultiStepForm(parentDiv) { - let formContainer = AddDiv(parentDiv, 'ov_dialog_form_container'); - let step1 = createStep1(formContainer); - let step2 = createStep2(formContainer); - + const formContainer = AddDiv(parentDiv, 'ov_dialog_form_container'); + const step1 = createStep(formContainer, 1); + const step2 = createStep(formContainer, 2); return { step1, step2 }; } - function createStep1(container) { - let step1 = AddDiv(container, 'ov_dialog_step ov_step1'); - - let leftContainer = AddDiv(step1, 'ov_left_container'); + function createStep(container, stepNumber) { + const step = AddDiv(container, `ov_dialog_step ov_step${stepNumber}`); + if (stepNumber === 2) step.style.display = 'none'; + + const content = stepNumber === 1 ? createStep1Content(step) : createStep2Content(step); + + return step; + } + + function createStep1Content(step) { + const leftContainer = AddDiv(step, 'ov_left_container'); AddDiv(leftContainer, 'ov_dialog_title', Loc('Share Snapshot')); AddDiv(leftContainer, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); - - let emailFieldsContainer = AddDiv(leftContainer, 'ov_email_fields_container'); + + const emailFieldsContainer = AddDiv(leftContainer, 'ov_email_fields_container'); for (let i = 0; i < 3; i++) { - let emailLabel = AddDiv(emailFieldsContainer, 'ov_dialog_label', Loc(`Email ${i + 1}`)); - let emailInput = AddDomElement(emailFieldsContainer, 'input', `email${i}`); - emailInput.setAttribute('type', 'email'); - emailInput.setAttribute('class', 'ov_dialog_input'); - emailInput.setAttribute('placeholder', Loc('Enter email address')); + AddDiv(emailFieldsContainer, 'ov_dialog_label', Loc(`Email ${i + 1}`)); + const emailInput = AddDomElement(emailFieldsContainer, 'input', `email${i}`); + emailInput.type = 'email'; + emailInput.className = 'ov_dialog_input'; + emailInput.placeholder = Loc('Enter email address'); } + + const rightContainer = AddDiv(step, 'ov_right_container'); + const previewContainer = AddDiv(rightContainer, 'ov_preview_container'); - let rightContainer = AddDiv(step1, 'ov_right_container'); - let previewContainer = AddDiv(rightContainer, 'ov_preview_container'); - let preview1Container = AddDiv(previewContainer, 'ov_preview1_container'); - let previewRow = AddDiv(previewContainer, 'ov_preview_row'); // New row container for side-by-side previews - let preview2Container = AddDiv(previewRow, 'ov_preview2_container'); - let preview3Container = AddDiv(previewRow, 'ov_preview3_container'); - - SnapshotManager.initializePreviewImages(preview1Container, preview2Container, preview3Container); - - let nextButton = AddDiv(leftContainer, 'ov_button ov_next_button', Loc('Next')); + const preview1Container = AddDiv(previewContainer, 'ov_preview1_container'); + const previewRow = AddDiv(previewContainer, 'ov_preview_row'); + const preview2Container = AddDiv(previewRow, 'ov_preview2_container'); + const preview3Container = AddDiv(previewRow, 'ov_preview3_container'); + + const previewContainers = [preview1Container, preview2Container, preview3Container]; + + snapshotManager.initializePreviewImages(previewContainers); + + const nextButton = AddDiv(leftContainer, 'ov_button ov_next_button', Loc('Next')); nextButton.addEventListener('click', () => { - step1.style.display = 'none'; - step2.style.display = 'block'; + step.style.display = 'none'; + step.nextElementSibling.style.display = 'block'; }); - - return step1; } - function createStep2(container) { - let step2 = AddDiv(container, 'ov_dialog_step ov_step2'); - step2.style.display = 'none'; - - AddDiv(step2, 'ov_dialog_title', Loc('Additional Options')); - - let sendToSelfCheckbox = AddCheckbox(step2, 'send_to_self', Loc('Send to myself'), false, () => {}); - let downloadCheckbox = AddCheckbox(step2, 'download_snapshot', Loc('Download snapshot and info'), false, () => {}); - - let intensityLabel = AddDiv(step2, 'ov_dialog_label', Loc('Pain Intensity')); - let intensityInput = AddDomElement(step2, 'input', null); - intensityInput.setAttribute('type', 'number'); - intensityInput.setAttribute('min', '1'); - intensityInput.setAttribute('max', '10'); - intensityInput.setAttribute('class', 'ov_dialog_input'); - intensityInput.setAttribute('placeholder', Loc('Enter pain intensity (1-10)')); - - let durationLabel = AddDiv(step2, 'ov_dialog_label', Loc('Pain Duration')); - let durationInput = AddDomElement(step2, 'input', null); - durationInput.setAttribute('type', 'text'); - durationInput.setAttribute('class', 'ov_dialog_input'); - durationInput.setAttribute('placeholder', Loc('Enter pain duration (e.g., 2 hours, 3 days)')); - - let submitButton = AddDiv(step2, 'ov_button ov_submit_button', Loc('Submit')); - submitButton.addEventListener('click', () => { - let snapshot1 = SnapshotManager.captureSnapshot(false, camera1, currentZoomLevel1, panOffset1, orbitOffset1); - let snapshot2 = SnapshotManager.captureSnapshot(false, camera2, currentZoomLevel2, panOffset2, orbitOffset2); - let snapshot3 = SnapshotManager.captureSnapshot(false, camera3, currentZoomLevel3, panOffset3, orbitOffset3); - - let info = { - intensity: intensityInput.value, - duration: durationInput.value, - }; - - // Here you would imsplement the actual sharing logic - // console.log('Sharing snapshot1:', snapshot1); - // console.log('Sharing snapshot2:', snapshot2); - // console.log('Sharing snapshot3:', snapshot3); - // console.log('Sharing info:', info); - - ShowMessageDialog(Loc('Success'), Loc('Your snapshot and information have been shared.')); - }); + function createStep2Content(step) { + AddDiv(step, 'ov_dialog_title', Loc('Additional Options')); + + AddCheckbox(step, 'send_to_self', Loc('Send to myself'), false, () => {}); + AddCheckbox(step, 'download_snapshot', Loc('Download snapshot and info'), false, () => {}); - return step2; + const intensityInput = createInputField(step, 'number', Loc('Pain Intensity'), 'Enter pain intensity (1-10)', { min: 1, max: 10 }); + const durationInput = createInputField(step, 'text', Loc('Pain Duration'), 'Enter pain duration (e.g., 2 hours, 3 days)'); + + const submitButton = AddDiv(step, 'ov_button ov_submit_button', Loc('Submit')); + submitButton.addEventListener('click', () => handleSubmit(intensityInput, durationInput)); + } + + function createInputField(container, type, labelText, placeholder, attributes = {}) { + AddDiv(container, 'ov_dialog_label', labelText); + const input = AddDomElement(container, 'input', null); + input.type = type; + input.className = 'ov_dialog_input'; + input.placeholder = Loc(placeholder); + Object.entries(attributes).forEach(([key, value]) => input.setAttribute(key, value)); + return input; + } + + function handleSubmit(intensityInput, durationInput) { + const snapshots = [1, 2, 3].map(i => snapshotManager.captureSnapshot(i - 1)); + const info = { + intensity: intensityInput.value, + duration: durationInput.value, + }; + + // Here you would implement the actual sharing logic + console.log('Sharing snapshots:', snapshots); + console.log('Sharing info:', info); + + ShowMessageDialog(Loc('Success'), Loc('Your snapshot and information have been shared.')); } function showDialog() { - let dialog = new ButtonDialog(); - let contentDiv = dialog.Init(Loc('Share'), [ + const dialog = new ButtonDialog(); + const contentDiv = dialog.Init(Loc('Share'), [ { name: Loc('Close'), - onClick() { - dialog.Close(); - } + onClick: () => dialog.Close() } ]); createMultiStepForm(contentDiv); const originalClose = dialog.Close.bind(dialog); - dialog.Close = function() { - SnapshotManager.cleanup(); + dialog.Close = () => { + snapshotManager.cleanup(); originalClose(); }; dialog.Open(); } - return { - showDialog - }; + return { showDialog }; } \ No newline at end of file From 6597b37373c20fb2190bcabe00695367af1e62ed Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Mon, 8 Jul 2024 02:08:30 +0800 Subject: [PATCH 13/18] styling and better bug fix. (Not totally fixed) --- source/website/sharingdialog.js | 92 +++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 713f2fbe..0dd4cca2 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -73,16 +73,19 @@ function createSnapshotManager(viewer, settings) { previewImages = containers.map((container, index) => { const img = CreateDomElement('img', 'ov_snapshot_preview_image'); container.appendChild(img); - ['wheel', 'mousedown'].forEach(eventType => - img.addEventListener(eventType, (e) => handleMouseEvent(index, eventType, e), true) - ); + ['wheel', 'mousedown', 'mousemove', 'mouseup', 'contextmenu'].forEach(eventType => { + img.addEventListener(eventType, (e) => { + e.stopPropagation(); + handleMouseEvent(index, eventType, e); + }, { passive: false }); + }); return img; }); // Update previews after initialization previewImages.forEach((_, index) => updatePreview(index)); } - + function handleMouseEvent(index, eventType, event) { const state = states[index]; switch (eventType) { @@ -272,14 +275,14 @@ function createDialogManager(snapshotManager) { const rightContainer = AddDiv(step, 'ov_right_container'); const previewContainer = AddDiv(rightContainer, 'ov_preview_container'); - + const preview1Container = AddDiv(previewContainer, 'ov_preview1_container'); const previewRow = AddDiv(previewContainer, 'ov_preview_row'); const preview2Container = AddDiv(previewRow, 'ov_preview2_container'); const preview3Container = AddDiv(previewRow, 'ov_preview3_container'); - + const previewContainers = [preview1Container, preview2Container, preview3Container]; - + snapshotManager.initializePreviewImages(previewContainers); const nextButton = AddDiv(leftContainer, 'ov_button ov_next_button', Loc('Next')); @@ -327,24 +330,93 @@ function createDialogManager(snapshotManager) { } function showDialog() { + const overlay = createModalOverlay(); + document.body.appendChild(overlay); + const dialog = new ButtonDialog(); const contentDiv = dialog.Init(Loc('Share'), [ { name: Loc('Close'), - onClick: () => dialog.Close() + onClick() { + dialog.Close(); + removeOverlayIfExists(overlay); + } } ]); createMultiStepForm(contentDiv); const originalClose = dialog.Close.bind(dialog); - dialog.Close = () => { + dialog.Close = function() { snapshotManager.cleanup(); + removeOverlayIfExists(overlay); originalClose(); }; + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + dialog.Close(); + } + }); + dialog.Open(); + + setTimeout(() => { + styleDialogForSharing(dialog); + }, 0); + } + + function createModalOverlay() { + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 9998; // Ensure this is below the dialog but above everything else + `; + return overlay; + } + + function styleDialogForSharing(dialog) { + if (!dialog) { + console.error('Invalid dialog object'); + return; + } + + // Try to find the dialog element + let dialogElement = null; + if (dialog.GetContentDiv) { + dialogElement = dialog.GetContentDiv().closest('.ov_dialog'); + } + if (!dialogElement && dialog.dialogDiv) { + dialogElement = dialog.dialogDiv; + } + if (!dialogElement) { + console.error('Cannot find dialog element'); + return; + } + + console.log('Styling dialog element:', dialogElement); + + dialogElement.style.position = 'fixed'; + dialogElement.style.top = '50%'; + dialogElement.style.left = '50%'; + dialogElement.style.transform = 'translate(-50%, -50%)'; + dialogElement.style.zIndex = '9999'; + dialogElement.style.maxWidth = '90%'; + dialogElement.style.maxHeight = '90%'; + dialogElement.style.overflow = 'auto'; + } + + function removeOverlayIfExists(overlay) { + if (overlay && overlay.parentNode === document.body) { + document.body.removeChild(overlay); + } } return { showDialog }; -} \ No newline at end of file +} From ea2b6bfa6952f999775307266145d864f2695a52 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Tue, 9 Jul 2024 15:51:30 +0800 Subject: [PATCH 14/18] savepoint for implementing pdf feature --- package.json | 6 ++- source/website/pdfGenerator.js | 93 +++++++++++++++++++++++++++++++++ source/website/sharingdialog.js | 27 ++++++++-- website/index.html | 3 ++ 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 source/website/pdfGenerator.js diff --git a/package.json b/package.json index c81b30cf..7d1d6e45 100644 --- a/package.json +++ b/package.json @@ -48,17 +48,18 @@ "create_dist_test": "npm run create_package_test && npm run lint && npm run test", "create_package": "npm run generate_docs && npm run build_engine && npm run build_engine_module && npm run build_website && run-python3 tools/create_package.py", "create_package_test": "npm run generate_docs && npm run build_engine && npm run build_engine_module && npm run build_website && run-python3 tools/create_package.py test", - "generate_docs": "run-python3 tools/generate_docs.py", + "copy-pdfmake": "node -e \"const fs = require('fs'); const path = require('path'); console.log('Current directory:', process.cwd()); const sources = ['node_modules/pdfmake/build/pdfmake.min.js', 'node_modules/pdfmake/build/vfs_fonts.js']; const dest = 'build/website_dev/'; if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } sources.forEach(source => { const sourcePath = path.resolve(__dirname, source); const destPath = path.join(dest, path.basename(source)); if (fs.existsSync(sourcePath)) { fs.copyFileSync(sourcePath, destPath); console.log(`Copied ${sourcePath} to ${destPath}`); } else { console.error('Source file not found:', sourcePath); } });\"", "generate_docs": "run-python3 tools/generate_docs.py", "build_dev": "npm run build_engine_dev && npm run build_website_dev", "build_engine_dev": "npm run update_engine_exports && esbuild source/engine/main.js --bundle --minify --global-name=OV --sourcemap --outfile=build/engine_dev/o3dv.min.js", "build_engine": "npm run update_engine_exports && esbuild source/engine/main.js --bundle --minify --global-name=OV --outfile=build/engine/o3dv.min.js", "build_engine_module": "npm run update_engine_exports && rollup --config tools/rollup.js && tsc --project tools/tsconfig.json", - "build_website_dev": "esbuild source/website/index.js --bundle --minify --global-name=OV --sourcemap --loader:.ttf=file --loader:.woff=file --loader:.svg=file --outfile=build/website_dev/o3dv.website.min.js", + "build_website_dev": "npm run copy-pdfmake && esbuild source/website/index.js --bundle --minify --global-name=OV --sourcemap --loader:.ttf=file --loader:.woff=file --loader:.svg=file --outfile=build/website_dev/o3dv.website.min.js", "build_website": "esbuild source/website/index.js --bundle --minify --global-name=OV --loader:.ttf=file --loader:.woff=file --loader:.svg=file --outfile=build/website/o3dv.website.min.js", "update_engine_exports": "run-python3 tools/update_engine_exports.py" }, "devDependencies": { "@types/node": "^20.1.0", + "cpy-cli": "^5.0.0", "esbuild": "^0.20.0", "eslint": "^8.29.0", "eslint-plugin-unused-imports": "^3.0.0", @@ -76,6 +77,7 @@ "dependencies": { "@simonwep/pickr": "1.9.0", "fflate": "0.8.2", + "pdfmake": "^0.2.10", "three": "0.163.0" }, "eslintConfig": { diff --git a/source/website/pdfGenerator.js b/source/website/pdfGenerator.js new file mode 100644 index 00000000..e36c9a36 --- /dev/null +++ b/source/website/pdfGenerator.js @@ -0,0 +1,93 @@ +import pdfMake from 'pdfmake/build/pdfmake'; +import pdfFonts from 'pdfmake/build/vfs_fonts'; +import { AddDiv, AddDomElement } from '../engine/viewer/domutils.js'; + +pdfMake.vfs = pdfFonts.pdfMake.vfs; + +function AddButton(parentElement, text, className, onClick) { + const button = AddDomElement(parentElement, 'button', className); + button.textContent = text; + button.addEventListener('click', onClick); + return button; +} + + +export function generatePdf(data) { + const { name, email, description, tags, intensity, duration, images, siteUrl } = data; + const date = new Date().toLocaleDateString(); + + const docDefinition = { + content: [ + { text: 'Pain Snapshot Report', style: 'header' }, + { text: `Generated on: ${date}`, style: 'subheader' }, + { text: `Name: ${name}`, style: 'subheader' }, + email ? { text: `Email: ${email}`, style: 'subheader' } : {}, + description ? { text: `Description: ${description}`, style: 'subheader' } : {}, + tags ? { text: `Tags: ${tags}`, style: 'subheader' } : {}, + { text: `Pain Intensity: ${intensity}`, style: 'subheader' }, + { text: `Pain Duration: ${duration}`, style: 'subheader' }, + { text: 'Snapshots', style: 'header', margin: [0, 20, 0, 10] }, + ...images.map((image, index) => ({ + image, + width: 500, + height: 375, + margin: [0, 10, 0, 10], + caption: `Snapshot ${index + 1}` + })), + { text: `Visit us at: ${siteUrl}`, style: 'footer', link: siteUrl } + ], + styles: { + header: { + fontSize: 22, + bold: true, + margin: [0, 0, 0, 10] + }, + subheader: { + fontSize: 16, + margin: [0, 5, 0, 5] + }, + footer: { + fontSize: 14, + margin: [0, 20, 0, 0], + color: 'blue' + } + } + }; + + pdfMake.createPdf(docDefinition).download('Pain_Snapshot_Report.pdf'); +} + +export function addPdfGenerationSection(parentDiv, modelFiles, siteUrl) { + let pdfSection = AddDiv(parentDiv, 'ov_dialog_section'); + AddDiv(pdfSection, 'ov_dialog_inner_title', 'Generate PDF'); + + // Create form fields for optional data + const nameInput = AddDomElement(pdfSection, 'input', null); + nameInput.setAttribute('type', 'text'); + nameInput.setAttribute('placeholder', 'Name (required)'); + nameInput.required = true; + + const emailInput = AddDomElement(pdfSection, 'input', null); + emailInput.setAttribute('type', 'email'); + emailInput.setAttribute('placeholder', 'Email (optional)'); + + const descriptionInput = AddDomElement(pdfSection, 'textarea', null); + descriptionInput.setAttribute('placeholder', 'Description (optional)'); + + const tagsInput = AddDomElement(pdfSection, 'input', null); + tagsInput.setAttribute('type', 'text'); + tagsInput.setAttribute('placeholder', 'Tags (optional)'); + + // Add button to generate PDF + AddButton(pdfSection, 'Generate PDF', 'ov_button', () => { + const data = { + name: nameInput.value, + email: emailInput.value, + description: descriptionInput.value, + tags: tagsInput.value, + images: modelFiles, // Assuming you have a way to convert modelFiles to base64-encoded image data + siteUrl + }; + generatePdf(data); + }); +} diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 0dd4cca2..6a16418a 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -4,6 +4,7 @@ import { ShowMessageDialog } from './dialogs.js'; import { ButtonDialog } from './dialog.js'; import { HandleEvent } from './eventhandler.js'; import { Loc } from '../engine/core/localization.js'; +import { generatePdf, addPdfGenerationSection } from './pdfGenerator.js'; const CONFIG = { @@ -240,7 +241,7 @@ function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOff viewer.navigation.MoveCamera(camera, 0); return imageDataUrl; -} + } function createDialogManager(snapshotManager) { function createMultiStepForm(parentDiv) { @@ -264,6 +265,9 @@ function createDialogManager(snapshotManager) { AddDiv(leftContainer, 'ov_dialog_title', Loc('Share Snapshot')); AddDiv(leftContainer, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); + const generatePdfButton = AddDiv(step, 'ov_button ov_generate_pdf_button', Loc('Generate PDF')); + generatePdfButton.addEventListener('click', () => handleGeneratePdf(intensityInput, durationInput)); + const emailFieldsContainer = AddDiv(leftContainer, 'ov_email_fields_container'); for (let i = 0; i < 3; i++) { AddDiv(emailFieldsContainer, 'ov_dialog_label', Loc(`Email ${i + 1}`)); @@ -294,17 +298,34 @@ function createDialogManager(snapshotManager) { function createStep2Content(step) { AddDiv(step, 'ov_dialog_title', Loc('Additional Options')); - + AddCheckbox(step, 'send_to_self', Loc('Send to myself'), false, () => {}); AddCheckbox(step, 'download_snapshot', Loc('Download snapshot and info'), false, () => {}); - + const intensityInput = createInputField(step, 'number', Loc('Pain Intensity'), 'Enter pain intensity (1-10)', { min: 1, max: 10 }); const durationInput = createInputField(step, 'text', Loc('Pain Duration'), 'Enter pain duration (e.g., 2 hours, 3 days)'); + + // Add PDF generation button const submitButton = AddDiv(step, 'ov_button ov_submit_button', Loc('Submit')); submitButton.addEventListener('click', () => handleSubmit(intensityInput, durationInput)); } + function handleGeneratePdf(intensityInput, durationInput) { + const snapshots = [1, 2, 3].map(i => snapshotManager.captureSnapshot(i - 1)); + const data = { + name: document.querySelector('input[placeholder="Name (required)"]').value, + email: document.querySelector('input[placeholder="Email (optional)"]').value, + description: document.querySelector('textarea[placeholder="Description (optional)"]').value, + tags: document.querySelector('input[placeholder="Tags (optional)"]').value, + intensity: intensityInput.value, + duration: durationInput.value, + images: snapshots, + siteUrl: window.location.origin + }; + generatePdf(data); + } + function createInputField(container, type, labelText, placeholder, attributes = {}) { AddDiv(container, 'ov_dialog_label', labelText); const input = AddDomElement(container, 'input', null); diff --git a/website/index.html b/website/index.html index 512deeb0..f695f1a3 100644 --- a/website/index.html +++ b/website/index.html @@ -17,8 +17,11 @@ + + + From 4051bd2a56055c225122003de1b7d7d0698839e9 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Tue, 9 Jul 2024 22:33:52 +0800 Subject: [PATCH 15/18] adding pdf generation and download button. --- source/website/css/sharingdialog.css | 67 ++++++++++++++++--------- source/website/pdfGenerator.js | 66 ++++++++++++++++++------ source/website/sharingdialog.js | 75 ++++++++++++++++++++-------- 3 files changed, 150 insertions(+), 58 deletions(-) diff --git a/source/website/css/sharingdialog.css b/source/website/css/sharingdialog.css index bfb4e925..d1b34e29 100644 --- a/source/website/css/sharingdialog.css +++ b/source/website/css/sharingdialog.css @@ -3,16 +3,16 @@ background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - max-height: 80vh; /* Ensure the form doesn't extend beyond 80% of the viewport height */ - overflow-y: auto; /* Enable scrolling if content overflows */ - width: 90vw; /* Make the form wider */ - max-width: 1200px; /* Limit the max width */ - margin: auto; /* Center the form horizontally */ + max-height: 80vh; + overflow-y: auto; + width: 90vw; + max-width: 1200px; + margin: auto; } .ov_dialog_step { display: flex; - flex-direction: row; /* Change direction to row to place elements side by side */ + flex-direction: row; gap: 15px; } @@ -30,8 +30,9 @@ .ov_dialog_label { font-size: 1em; - margin-bottom: 5px; + margin-right: 10px; color: #333; + flex: 0 0 150px; /* Adjust the width as needed */ } .ov_dialog_input { @@ -52,8 +53,8 @@ border: 1px solid #ccc; max-width: 100%; height: auto; - border-radius: 4px; /* Match the border radius of other elements */ - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Add a subtle shadow for better visual separation */ + border-radius: 4px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .ov_button { @@ -83,7 +84,7 @@ padding: 20px; display: flex; flex-direction: column; - gap: 20px; /* Add space between preview containers */ + gap: 20px; } .ov_step1, .ov_step2 { @@ -96,30 +97,30 @@ .ov_preview_container { display: flex; - flex-direction: column; /* Ensure images are stacked correctly */ - gap: 10px; /* Add space between preview containers */ + flex-direction: column; + gap: 10px; } .ov_preview1_container { width: 100%; display: flex; - justify-content: center; /* Center the image horizontally */ - align-items: center; /* Center the image vertically */ + justify-content: center; + align-items: center; } .ov_preview_row { display: flex; width: 100%; - gap: 10px; /* Add space between side-by-side previews */ + gap: 10px; } .ov_preview2_container, .ov_preview3_container { - flex: 1; /* Each takes up equal space in the row */ - height: 150px; /* Adjust height for better layout */ + flex: 1; + height: 150px; display: flex; - justify-content: center; /* Center the image horizontally */ - align-items: center; /* Center the image vertically */ - overflow: hidden; /* Ensure no overflow */ + justify-content: center; + align-items: center; + overflow: hidden; } .ov_preview2_container, @@ -131,9 +132,9 @@ width: 100%; height: 100%; object-fit: contain; - border: 1px solid #ddd; /* Add border to preview images */ - border-radius: 4px; /* Match the border radius of other elements */ - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Add a subtle shadow for better visual separation */ + border: 1px solid #ddd; + border-radius: 4px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .ov_next_button, .ov_submit_button { @@ -150,3 +151,23 @@ .ov_next_button:hover, .ov_submit_button:hover { background-color: #0056b3; } + +.ov_info_fields_container { + margin-bottom: 20px; + display: flex; + flex-direction: column; + gap: 15px; +} + +.ov_email_fields_container { + margin-bottom: 20px; + display: flex; + flex-direction: column; + gap: 15px; +} + +.ov_input_wrapper { + display: flex; + align-items: center; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/source/website/pdfGenerator.js b/source/website/pdfGenerator.js index e36c9a36..c5569f81 100644 --- a/source/website/pdfGenerator.js +++ b/source/website/pdfGenerator.js @@ -11,11 +11,29 @@ function AddButton(parentElement, text, className, onClick) { return button; } - export function generatePdf(data) { const { name, email, description, tags, intensity, duration, images, siteUrl } = data; const date = new Date().toLocaleDateString(); + // Ensure images are correctly formatted and include a border + const imageObjects = images.map((image, index) => ({ + stack: [ + { + canvas: [ + { type: 'rect', x: 0, y: 0, w: 250, h: 188, r: 5, lineWidth: 1, lineColor: '#000000' } + ] + }, + { + image: image.startsWith('data:image/') ? image : 'data:image/png;base64,' + image, + width: 250, + height: 188, + margin: [0, -188, 0, 10], + alignment: 'center' + } + ], + margin: [0, 10] + })); + const docDefinition = { content: [ { text: 'Pain Snapshot Report', style: 'header' }, @@ -24,33 +42,51 @@ export function generatePdf(data) { email ? { text: `Email: ${email}`, style: 'subheader' } : {}, description ? { text: `Description: ${description}`, style: 'subheader' } : {}, tags ? { text: `Tags: ${tags}`, style: 'subheader' } : {}, - { text: `Pain Intensity: ${intensity}`, style: 'subheader' }, - { text: `Pain Duration: ${duration}`, style: 'subheader' }, + intensity ? { text: `Pain Intensity: ${intensity}`, style: 'subheader' } : {}, + duration ? { text: `Pain Duration: ${duration}`, style: 'subheader' } : {}, { text: 'Snapshots', style: 'header', margin: [0, 20, 0, 10] }, - ...images.map((image, index) => ({ - image, - width: 500, - height: 375, - margin: [0, 10, 0, 10], - caption: `Snapshot ${index + 1}` - })), + { + columns: [ + { stack: [imageObjects[0]], width: '100%' } + ], + columnGap: 10, + margin: [0, 20, 0, 10] + }, + { + columns: [ + { stack: [imageObjects[1]], width: '50%' }, + { stack: [imageObjects[2]], width: '50%' } + ], + columnGap: 10 + }, { text: `Visit us at: ${siteUrl}`, style: 'footer', link: siteUrl } ], styles: { header: { - fontSize: 22, + fontSize: 28, bold: true, - margin: [0, 0, 0, 10] + margin: [0, 0, 0, 10], + alignment: 'center', + color: '#2E86C1' }, subheader: { fontSize: 16, - margin: [0, 5, 0, 5] + margin: [0, 5, 0, 5], + color: '#34495E' }, footer: { fontSize: 14, margin: [0, 20, 0, 0], - color: 'blue' + color: '#2980B9', + alignment: 'center' + }, + imageBorder: { + border: [1, 1, 1, 1], + borderColor: '#000' } + }, + defaultStyle: { + font: 'Roboto' } }; @@ -90,4 +126,4 @@ export function addPdfGenerationSection(parentDiv, modelFiles, siteUrl) { }; generatePdf(data); }); -} +} \ No newline at end of file diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 6a16418a..17ffbe19 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -260,35 +260,63 @@ function createDialogManager(snapshotManager) { return step; } + function createLabeledInput(container, type, labelText, placeholder, attributes = {}) { + const wrapper = AddDiv(container, 'ov_input_wrapper'); + const label = AddDomElement(wrapper, 'label', 'ov_dialog_label'); + label.textContent = labelText; + let input; + if (type === 'textarea') { + input = AddDomElement(wrapper, 'textarea', 'ov_dialog_input'); + } else { + input = AddDomElement(wrapper, 'input', 'ov_dialog_input'); + input.type = type; + } + input.placeholder = placeholder; + Object.entries(attributes).forEach(([key, value]) => input.setAttribute(key, value)); + return input; + } + function createStep1Content(step) { const leftContainer = AddDiv(step, 'ov_left_container'); AddDiv(leftContainer, 'ov_dialog_title', Loc('Share Snapshot')); AddDiv(leftContainer, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); - - const generatePdfButton = AddDiv(step, 'ov_button ov_generate_pdf_button', Loc('Generate PDF')); - generatePdfButton.addEventListener('click', () => handleGeneratePdf(intensityInput, durationInput)); + // Info fields container + const infoFieldsContainer = AddDiv(leftContainer, 'ov_info_fields_container'); + + // Name input field + const nameInput = createLabeledInput(infoFieldsContainer, 'text', Loc('Name'), 'John Doe'); + + const intensityInput = createLabeledInput(infoFieldsContainer, 'number', Loc('Pain Intensity'), 'Enter pain intensity (1-10)', { min: 1, max: 10 }); + const durationInput = createLabeledInput(infoFieldsContainer, 'text', Loc('Pain Duration'), 'Enter pain duration (e.g., 2 hours, 3 days)'); + + // Description and Tags input fields (optional) + const descriptionInput = createLabeledInput(infoFieldsContainer, 'textarea', Loc('Description'), 'Description (optional)'); + const tagsInput = createLabeledInput(infoFieldsContainer, 'text', Loc('Tags'), 'Tags (optional)'); + + // Email fields container const emailFieldsContainer = AddDiv(leftContainer, 'ov_email_fields_container'); for (let i = 0; i < 3; i++) { - AddDiv(emailFieldsContainer, 'ov_dialog_label', Loc(`Email ${i + 1}`)); const emailInput = AddDomElement(emailFieldsContainer, 'input', `email${i}`); emailInput.type = 'email'; emailInput.className = 'ov_dialog_input'; - emailInput.placeholder = Loc('Enter email address'); + emailInput.placeholder = Loc(`Enter email ${i + 1}`); } - + const rightContainer = AddDiv(step, 'ov_right_container'); const previewContainer = AddDiv(rightContainer, 'ov_preview_container'); - + const preview1Container = AddDiv(previewContainer, 'ov_preview1_container'); const previewRow = AddDiv(previewContainer, 'ov_preview_row'); const preview2Container = AddDiv(previewRow, 'ov_preview2_container'); const preview3Container = AddDiv(previewRow, 'ov_preview3_container'); - + const previewContainers = [preview1Container, preview2Container, preview3Container]; - snapshotManager.initializePreviewImages(previewContainers); - + + const generatePdfButton = AddDiv(leftContainer, 'ov_button ov_generate_pdf_button', Loc('Generate PDF')); + generatePdfButton.addEventListener('click', () => handleGeneratePdf(intensityInput, durationInput, nameInput)); + const nextButton = AddDiv(leftContainer, 'ov_button ov_next_button', Loc('Next')); nextButton.addEventListener('click', () => { step.style.display = 'none'; @@ -302,27 +330,34 @@ function createDialogManager(snapshotManager) { AddCheckbox(step, 'send_to_self', Loc('Send to myself'), false, () => {}); AddCheckbox(step, 'download_snapshot', Loc('Download snapshot and info'), false, () => {}); - const intensityInput = createInputField(step, 'number', Loc('Pain Intensity'), 'Enter pain intensity (1-10)', { min: 1, max: 10 }); - const durationInput = createInputField(step, 'text', Loc('Pain Duration'), 'Enter pain duration (e.g., 2 hours, 3 days)'); - - // Add PDF generation button const submitButton = AddDiv(step, 'ov_button ov_submit_button', Loc('Submit')); submitButton.addEventListener('click', () => handleSubmit(intensityInput, durationInput)); } - function handleGeneratePdf(intensityInput, durationInput) { + function handleGeneratePdf(intensityInput, durationInput, nameInput) { + console.log('Generating PDF...'); const snapshots = [1, 2, 3].map(i => snapshotManager.captureSnapshot(i - 1)); + const descriptionInput = document.querySelector('textarea[placeholder="Description (optional)"]'); + const description = descriptionInput ? descriptionInput.value : ''; + const data = { - name: document.querySelector('input[placeholder="Name (required)"]').value, - email: document.querySelector('input[placeholder="Email (optional)"]').value, - description: document.querySelector('textarea[placeholder="Description (optional)"]').value, + name: nameInput.value || 'John Doe', // Use 'John Doe' if the field is empty + email: document.querySelector('input[placeholder*="Enter email"]').value, + description: description, tags: document.querySelector('input[placeholder="Tags (optional)"]').value, - intensity: intensityInput.value, - duration: durationInput.value, images: snapshots, siteUrl: window.location.origin }; + + // Add intensity and duration only if the inputs exist and have values + if (intensityInput && intensityInput.value) { + data.intensity = intensityInput.value; + } + if (durationInput && durationInput.value) { + data.duration = durationInput.value; + } + generatePdf(data); } From 7d6404bd1dfa9656ceada0133570197cb2d19fcf Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Mon, 15 Jul 2024 20:06:48 +0800 Subject: [PATCH 16/18] pdf download format checkpoint --- source/website/pdfGenerator.js | 296 +++++++++++++++++++------------- source/website/sharingdialog.js | 117 +++++-------- 2 files changed, 220 insertions(+), 193 deletions(-) diff --git a/source/website/pdfGenerator.js b/source/website/pdfGenerator.js index c5569f81..22e447a2 100644 --- a/source/website/pdfGenerator.js +++ b/source/website/pdfGenerator.js @@ -1,129 +1,183 @@ -import pdfMake from 'pdfmake/build/pdfmake'; -import pdfFonts from 'pdfmake/build/vfs_fonts'; -import { AddDiv, AddDomElement } from '../engine/viewer/domutils.js'; +import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; -pdfMake.vfs = pdfFonts.pdfMake.vfs; +async function generatePdf(data) { + const { name, email, age, gender, typeOfPain, date, images } = data; -function AddButton(parentElement, text, className, onClick) { - const button = AddDomElement(parentElement, 'button', className); - button.textContent = text; - button.addEventListener('click', onClick); - return button; + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage([595.28, 841.89]); // A4 size + + const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); + const helveticaBoldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + + const darkBlue = rgb(0.1, 0.2, 0.4); + const lightBlue = rgb(0.8, 0.9, 1); + + // Background + page.drawRectangle({ + x: 0, + y: 0, + width: 595.28, + height: 841.89, + color: lightBlue, + }); + + // Fetch and embed PNG image + const pngImage = await fetch('assets/images/tmwihn.png').then(res => res.arrayBuffer()); + const image = await pdfDoc.embedPng(pngImage); + const pngScale = 0.25; // Scale down the image by 50% + const pngDims = image.scale(pngScale); + const imageX = (page.getWidth() - pngDims.width) / 2; + const imageY = page.getHeight() - pngDims.height - 30; // 50 pixels from the top + + // Draw the PNG image + page.drawImage(image, { + x: imageX, + y: imageY, + width: pngDims.width, + height: pngDims.height, + }); + + //Lets check first if helveticaBoldFont and helveticaFont are loaded + if (!helveticaBoldFont || !helveticaFont) { + throw new Error('One or more fonts failed to load'); + } + + // Header + // drawCenteredText(page, 'TellMewhereithurtsnow', 800, helveticaBoldFont, 24, darkBlue); + drawCenteredText(page, 'Tell people about your pain', 1500, helveticaFont, 12, darkBlue); + + // Introduction text (justified and indented) + const introStartY = imageY - 50; + const introWidth = 495; + const lineHeight = 20; + const indent = 20; + const introText = `Hi ${email || ''},`; + + const wrappedText = wrapText(introText, helveticaBoldFont, 12, introWidth); + wrappedText.forEach((line, index) => { + const y = introStartY - index * lineHeight; + const x = (595.28 - introWidth) / 2; + page.drawText(line, { x: x + (index === 0 ? indent : 0), y, size: 12, font: helveticaBoldFont, color: darkBlue }); + }); + + // Adjust introBody width for narrower justification + const introBodyWidth = 450; // Adjust this value as needed + const introBody = ` Your acquaintance, ${name || ''}, has shared with you a snapshot describing their pain and where they are feeling it. Pain can significantly impact one's quality of life. Understanding its location and intensity helps in diagnosing and managing the underlying causes effectively.`; + const wrappedBody = wrapText(introBody, helveticaFont, 12, introBodyWidth); + wrappedBody.forEach((line, index) => { + const y = introStartY - (wrappedText.length + index) * lineHeight - 10; + const x = (595.28 - introBodyWidth) / 2; + page.drawText(line, { x: x, y, size: 12, font: helveticaFont, color: darkBlue }); + }); + + // Separator + const separatorY = introStartY - wrappedText.length * lineHeight - 90; + page.drawLine({ + start: { x: 50, y: separatorY }, + end: { x: 545, y: separatorY }, + thickness: 1, + color: darkBlue, + }); + + // Patient information + const infoStartY = separatorY - 20; + const infoGap = 15; + const patientInfo = [ + `Date of record: ${date || ''}`, + `Patient Name: ${name || ''}`, + `Age: ${age || ''}`, + `Gender: ${gender || ''}`, + `Type of Pain: ${typeOfPain || ''}` + ]; + + patientInfo.forEach((text, index) => { + page.drawText(text, { x: 50, y: infoStartY - index * infoGap, size: 10, font: helveticaFont, color: darkBlue }); + }); + + // Images (lowered position) + const imageStartY = infoStartY - patientInfo.length * infoGap - 390; + const mainImageWidth = 320; + const mainImageHeight = 380; + const smallImageWidth = 160; + const smallImageHeight = 185; + + page.drawRectangle({ x: 50, y: imageStartY, width: mainImageWidth, height: mainImageHeight, borderColor: darkBlue, borderWidth: 2 }); + page.drawRectangle({ x: 385, y: imageStartY + smallImageHeight + 10, width: smallImageWidth, height: smallImageHeight, borderColor: darkBlue, borderWidth: 2 }); + page.drawRectangle({ x: 385, y: imageStartY, width: smallImageWidth, height: smallImageHeight, borderColor: darkBlue, borderWidth: 2 }); + + // Footer with centered "hyperlink-like" text + const url = 'www.tellmewhereithurtsnow.com'; + drawCenteredText(page, url, 30, helveticaBoldFont, 14, rgb(0, 0, 1)); // Using blue color for link-like appearance + + // Save and download PDF + const pdfBytes = await pdfDoc.save(); + const blob = new Blob([pdfBytes], { type: 'application/pdf' }); + const downloadUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = downloadUrl; + a.download = 'pain_snapshot_report.pdf'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); } -export function generatePdf(data) { - const { name, email, description, tags, intensity, duration, images, siteUrl } = data; - const date = new Date().toLocaleDateString(); - - // Ensure images are correctly formatted and include a border - const imageObjects = images.map((image, index) => ({ - stack: [ - { - canvas: [ - { type: 'rect', x: 0, y: 0, w: 250, h: 188, r: 5, lineWidth: 1, lineColor: '#000000' } - ] - }, - { - image: image.startsWith('data:image/') ? image : 'data:image/png;base64,' + image, - width: 250, - height: 188, - margin: [0, -188, 0, 10], - alignment: 'center' - } - ], - margin: [0, 10] - })); - - const docDefinition = { - content: [ - { text: 'Pain Snapshot Report', style: 'header' }, - { text: `Generated on: ${date}`, style: 'subheader' }, - { text: `Name: ${name}`, style: 'subheader' }, - email ? { text: `Email: ${email}`, style: 'subheader' } : {}, - description ? { text: `Description: ${description}`, style: 'subheader' } : {}, - tags ? { text: `Tags: ${tags}`, style: 'subheader' } : {}, - intensity ? { text: `Pain Intensity: ${intensity}`, style: 'subheader' } : {}, - duration ? { text: `Pain Duration: ${duration}`, style: 'subheader' } : {}, - { text: 'Snapshots', style: 'header', margin: [0, 20, 0, 10] }, - { - columns: [ - { stack: [imageObjects[0]], width: '100%' } - ], - columnGap: 10, - margin: [0, 20, 0, 10] - }, - { - columns: [ - { stack: [imageObjects[1]], width: '50%' }, - { stack: [imageObjects[2]], width: '50%' } - ], - columnGap: 10 - }, - { text: `Visit us at: ${siteUrl}`, style: 'footer', link: siteUrl } - ], - styles: { - header: { - fontSize: 28, - bold: true, - margin: [0, 0, 0, 10], - alignment: 'center', - color: '#2E86C1' - }, - subheader: { - fontSize: 16, - margin: [0, 5, 0, 5], - color: '#34495E' - }, - footer: { - fontSize: 14, - margin: [0, 20, 0, 0], - color: '#2980B9', - alignment: 'center' - }, - imageBorder: { - border: [1, 1, 1, 1], - borderColor: '#000' - } - }, - defaultStyle: { - font: 'Roboto' +// Helper function to wrap text +function wrapText(text, font, fontSize, maxWidth) { + const words = text.split(' '); + const lines = []; + let currentLine = words[0]; + + for (let i = 1; i < words.length; i++) { + const word = words[i]; + const width = font.widthOfTextAtSize(currentLine + " " + word, fontSize); + if (width < maxWidth) { + currentLine += " " + word; + } else { + lines.push(currentLine); + currentLine = word; } - }; + } + lines.push(currentLine); + return lines; +} - pdfMake.createPdf(docDefinition).download('Pain_Snapshot_Report.pdf'); +// Function to convert base64 to Uint8Array +function base64ToUint8Array(base64) { + const binaryString = window.atob(base64.split(',')[1]); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; } -export function addPdfGenerationSection(parentDiv, modelFiles, siteUrl) { - let pdfSection = AddDiv(parentDiv, 'ov_dialog_section'); - AddDiv(pdfSection, 'ov_dialog_inner_title', 'Generate PDF'); - - // Create form fields for optional data - const nameInput = AddDomElement(pdfSection, 'input', null); - nameInput.setAttribute('type', 'text'); - nameInput.setAttribute('placeholder', 'Name (required)'); - nameInput.required = true; - - const emailInput = AddDomElement(pdfSection, 'input', null); - emailInput.setAttribute('type', 'email'); - emailInput.setAttribute('placeholder', 'Email (optional)'); - - const descriptionInput = AddDomElement(pdfSection, 'textarea', null); - descriptionInput.setAttribute('placeholder', 'Description (optional)'); - - const tagsInput = AddDomElement(pdfSection, 'input', null); - tagsInput.setAttribute('type', 'text'); - tagsInput.setAttribute('placeholder', 'Tags (optional)'); - - // Add button to generate PDF - AddButton(pdfSection, 'Generate PDF', 'ov_button', () => { - const data = { - name: nameInput.value, - email: emailInput.value, - description: descriptionInput.value, - tags: tagsInput.value, - images: modelFiles, // Assuming you have a way to convert modelFiles to base64-encoded image data - siteUrl - }; - generatePdf(data); - }); -} \ No newline at end of file +// Function to draw centered text +function drawCenteredText(page, text, y, font, size, color) { + const textWidth = font.widthOfTextAtSize(text, size); + const pageWidth = page.getWidth(); + const x = (pageWidth - textWidth) / 2; + page.drawText(text, { x, y, size, font, color }); +} + + + +// Sample data for testing +const sampleData = { + name: 'John Doe', + email: 'john.doe@example.com', + age: '30', + gender: 'Male', + typeOfPain: 'Chronic', + date: new Date().toLocaleDateString(), + images: [ + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', // Add your base64 image data + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', // Add your base64 image data + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...' // Add your base64 image data + ] +}; + +// Generate the PDF with the sample data +// generatePdf(sampleData); + +export { generatePdf }; diff --git a/source/website/sharingdialog.js b/source/website/sharingdialog.js index 17ffbe19..4ea00206 100644 --- a/source/website/sharingdialog.js +++ b/source/website/sharingdialog.js @@ -4,13 +4,12 @@ import { ShowMessageDialog } from './dialogs.js'; import { ButtonDialog } from './dialog.js'; import { HandleEvent } from './eventhandler.js'; import { Loc } from '../engine/core/localization.js'; -import { generatePdf, addPdfGenerationSection } from './pdfGenerator.js'; - +import { generatePdf } from './pdfGenerator.js'; const CONFIG = { SNAPSHOT_SIZES: { - LARGE: { width: 2000, height: 1080 }, - SMALL: { width: 1080, height: 540 } + LARGE: { width: 2000, height: 2160 }, + SMALL: { width: 1080, height: 1080 } }, INITIAL_ZOOM: 0.5, MAX_ZOOM: 3, @@ -21,6 +20,7 @@ const CONFIG = { }; export function ShowSharingDialog(settings, viewer) { + console.log("ShowSharingDialog called with settings:", settings); const SnapshotManager = createSnapshotManager(viewer, settings); const DialogManager = createDialogManager(SnapshotManager); DialogManager.showDialog(); @@ -39,6 +39,7 @@ function createSnapshotManager(viewer, settings) { let previewImages = []; function captureSnapshot(index) { + console.log(`Capturing snapshot for index: ${index}`); if (index < 0 || index >= cameras.length) { console.error(`Invalid index: ${index}`); return null; @@ -132,20 +133,6 @@ function createSnapshotManager(viewer, settings) { event.preventDefault(); } - function initializePreviewImages(containers) { - previewImages = containers.map((container, index) => { - const img = CreateDomElement('img', 'ov_snapshot_preview_image'); - container.appendChild(img); - img.addEventListener('wheel', (e) => handleMouseEvent(index, 'wheel', e), { passive: false }); - img.addEventListener('mousedown', (e) => handleMouseEvent(index, 'mousedown', e)); - img.addEventListener('contextmenu', (e) => e.preventDefault()); - return img; - }); - - // Update previews after initialization - previewImages.forEach((_, index) => updatePreview(index)); - } - function cleanup() { previewImages.forEach((img, index) => { img.removeEventListener('wheel', (e) => handleMouseEvent(index, 'wheel', e)); @@ -241,7 +228,7 @@ function CaptureSnapshot(viewer, width, height, isTransparent, zoomLevel, panOff viewer.navigation.MoveCamera(camera, 0); return imageDataUrl; - } +} function createDialogManager(snapshotManager) { function createMultiStepForm(parentDiv) { @@ -275,25 +262,25 @@ function createDialogManager(snapshotManager) { Object.entries(attributes).forEach(([key, value]) => input.setAttribute(key, value)); return input; } - + function createStep1Content(step) { const leftContainer = AddDiv(step, 'ov_left_container'); AddDiv(leftContainer, 'ov_dialog_title', Loc('Share Snapshot')); AddDiv(leftContainer, 'ov_dialog_description', Loc('Quickly share a snapshot and details of your pain location with family, friends, or therapists.')); - + // Info fields container const infoFieldsContainer = AddDiv(leftContainer, 'ov_info_fields_container'); - + // Name input field const nameInput = createLabeledInput(infoFieldsContainer, 'text', Loc('Name'), 'John Doe'); - + const intensityInput = createLabeledInput(infoFieldsContainer, 'number', Loc('Pain Intensity'), 'Enter pain intensity (1-10)', { min: 1, max: 10 }); const durationInput = createLabeledInput(infoFieldsContainer, 'text', Loc('Pain Duration'), 'Enter pain duration (e.g., 2 hours, 3 days)'); - + // Description and Tags input fields (optional) const descriptionInput = createLabeledInput(infoFieldsContainer, 'textarea', Loc('Description'), 'Description (optional)'); const tagsInput = createLabeledInput(infoFieldsContainer, 'text', Loc('Tags'), 'Tags (optional)'); - + // Email fields container const emailFieldsContainer = AddDiv(leftContainer, 'ov_email_fields_container'); for (let i = 0; i < 3; i++) { @@ -302,86 +289,72 @@ function createDialogManager(snapshotManager) { emailInput.className = 'ov_dialog_input'; emailInput.placeholder = Loc(`Enter email ${i + 1}`); } - + const rightContainer = AddDiv(step, 'ov_right_container'); const previewContainer = AddDiv(rightContainer, 'ov_preview_container'); - + const preview1Container = AddDiv(previewContainer, 'ov_preview1_container'); const previewRow = AddDiv(previewContainer, 'ov_preview_row'); const preview2Container = AddDiv(previewRow, 'ov_preview2_container'); const preview3Container = AddDiv(previewRow, 'ov_preview3_container'); - + const previewContainers = [preview1Container, preview2Container, preview3Container]; snapshotManager.initializePreviewImages(previewContainers); - - const generatePdfButton = AddDiv(leftContainer, 'ov_button ov_generate_pdf_button', Loc('Generate PDF')); - generatePdfButton.addEventListener('click', () => handleGeneratePdf(intensityInput, durationInput, nameInput)); - - const nextButton = AddDiv(leftContainer, 'ov_button ov_next_button', Loc('Next')); + + const generatePdfButton = AddDomElement(leftContainer, 'button', 'ov_button ov_generate_pdf_button'); + generatePdfButton.textContent = Loc('Generate PDF'); + generatePdfButton.addEventListener('click', () => handleGeneratePdf(nameInput, intensityInput, durationInput, descriptionInput, tagsInput, emailFieldsContainer)); + + const nextButton = AddDomElement(leftContainer, 'button', 'ov_button ov_next_button'); + nextButton.textContent = Loc('Next'); nextButton.addEventListener('click', () => { step.style.display = 'none'; step.nextElementSibling.style.display = 'block'; }); + + return { nameInput, intensityInput, durationInput, descriptionInput, tagsInput, emailFieldsContainer }; } function createStep2Content(step) { AddDiv(step, 'ov_dialog_title', Loc('Additional Options')); - + AddCheckbox(step, 'send_to_self', Loc('Send to myself'), false, () => {}); AddCheckbox(step, 'download_snapshot', Loc('Download snapshot and info'), false, () => {}); - const submitButton = AddDiv(step, 'ov_button ov_submit_button', Loc('Submit')); - submitButton.addEventListener('click', () => handleSubmit(intensityInput, durationInput)); + submitButton.addEventListener('click', () => handleSubmit()); } - function handleGeneratePdf(intensityInput, durationInput, nameInput) { + function handleGeneratePdf(nameInput, intensityInput, durationInput, descriptionInput, tagsInput, emailFieldsContainer) { console.log('Generating PDF...'); const snapshots = [1, 2, 3].map(i => snapshotManager.captureSnapshot(i - 1)); - const descriptionInput = document.querySelector('textarea[placeholder="Description (optional)"]'); const description = descriptionInput ? descriptionInput.value : ''; - + + const emails = []; + for (let i = 0; i < emailFieldsContainer.children.length; i++) { + const emailInput = emailFieldsContainer.children[i]; + if (emailInput.value) { + emails.push(emailInput.value); + } + } + const data = { name: nameInput.value || 'John Doe', // Use 'John Doe' if the field is empty - email: document.querySelector('input[placeholder*="Enter email"]').value, + email: emails.join(', ') || 'john_doe@gmail.com', description: description, - tags: document.querySelector('input[placeholder="Tags (optional)"]').value, + tags: tagsInput.value, + intensity: intensityInput.value, + duration: durationInput.value, images: snapshots, siteUrl: window.location.origin }; - - // Add intensity and duration only if the inputs exist and have values - if (intensityInput && intensityInput.value) { - data.intensity = intensityInput.value; - } - if (durationInput && durationInput.value) { - data.duration = durationInput.value; - } - - generatePdf(data); - } - function createInputField(container, type, labelText, placeholder, attributes = {}) { - AddDiv(container, 'ov_dialog_label', labelText); - const input = AddDomElement(container, 'input', null); - input.type = type; - input.className = 'ov_dialog_input'; - input.placeholder = Loc(placeholder); - Object.entries(attributes).forEach(([key, value]) => input.setAttribute(key, value)); - return input; + generatePdf(data); } - function handleSubmit(intensityInput, durationInput) { - const snapshots = [1, 2, 3].map(i => snapshotManager.captureSnapshot(i - 1)); - const info = { - intensity: intensityInput.value, - duration: durationInput.value, - }; - - // Here you would implement the actual sharing logic - console.log('Sharing snapshots:', snapshots); - console.log('Sharing info:', info); - + function handleSubmit() { + console.log('Submitting...'); + // Implement submission logic ShowMessageDialog(Loc('Success'), Loc('Your snapshot and information have been shared.')); } @@ -400,7 +373,7 @@ function createDialogManager(snapshotManager) { } ]); - createMultiStepForm(contentDiv); + const { step1, step2 } = createMultiStepForm(contentDiv); const originalClose = dialog.Close.bind(dialog); dialog.Close = function() { From b11320432816a2b47695ace685515eb983ec5840 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Tue, 16 Jul 2024 01:31:42 +0800 Subject: [PATCH 17/18] pdf finish feature --- assets/reports/pain_report.pdf | Bin 0 -> 23546 bytes source/website/pdfGenerator.js | 41 +++++++++++++++++++++++++ website/assets/reports/pain_report.pdf | Bin 0 -> 23546 bytes website/index.html | 2 -- 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 assets/reports/pain_report.pdf create mode 100644 website/assets/reports/pain_report.pdf diff --git a/assets/reports/pain_report.pdf b/assets/reports/pain_report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2dceba4c88ee3c67b9e80fd0702846804d242f12 GIT binary patch literal 23546 zcmcG#1wd9y*EUSIq|(X_2m%s!&>+%X(jnbQcbBA;ARrCW-Q6W2B_JU!f}nt)bO`^x zLEod#bDsFW_xp|q+%tP-&6<_7uG!Klii)#9SlO}Y4p+~Yupl5X$kxyji=Q8hO~u{L z1jHs{;ACKBYlg+9X5!#zVQT|ogO6rYG%z!91VMq}N?2^lP7cmSPAU#2CQ7!pP9WHI zubhFy6Db>0TM+woTh_qF48&k!!wAr_FmW|;P;@XcHE}SpF#;%YT#XSBz%sEhhBLqk zwEu?@3nwcR5QB<|m6e={t2r>m!pYp(!O79a))gQo;$ULnWa|K85HYZEF#tLg9c+!A zjZA*)xf%j6FKXZf5EA8uf}!kSE(jFD4&{V!-Uoy40sjLtpOlNmCTihmXJz0nYHOr&rOPXte+dJ=+dpZ*g@MJUWa4P+>;N|pxIF@DHBvTl z0%@}WHVa}?0r&^8Nm&CUg@67n^7C&gKyqSkP7=yaaD79-*IU<6Fth}=a79al69j?V zx)OXRY~r?XAzvFaye(&9Y+)d5>ju&Wvw}fftlUr@9&QK>#KFVC$^qi$0D8H2csQ^0 zW8(x!*bxN0{mm7wrR)d-{}T8wPf;;2bOf;}W3dTanV1;=S4*fkSXlp814SHc?f$EQ zO14*W0hklAv9Sf1*Vg$>p>W&zQyJF=CT3+~4cGyQ4bY;qHLz)5Z@*j}FgeJTm;X)( zZgIZ|$y!)jz!w5k0Z2{W+4|~P8*=6LAPC&sl|Yays|P`@tQQ2ivRDxGN_!yam9{|8 zD^bj4D!l0 zeod1#u`zQp2h0faD<=N5B)C@?I$b%hxUB;qoc}xa3cI#fxFHH(>l2U;4+lGl8xWm@ zgRQe22zG6|Y~mJHPC%Sv6So2kUDU+L*7(Xbfp}wLV0{D2E!8BVnc%4+4vM3y{jL6d zJdXk-!xTacNm>2oOENqH#FTBcvlzv_u$-qLRvEhY*;uoX^@75R+gUZN16iG#xj?X= zc|x2U1pDbH;yfVOwf26KI{e+QX+Nb7<>2N99P~f!b6Z;`no^43u%yRdzzzt1uXvdY zB4?aT#YF|4n^>ej2~_Laj3K7R1?j76fkW3i!Y)VD%S*nLJ^NCY+}n>4%@JKx)*s5& z=4Nb~`zzUjUh;S;MN9u;4=`m?jLwjCaxkFQUk^_Yr0@;cbgaM8PPI#Txv zi0FP0-h5z&cNu`8KB=eYHTC2B_H;8z#-oFShvWkg$FAI?g)LQ36u zI{4K(JMBp?O5Uo{S`fuaT_Z-)Hd`cR;abbFbHUe(DY8%fo<`N>li|YjM7#aW^zTE* zj3oUAn|Nf_I+O}c#TcL2;c;r(2HfTDaRiT;N*^fN@*TA71w+w4ib2Xv?mGt-Fotd z;7$m8G|B*`Y026XA{jOx)mE>QmAyH=d7+D9^~vFd?XRCITjzhAtWc{hJ7P?JRKFem zu++EV@q@Ox)*8-l#b3TrEL$NIV+fcR_dWc2sNU|m__&@P;Yo_J?~mqTXHG^5Dz77Z zj3!UF$p`vRHRRiTmqS`X-$0>pn-49^!U>ioSdU&_PCg!?$41#jT}euw}<~wJB^zmP;Z!%JP{AeTEii#~%Vsr@uIha>Fp`K?Pd^eC6#@8c7o7CI;c<9q|dG6wR>Bd^C$!l4hTx!}UHdCKK zT9nAvgh6mfowwGZGGD>kucF3ZQ{uPVFPpB=pG2kV+?L?)^Imh+`_NfQ_s;&o-bgNI zqssb^7d<-rmVK{J`%+%#i7tZ%L~j{qKODe(RL3~ben>aS$1#}yEKLYcCEA-5clC?i z%)_&)M^uxXd{i&)gIDJ}mN7YQp~$ZF%6UyTVc#MusVyssbPS~y@G)+k=4bN!6zP0Yd!Ka=n>Cz$2O2YF z(!Nk1ibl&yh!?uJP$LLB6I(x{I5T@_QSG5)Ie^RBRzKYL{^>a4WwiZM%A1Hi<6($j zD4@NLL2}Q>$q)9_BtrJ%>6$;<8MD*0*|ei?Q%yCzom<8+j4YacXWj^7wd-bHF3*ECL-c_jm~&2Z&4os)f(k+pJQ zjsBQ^fKlN~Z-k63w)lt=Mz*{Lndu_lnHd)yJ*ek9najLGp>JGII!+ii^dsv{)12pP zVME+RhXf{5-MEf6N*3$cjw8*moZHE;FCLtJ?O~r1-D~(S3)Upqw;n`-n?CvQe-G>K zzjc|-?xwUaXTJQAXUz<|e{NpxmGjX(Z(hFuqvksOSL^mV$y%-#3hD0Otup)Hvv!NF zO0n|gtPG^PYt*A^+9h>Q_JE+qHq}`ih&eR^VVU zJ;$2~KeT%G65b9-qDUN*hWl;q22#Y|_-gAW-`y)*NI`6VFNNMJ+oPN=>YlFag96SvxS82;+k{U2|=a=7RI!AFE^?{MM2WD7s- z$#hHPc7aOow1pZQC|6CmcdI1W$gHcY944ab<*R;eMPY6gla>y7)jcfiPRVjZXZ^s^ z`BA*w!sJX9M`je{@+6$59B*;@R=B|HQ|@HqgaDELF9(F`WLf$FR$_Ng-jQOdgy5}y zE2<9ef3@(DsbLwHL;9ToZp+tRZRwDLoH24G^p-`ixqVb;vvuv`5)n;@sJzc^Z%`(K zRSs{ZiPn6hZCi_?Aw{+!2dgLxHcf+6sK&@V9!Q^dJ$6}AIo)8Cxgmd>P4$Dj3UQW; z$akIasV~p9P;_5S3)?)0;$au0(pK3D>E@<=4^6)_Lfa!W|Hbk)y`WSz9rar$3nTi8 z)CxNho6Isy(plT^nm!laur#f_rO(Nv{(3BYmp6OP{p@AJgJP_Sh60l3gRiP?UNV*AJ>_3}|BegyJF?7{8=1%^ zPyIXO+X>qXq8rVat^V2!1ePDYJQolHjvcoY?Ct%VP2TS`3_Lo0BZe|<294+6ORKO6 zjoR_@+K8Q^sB9xIk;~GHSFIED3mc&xO*mA&pJe$=`G>11J&zT(O8fyzCKlgN<=*X8 zO&)QMN@NoewG9#?r?SsZi?J=!Y_Jk}^~<$SZX(4w0aYeh?QQ4{8lUFIe-yW9^F8n_ zk_di2N$V)0rLn@-p{57_GZBCF=O_J>cR*-QcWs zCsV?0^#c4&LL#9~cxzr^tH9n@n{$VbY?_%EMy8S0SHs%KvpEUYfc%LXJ4GpTv zr{^@8uIn+xZq670bb4qM!#y)CdG zFKjGSSv;0Mmy+FbYwGc?;c9I^ZOAa4ZXWOtu4htfoAi@@BK%3iJw;fDIsHPsX+lB3 zr^C;;9i6f^Nc_N@Z~6T*%s4fwh;*bfD&bQ<#7m)!1h1QM+a?`LTNgLJ3}D#(-SHHD z@ceboyslaQp~L~|x2y8zXNjO_2Kreyzzd!K7d6Xohv0uMsQ-aENf=OvfEXk#m_huG zz}Z{R$yU$A+Q7m}0DvCE{?&^=_5hTwzpeNO)^b(O{KJZhK&5EpDJK<>8AAOn!2je(t`IRFD0n>ZRdSQuIWsE?DmiG>5m&cMP3 zWME?qy25bao$eq2jsTgOm{`F_SvawR6yalx3~WG-7G^dUrWU}Xla)Kj!rIONDBW#s zOzt^?>;Xk?aKbGf`@G=#|Amtde&^^Ko9U^TL}zGnUB(;qZpztg-b;s3qA{b^v2oGpx=z_ku26wspy z+*JORN&zDP?C#pQ{!upo*6^2^Um*zpmW!W`b%g)|WF3Hd+QFC?#BU6+AOKkAmC=Lb z0iy>x|MUSNv)}swNJ;=C0l21#!*3(at~!1?>Hqsh{?G^eACmZI@bMq56hL?Z8@kdz zAPKlr0vmxt9eTE=dT>AfxsCrY;tw7;{@~&FTL16u>^jWxTfh-M$j|kDYyCl+=MUQd zF~0xQ^ffi;AGEJpf6#{fX-5BTe7+_Q`-AvZ>rdi;c=r{A^>1zTA40LKt1GJ$fQwuI zBQE}TP6^;gzq!Njp~}brz?j*DFk~N0Tdfm`&*Fzx#^LhE^6v$^dm1B@;7e zD+320>DmCu8$4gINf`r)5Xdwvk^txp+ z>k5_qgD>{~fkpwr!liU2(!UcIoIAJz94zeMcSGzy0n`6Vo&SUCib#pV>A+0^FmIrx zVk;pfDraE#m!hw_mE9ekOsuc(x_&~P>Ts-Eo0A(jX#sZwP~b*~or@KSfB>w`0n+B+ zWWQ=a0U%qO1IF{S!2>jSfI(0I8U@AxNfZR!PXUiyoWQ*UeD7eO1A0B08$Oj2t{0$_ z=bAW_2Uv-llLN%T1)NJjFfhOa2Pcr=VF20<(7qlGp;R3xA?cvHxCg@qRJ}7!#;P` z>p_j0E6{_el49Cnm`*Xod9M7&6-GJ%AFXZ4vG?5SId&Y8`5k3Pc327Re63Ukd& zz2eWMT(PlY)4FbBpcy7pwPQ|4LicS+tDc?NfC9Z52& z>^SGpM~Q0E$kO8l1P&PAjG7BkV6RwSmgv)c?@HPVef zkq$zBVfmxasWi{=p}j9GeY0d|D!|^&+b+n*z14sHMoRC1>&ZF^xU*8|Z;B$=@A=?5 znqGVOKYG$n0RJyf`j_$Nf8|GTQ&qxZyY?e?4j>1E;O5H74UY=!JU}i3j2v$4Fc23Q z?kU_rixUbrdk7eKjW;}LM}1d!)} z0EDky6h0rgeFA(E#{J9rI3V!(P~d7x8^!}I=e3_Y^XYV$8MBBdiIy+*hZWH^GBu{;D#lk zRIxtEet@^QkVC%PGM(oLR&dStThf0U97M2F^BwO)QSVfh;FL-A(4F4deWF3fk*9d1 z`&$cLT=_Oloe8M6Wytx%9bD?~tu)`xm;MMQh*%G}g}Rez=I>>({e%w*4{W9Gs^4-r z_?0Q8ohTz5_0B7iB5}FfLv^alS}=scApERlcU4u@&JL%`!|u2)o7Pjy1rL5{>~!qP zhc^q5KZi;~RjMR%&6I+9u~f)kMCG73XEnrKxcf-P-YpazNK|?C20vabCs343`c3*H z782lBn~Yf3o37B6ENtD5?1|m_o%IK=RUEvsq6YgcX7y}s(smu0zkH=6jUGjisKgbb zaBI8C_nyP@hYtdwc%IGQnfveOkbh_HS2_FlbHKmOYyT^Izdi{3w0ABD2XN^O22MCo zb`DkugbT1ZPA)JjHxD;E+*W~FMw^R+o0W^32Mo7J;1~fU7$_@*n+tA=K(+&3v$Jw@ zL%HEr$_anX31J1oGdp0PaGU1@F0r|R>G0P)SMC5~1%tV+hC{&cd<6qofC1|P10cXz zOq+v)ixmRBo&kI}0gwO|_u?D?lU%?v+~WW*;A97gadKWIU3hxp;9_Tm{^Sh; zI4s~%uxs9dsSsB9OgJA9PWW6X_~(YNvmF;J7`|)xYaW0LAj7fpa9(c{!UOD!2f}sT z0s5brKmrBBw+P{8g>V8{2$&0e>G31%2xs4I>G?Lgi8+uPm@2#LOI~6k`wrPfC~b?iX~idIm4i= zP$(w{Cy0Zc8_@BU__*O86aY`aitO;%0_B7Os)TXy0NEa(2jk>|0#85t;Ddm3kTyGz zYkvwI#LfZm2?eD7bHD6YDun?W2aW>3id+zOxCpNU6Zp5VbakNmb$B4%WBDhXU4-czlKU!%+WY1zp7lG)UACY%Ie)DRt%`%5 z3QeEfIah9*oN&lNyXVL3KiZ#VjK&*xl`NY+rf(|mmh`bxCp4k#v!|1JQd)}A__=*Y%D-;U z>x11GaXfK6PyKya?Lx}BcQ)AKb*i&!7q$5xyFDHJFR>Sgt?!#jl zUB>XME(1E1o$c&^Pg&rM|KSP$3`qQW^7&mj|MJN5kD}q?{6jQgpmi-82wX~kNrsaX zX#Q)-umfcw3lxYiKmdjUasdOe=y$pNW4Q(drN0pF|D{~u3Glx)ntz=e|4}dyFeecH z*ts}4e>a_9!Tpz%IAB1qgh$KYWyJ-7v2t?)De>P)3|c3jS0(4~2KIZ^^1l@1?+5H(U$?OH0D8Y}KtOPQ0B-){w=No< z8Y(8_m)=V}Fyt4HqlW0%^9|6@*uX2Pjidy;jJo(jWHR`O=%l0$4H#}hs8Z8nFTN`o zXlOB>&Nveczh&Xru1twJi#v>~)p4Lci zJZPD)`gxVeuL2juFrfTs<2-ZS2*HZ5!*VcHhUqg#3(|8C!mNW?(xy1;0QOr6gsR-C ztS0f9PUn0k%shwT(;4aR zWt0#aNw1@Q|8HaHAMQM|MZg#QL=fx0{GuxY zQp@*|0EP9_sC*ZGwp4(U+yJ3cy>M~x-$ITKXnT$^$5y7ukQu;fcZfK$GP+BMiAgUK7A*RLlu#n? zxy9WVL)3dc?m>nSCeGt={{7`OKe=PCq!_8Bl1Zr*;ZJ}m z>9@_-TC0Bgr2Na0>*cVd1lo8^K#H%RJMlb&*oo>Lw5cRNER`%`RrRVdLvAbxl2+n# zKS%`#S4&omjOjz*z2(xIT0_DtOdq};?R0!o1F#;s9ma`3@NPM z^S;aV*x*z2rvRUphaG)BDA6a!22`KHmY2B2zBE)v-$Z?(2I^zu(T*34tebt?mt0<7 zvA|W9+IEB7Ip?vZp)keX0m32QIaI7@E)}SV1Aa^> zBIT#fv6isgB`S(Z(z$A1aDp`oRm#Kj>UKL@EWjl42fSY_s70b$1ZlX++a8)49j;oZ z^d*@_a5H;VtsNZcxgY$1-P7e0QP2f_R(Qx#qj#*x&CdpV`o4&b5f|4dASazf9(z6P zSx*eXUFqZ223KOY=ERRBXx(5e$uTk<;%bgwQwYVrQlV^ytosY;+|~U`C5sZqq*>-F z#Uho0{%SF8Q(yT2JQbD!8b)Xm%Hv3k5% zZ_Q(RgdCMil0JJeNe&sbvL&IQ`5Z{d3z2z~RI0X^^U5-6eZTURY{`ISBeS%K-sd;I zWsJ2R33RK=3s~;+5lua%k*lAYVuK|(nL-0l9-f|*KXeUQlU>;HWJ_KRLynJhgo@dzn)Ui829no%ZSiq}(HLXP6WcG>x&!Mx*U?W@`&1nmq2i{RGDcGo(`Y%8*k%Xg=;!juNC-K-jQG z(>69h87*`r*;QOYM6n}xDB>}~djuBCCF^sD3|1P*3WGlK1WA{wN#`2Rl@Mz8TxeSv zP3GYnH4VOLn@LMgABm-_TJ}q3kC_Z@_U7oj|078KtZyO17&Pc(DsxuwiOz_G7)Q>W zrIOVl@?qj?4V~LK@74&);CW$5i@RYZ^>9K0#acsslPl{&oH}$Ie6Q7&*ZU-4D2CQC z?@%6(0k%{Xo)`MpU9G+2y!m zt<&31E*>U&Hs6b=*z70DpUgD4v!2ZOEIpw3+U%TbCmCC(z{faHu{?2jSWaQsCl^LY z>ad})p8g^~y<}$Rn|g5D8_OQs#QX7&?%zObrH*52OsJ!FuXS;r&#V~xFpxepR&t)J zViurMr%rD$Y2>)o>Wsn9lB{`SP<|*K6S4Jmkt-oqc!Wd6Q^LeIDv4dODP%)Wwn< z{3W&7)M_z~m#^D^BA;F2Zaxpr$&-Bl%y)Bgc+ysi$Sx1=l{x4#CJgCUgoc9y*4>(R zQfCt?K!&GsdnA#IKUB_>9fyh*c`2xx9@8t7IJ~WmysPWBIqteZzT^H0Ta9((HsZ<00#`)(H$mupSi+lq>gV4;SMYH_68`O-S9+U`c+U30ebZQuQyp*11m z0FPRfVu$=bPWC;oz>HYOE}64t0WWd44`JM zZyv&s?vqILzk)?n7N`!qGo|IH=EuxkObyEov*_*5H1FKgh6YFYh)YuFYTVzVstQyJ zvN0A#+!Gt^i^%hI6>_Jg`qsu3A6-ac|iS zxvpkMuiIC2G`hZ%WeiSg?V?@a0y?bq$NQw!@adE1KZ$;PwI z4b*YHhS#&rmy>nwSCo2Jf~%;a!oOX7Y)B-H!`*0~I`Z907Azt;%0y(B=yWS(_w7_b z=Ar*OddUYWNA^VzQhZ30Wy;pq*HKtnQ$o^X57j;-rPJA8V3G1)HFW$ zSqGunCk4Oexy67_on@-JCR3i^$ps2v=!FCb;eY}mp$%n4#e#H{`1a%<@yCLNQ0$0l z=0o*w`k=fc(*|YTF$kn)T_II14W2)E4=ksBalb zf7vbW3yf4fR5s@2Uc&Rw1d?WgY2j~c{k}=iTwt!;@|#y0eXfu;w(}~ccGcGW*tfAk zmwh~itW9BjgF$w4pzjA+{N_OY9dQc9$|8k@F9K8)(@4|Fq7N#X&2@dk!!KRsXKOjr zj13v@O?I4D`izRS)8xzixUKiXUc}9-v@*le6BBKdK&x5 zN4veaWqVW($BlK1fY#ek)t<*gE1_c=g08NQWq&yE?Ihhzcx+Tg zp052UwMCj}0MAVZF`P4ZVjn{egbqRrd+9bH9erMF6h%)SJ8ki@sDyCYCsCE&k+4rH zH8q26-4Stge>J}N#l+%(di#En^p1?rQ~KDK7JA>I;U$uzW7{g-F16kn!}RK8*mtWn zna0WX#T(z4zN}X)?6)ej6OB%#7^}5$qOyGW`HAa&9-A`W)vux47tY)+;Mv;t6Jxgx zQSG$s{L^J_PZG116P2gJE_Zf3B#npfLdf?|DPN2w>g5bVl7@3?Ue^xfvf#P#h8(>Q zc~JV)C7FCG-HyZA78IG9Z{^$AxG>AUI7`l_LgKvk$(nLEw!GfS^^U2d9ikIanvj5C zQ3RUq%26QmzJmJg&FuMN-&81i;*0kw==j|>6#N?+Cqi~ zy{d)HVk5~+%fhQ%?VcCW_29usX9BUTI8PTgr6XIr7e3Y854rW)t>h%Ob0a%hyRbgA zdpk9;;UqKZQrLU&o&S_o{Ifzd?Mmdgtc=m}eTDb+Rber$i{{SG?=~Y<)FsB#Bvdzt z232wxr^a~~MIF!;(#ZKI=2FJwz+NX36v8+U|7(^Pr%f_mv%JQBR6}3pXF8vd+jal|bKO@@{4eyDh^oj0dm*I=2 z&XZzmX1poF-V&#S?V841@$vkXd?8CS$BiP=uF91YRmFA+BU7Udhs_JO?j&g9J;%QR z!h0S*q^4UnyA9>zb9--B-dI(y$I$AsUDPf0N;57(V5*OMaJUx~*X2dBKk{ca$ppNnL=a#@b`` zwG1uhPS35;Tg1}3@@aH?g{BU)uW?045%|N6zNA;aK@-`S`Egl$iHQC-Bkk|f?e8}~ zKXDi?C16(S5`H#B=SsY zlQ;Qr{pt|~bG)6M(uR$EZwl@rcbsjfa56Q@F!3WCBO+`)ZnJs)MI`=E9f7$Rp=$Nk z*o{pD5IO*vAnZL;)lPVL*?j4Yq<_weaI!rTWj@qw`(6iu6v^++2+%qd`DBlvfs0T! zGD*Sp(EhCtF=$%=VIsVEs!oDx!KH;%a0?y(qJ_7~J?(8$D+=SIDnM9Sx6e#e!=$VGqG~QX6A_zbloD#ri!5Rg#=( z>-zo#57qZmS68WGU>KzZSK?Q61fu7wib9m(_5K*3gfSAtW!7wZHep5{3_+-<5PnRS z{;vI3tP!P~yHAm{bG~8nZ9$i$g&A?I3Cb>!W}S1={s$Qx-=<}G-2ot#y!8}tr4it1lY?hDG+; zz8{zW#)a_*76m(t&5x|}&7huhPmyy0HL8q`owH$mc0unM0=5-7h2&<$LSNgnEEV+j z^Up9zRC^hBnqj7|eXkZG7Rm1^`aX`3Qq;q1iWizLzKx9R44I9Kzs!fuK_K??(-2U zJ!>rBAHUT3WqnlpJTsDmQmxPO82t$dO{W4?TF{?Y1GQL?3^I-OqYRmIW85|XH&h4{ zK{ha~VqYe{`C+;Kh+W~y8@j`wwU7*DAKu3o7YA5jdMz04U2h21Ze=STiM`<*gFP;r z8r;^);`c3+6dP|MSo;=!Yl0j2Gn}U^4tbfXK85%4B0Qmfyh=O{RWEx>_Dr?uFk>w% zm&aTAbdgOp$}8;qJ1#9<1z$ZcLZ4_V3h&SE&r96N{2P`<07zzt9$ADxCarv z-z{-q)7Dbn;0G5B&j%xeP)IC-EvT5^}ZeF1d|L*$R=wV5$e*e`=M8lTEx zNF>~U5H1qYkll-H-`q6x!3l~1!zeoFL!c(bJSMf24P!onWEIL!el`lo>xsg4Awv=} z#b%0;1j%o83eK~jkYTdUmQ={b$LNE}jcQExAi|PhLj94bO8G?(W|`g&ji8uJa&A)* zZ7`uCSxq2sSD!=72DVZprY=3Yh}VMz3LJVwT)Mmpe`4_mGSPA!k2-@7(dan>Q)TT! zw97GgRP(d%ZbYOdJhGbBMZb43K*{r#Pzl(_gGWKR z^?@|FL5wYIY0B}bMG@RIAM9p2LsLx9=E!P-N!cALUYMZWjx6z0MvWZ1pfq0(^&vaH|Te8oj6FO9Ee$jnuM)+Io1DgCsofR9``k#UC5ke;Js$g<;uQ!tshm+biDFiJUL zcuE55&BcM&&ZJv=3gWakwq==Jn1(1-JmT&IFCH6@4HuXRF^sQ-)&+}L&XB1k`Wl43 zn(jP^HGbVEOD==$5se5LvYOIJCResYy&S_giix`Ch>Gg;~-@w}u*Bn_R;{76=fQTkqX_091HSx*#8@M+3dEAM< z8V0g<#bX{uVUJ#tjyz_e7MSs^6w*v)ef#ZE5{SJfFco)_jMe2-qmKAqj{obd1A&BW zNCX4cAnwQ-5d*XKjX+J@_9r(OnI-C9iYaSEmEFwE5cZ5FH%ZVJ{T#+Hl!c~3Rc#p= zE^JR;jMqFcy>3L3nXRHW_m^c1}pCC(kW24xu0f>*C1pCx2O6< z0@og$^ivTQ(LBp5Zgr~cuYwBET`z|jU5aqB8D|al_o#bH>jO1i>^KK8N20(EZI!GB zUEF?hrDmkYik>(G&QAu&d;Hm%u^`3disTvzBK`Oa{A5^{OjuSbvV6F|0tz(}l#y*pM-GcJo zx}i~bq=e+CsX$ugASCf-{oP8Yn|Fy~IBmHqi@e+R_zF__f>!GH+3eS1x+E6%Vber4 z4ZyG3hNS7&TKtjIbjeEDo?q|Eg;k6Z&=o)rW{OwoG6rura)sAhgrDBc`_RQf z=E3#`+cspr?_f(+uUM^b@X@C#HB0G8=o#TT3`4xwyF~i_K=HTAFKhu&oI3Th85I_J z&~#}WL$$k=evh}N!l;#V9l5ITASM5Kk-yPoXjLn~js=c`W;(h4yWs-ukk>Hk4rst) zR63#RBTJ7WZ>!T2aBfdxw3H<%NKGRUo#Zm$t$k7Gvya>dp+;SZcN`|3N=Fw(E1HQV z-aOxGi`=a+%ehA$(W63-mR*^EpWRvTG^`T?&pqQdk?O6WN720ZwYC*CJ?w}TbUq7t z$A>ypimA{xNOxjz1#d(_zXfQL9oW&&r($;A4x@(l?GO)4#}wB;8~-c+n~_l^H(tW5UQ>;0<)rkDAw2 zQMK@q^t&&s=hZcCyw2!KB5heRqZfHCiGw{KH6pdP?V@%`%(w^Etypj;S{O@*##X=! zn<_6nq@-ab0fX#lRh8S(h)W;p2-P&Vj2zj<77Jb@ZgZ-sZW-;dbJouEB{vXLsN{+! zO=gQL&st73h*Z;;M-NX&Ar5^%)d+XNc}|2~V^mOIW7(%@tJx2!ZWX8tHR7&J1bzaI zxa}KVQhmif98^AcNzHiGB0xM9l|5Fc!WShzcZ)J4wJG+a(-)?wiNazY`TW`A1Zz(m49WCf5N%V!`ZkRqQ6EIn`|J#PfS;fd_15V=UgtLMTBY@H<>?JC`!8B) zZ1Sb!CoAtP;SXcLgBA-eM>pEz{3S^Hbl(((aoTiWQ^WW{v)^;{(VnjQm7)zG&ed?g!yT>HX`lGOhZEb~ zd?U8o_T5S%$`-pMZo>eGNB<>pNK*IS_SBBk@tvouyD7kcBTIW(BpZO#2JkbJe|zw4 zbc3t|Wwxj}MxlHNj1t!^sqrahS;BCk@iy`gik>Cpw_L9U@H-vOipH(X}*s2ek9dd)iy8%vWbNIy zqV6S1rk|jFP+%5AjX$U%1>^e;L&~tJV=zy*Wu=CLtZB$}A(p(*Yv_yRo1*HM!%ypX zyeWxxb}r(Ky#wm+O+EAe)>*l#13K3!nqfH<;_c5oxfcM^rw|i$eG!V*?R7eYgV4+& zG7?99dz?R(XB7J#(;TL18gag&mPIMUy*rZ@1YbLnq6L!OzNU#1v{~?+bWW90rOLM4 zKfEo^fAXy_>>ggYcSg%X&?;9st&4XP8a^&g8o_#4ekOk1>BIzq6e3pK&=cbm7HVGt z5CP`11rX9L(WZb+Fc!ihJw6JfB`1N@cgbPg_9MsVv8x~Kl;1_rzc97LJv*7s4_dC# zu(EtL{KE9kdM>NdBOB-V_;LOhS^-6d1CfL8{GwA({cO2AOz*DG(ot9B1G9%7 zLXX?Y6{lLU>>iC}+c%$k98(V$Y(p};+2x=`y)#k0{c z`uQKrPgY4z@c0E1GuRHjXJssoUo`GBaCik=Jl=XLThVFUuCVe@SrAbTAvY(oIj9-a z$);KyQNvOEeU;U7Ofur=0sdKRRED`~#Wh(z)Yq{v0+y~QuBme0k7r?LRMccN@=`6S z$CWenUr0QUF1!wwu??5>1&FgXS%hjwHGngiH2$A zdIDq12;#j9?UmMS1^&pCDEdX2b5-D9pJP4bX*SnKXbzGXsiVJ0IL2N|DGnKV`7Yg>&ZfU9CTE2k^5byD&~K&i^(2^pzCou* zRR+e_@=p1YZn$b~PS$)ju{3>c0r8-;qe*ycNsT4H=_{>`q@H3eC~|n!+c+^}PaCo( zc~-aMgyFfC`%Pu7gSNIw2jZi?-dXNL8_X%rr^olp$mtZCY-Nsy9j(4TnWfph=jz;# zN2TSmx5Dpfv7OVK5LCL^eLvdAcXu`3!pZwnL4v8nNo-p9&evnk+u5d(MUS~@XFHt* zz)rz;;>SDDP~W~cBMy~>klpV^G<-zl`bfl*l}?n-e}k{1^Ole9Lp&>9$x!KF^}ru4 z&9^&r^{HNFpmSCf5jQ9Ch35C)-MgKruBnzhn4O-}#N1@lFmL=m46CtTeet4mkz`u$ z^38C8fWt=m#f9f;ovBAioPZmZx2dP$Mp8bZ2p{fpcfOv-cH~l9?*s3_w`5wvM&LW= z7i9e3BPVI8K(A0NxD@>r4Lq5mKAYamjy2#zAjLyfQ+PFOG|tkSO|m``svD%qFo#`H zQd3pL%!aLcK9O;>b+YH*r^~AO`$<&N8Sy=}|k_~)M39QHMKw3AH1QeO{pnof5?1b_W{OgaRb zPAq&e@8&pCYGfzfz6i4Hh)2)(i)z7rR(CeV)-eu<(~V|qm{v|-%D#gn+VJ}9;pXVKAk&#M|gzYJ`c

d#b=s-(14r41Wu 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 literal 0 HcmV?d00001 diff --git a/source/website/pdfGenerator.js b/source/website/pdfGenerator.js index 22e447a2..a3b17728 100644 --- a/source/website/pdfGenerator.js +++ b/source/website/pdfGenerator.js @@ -104,6 +104,9 @@ async function generatePdf(data) { page.drawRectangle({ x: 50, y: imageStartY, width: mainImageWidth, height: mainImageHeight, borderColor: darkBlue, borderWidth: 2 }); page.drawRectangle({ x: 385, y: imageStartY + smallImageHeight + 10, width: smallImageWidth, height: smallImageHeight, borderColor: darkBlue, borderWidth: 2 }); page.drawRectangle({ x: 385, y: imageStartY, width: smallImageWidth, height: smallImageHeight, borderColor: darkBlue, borderWidth: 2 }); + + // Paste images + await pasteImages(page, images, pdfDoc, imageStartY); // Footer with centered "hyperlink-like" text const url = 'www.tellmewhereithurtsnow.com'; @@ -121,6 +124,44 @@ async function generatePdf(data) { document.body.removeChild(a); } +async function pasteImages(page, images, pdfDoc, yStart) { + const imageStartY = yStart; + const mainImageWidth = 320; + const mainImageHeight = 380; + const smallImageWidth = 160; + const smallImageHeight = 185; + + if (images.length > 0) { + const mainImage = await pdfDoc.embedPng(base64ToUint8Array(images[0])); + page.drawImage(mainImage, { + x: 50, + y: imageStartY, + width: mainImageWidth, + height: mainImageHeight + }); + } + + if (images.length > 1) { + const smallImage1 = await pdfDoc.embedPng(base64ToUint8Array(images[1])); + page.drawImage(smallImage1, { + x: 385, + y: imageStartY + smallImageHeight + 10, + width: smallImageWidth, + height: smallImageHeight + }); + } + + if (images.length > 2) { + const smallImage2 = await pdfDoc.embedPng(base64ToUint8Array(images[2])); + page.drawImage(smallImage2, { + x: 385, + y: imageStartY, + width: smallImageWidth, + height: smallImageHeight + }); + } +} + // Helper function to wrap text function wrapText(text, font, fontSize, maxWidth) { const words = text.split(' '); diff --git a/website/assets/reports/pain_report.pdf b/website/assets/reports/pain_report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2dceba4c88ee3c67b9e80fd0702846804d242f12 GIT binary patch literal 23546 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

d#b=s-(14r41Wu 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 literal 0 HcmV?d00001 diff --git a/website/index.html b/website/index.html index f695f1a3..17c9d481 100644 --- a/website/index.html +++ b/website/index.html @@ -20,8 +20,6 @@ - - From 611cba0c9b1dcbb529ad93b5a023d41e1a6d20c7 Mon Sep 17 00:00:00 2001 From: Tomas1337 Date: Tue, 16 Jul 2024 01:32:07 +0800 Subject: [PATCH 18/18] logo --- website/assets/images/tmwihn.png | Bin 0 -> 48866 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/assets/images/tmwihn.png diff --git a/website/assets/images/tmwihn.png b/website/assets/images/tmwihn.png new file mode 100644 index 0000000000000000000000000000000000000000..2e13303b58bf600cde92a3a929f6def693cf78d2 GIT binary patch literal 48866 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

4~?5AlP#$)e*Y5HtJDO2O>F2c8n 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