@collabdt/core 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +29 -21
  2. package/dist/core/components/AppSidebarContent.d.ts.map +1 -1
  3. package/dist/core/components/AppSidebarContent.js +21 -27
  4. package/dist/core/components/AppSidebarContent.js.map +1 -1
  5. package/dist/core/components/authentication/AuthPage.d.ts.map +1 -1
  6. package/dist/core/components/authentication/AuthPage.js +32 -7
  7. package/dist/core/components/authentication/AuthPage.js.map +1 -1
  8. package/dist/core/components/authentication/SignInAuthHeader.d.ts +7 -0
  9. package/dist/core/components/authentication/SignInAuthHeader.d.ts.map +1 -0
  10. package/dist/core/components/authentication/SignInAuthHeader.js +32 -0
  11. package/dist/core/components/authentication/SignInAuthHeader.js.map +1 -0
  12. package/dist/core/components/authentication/Signin.d.ts.map +1 -1
  13. package/dist/core/components/authentication/Signin.js +308 -10
  14. package/dist/core/components/authentication/Signin.js.map +1 -1
  15. package/dist/core/components/settings/index.js +3 -3
  16. package/dist/core/components/settings/index.js.map +1 -1
  17. package/dist/core/components/settings/src/OrganizationSettingsPanel.d.ts.map +1 -1
  18. package/dist/core/components/settings/src/OrganizationSettingsPanel.js +5 -4
  19. package/dist/core/components/settings/src/OrganizationSettingsPanel.js.map +1 -1
  20. package/dist/core/components/settings/src/SettingsSidebar.d.ts.map +1 -1
  21. package/dist/core/components/settings/src/SettingsSidebar.js +12 -2
  22. package/dist/core/components/settings/src/SettingsSidebar.js.map +1 -1
  23. package/dist/core/components/support/BugReportDialog.d.ts.map +1 -1
  24. package/dist/core/components/support/BugReportDialog.js +94 -13
  25. package/dist/core/components/support/BugReportDialog.js.map +1 -1
  26. package/dist/core/components/support/FeatureRequestDialog.d.ts.map +1 -1
  27. package/dist/core/components/support/FeatureRequestDialog.js +33 -6
  28. package/dist/core/components/support/FeatureRequestDialog.js.map +1 -1
  29. package/dist/core/components/ui/AppSidebar.d.ts.map +1 -1
  30. package/dist/core/components/ui/AppSidebar.js +52 -9
  31. package/dist/core/components/ui/AppSidebar.js.map +1 -1
  32. package/dist/core/components/ui/DataTable.js +3 -3
  33. package/dist/core/components/ui/DataTable.js.map +1 -1
  34. package/dist/core/components/ui/Dialog.js +1 -1
  35. package/dist/core/components/ui/Dialog.js.map +1 -1
  36. package/dist/core/components/ui/InfoSidebar/Container.js +1 -1
  37. package/dist/core/components/ui/InfoSidebar/Container.js.map +1 -1
  38. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.d.ts.map +1 -1
  39. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js +2 -2
  40. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js.map +1 -1
  41. package/dist/core/components/ui/Sensors/SensorsSection.d.ts.map +1 -1
  42. package/dist/core/components/ui/Sensors/SensorsSection.js +2 -1
  43. package/dist/core/components/ui/Sensors/SensorsSection.js.map +1 -1
  44. package/dist/core/components/ui/Sensors/sensorUtils.d.ts +9 -0
  45. package/dist/core/components/ui/Sensors/sensorUtils.d.ts.map +1 -1
  46. package/dist/core/components/ui/Sensors/sensorUtils.js +6 -0
  47. package/dist/core/components/ui/Sensors/sensorUtils.js.map +1 -1
  48. package/dist/core/components/ui/Sidebar.d.ts +4 -0
  49. package/dist/core/components/ui/Sidebar.d.ts.map +1 -1
  50. package/dist/core/components/ui/Sidebar.js +11 -5
  51. package/dist/core/components/ui/Sidebar.js.map +1 -1
  52. package/dist/core/components/ui/Table.js +2 -2
  53. package/dist/core/components/ui/Table.js.map +1 -1
  54. package/dist/core/components/viewers/Data/DataMenu.d.ts.map +1 -1
  55. package/dist/core/components/viewers/Data/DataMenu.js +8 -8
  56. package/dist/core/components/viewers/Data/DataMenu.js.map +1 -1
  57. package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.d.ts.map +1 -1
  58. package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.js +26 -0
  59. package/dist/core/components/viewers/Data/buildingDetails/BuildingDetails.js.map +1 -1
  60. package/dist/core/components/viewers/Data/details/DetailHeader.js +14 -14
  61. package/dist/core/components/viewers/Data/details/DetailHeader.js.map +1 -1
  62. package/dist/core/components/viewers/Data/details/TabSidebar.d.ts.map +1 -1
  63. package/dist/core/components/viewers/Data/details/TabSidebar.js +22 -0
  64. package/dist/core/components/viewers/Data/details/TabSidebar.js.map +1 -1
  65. package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.d.ts.map +1 -1
  66. package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.js +19 -0
  67. package/dist/core/components/viewers/Data/infrastructureDetails/InfrastructureDetails.js.map +1 -1
  68. package/dist/core/components/viewers/Data/siteDetails/SiteDetails.d.ts.map +1 -1
  69. package/dist/core/components/viewers/Data/siteDetails/SiteDetails.js +19 -0
  70. package/dist/core/components/viewers/Data/siteDetails/SiteDetails.js.map +1 -1
  71. package/dist/core/components/viewers/Data/utils/Columns.js +2 -2
  72. package/dist/core/components/viewers/Data/utils/Columns.js.map +1 -1
  73. package/dist/core/components/viewers/Data/utils/positionParser.d.ts.map +1 -1
  74. package/dist/core/components/viewers/Data/utils/positionParser.js +6 -5
  75. package/dist/core/components/viewers/Data/utils/positionParser.js.map +1 -1
  76. package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.d.ts.map +1 -1
  77. package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.js +8 -1
  78. package/dist/core/components/viewers/bim/src/BimSidebar/src/CommunicationTab/src/TopicsSection.js.map +1 -1
  79. package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.d.ts.map +1 -1
  80. package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js +7 -1
  81. package/dist/core/components/viewers/bim/src/BimSidebar/src/FileTab/src/FilesSection.js.map +1 -1
  82. package/dist/core/components/viewers/map/datasets/RowActions.js +6 -6
  83. package/dist/core/components/viewers/map/datasets/RowActions.js.map +1 -1
  84. package/dist/core/components/viewers/map/datasets/data.js +1 -1
  85. package/dist/core/components/viewers/map/datasets/data.js.map +1 -1
  86. package/dist/core/components/viewers/map/datasets/index.d.ts.map +1 -1
  87. package/dist/core/components/viewers/map/datasets/index.js +28 -9
  88. package/dist/core/components/viewers/map/datasets/index.js.map +1 -1
  89. package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.d.ts.map +1 -1
  90. package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.js +54 -31
  91. package/dist/core/components/viewers/pointcloud/src/PointCloudLoadingState/index.js.map +1 -1
  92. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js +1 -1
  93. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/index.js.map +1 -1
  94. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.d.ts +2 -1
  95. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.d.ts.map +1 -1
  96. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.js +5 -4
  97. package/dist/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.js.map +1 -1
  98. package/dist/core/hooks/useCaptureScreenshot.d.ts.map +1 -1
  99. package/dist/core/hooks/useCaptureScreenshot.js +62 -31
  100. package/dist/core/hooks/useCaptureScreenshot.js.map +1 -1
  101. package/dist/core/hooks/useCurrentViewerPosition.d.ts +18 -0
  102. package/dist/core/hooks/useCurrentViewerPosition.d.ts.map +1 -0
  103. package/dist/core/hooks/useCurrentViewerPosition.js +66 -0
  104. package/dist/core/hooks/useCurrentViewerPosition.js.map +1 -0
  105. package/dist/core/hooks/useShareUrl.d.ts +2 -0
  106. package/dist/core/hooks/useShareUrl.d.ts.map +1 -0
  107. package/dist/core/hooks/useShareUrl.js +74 -0
  108. package/dist/core/hooks/useShareUrl.js.map +1 -0
  109. package/dist/core/styles/globals.css +46 -33
  110. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.tsx"],"sourcesContent":["'use client'\r\n\r\n// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport * as React from 'react'\r\nimport * as LR from 'lucide-react'\r\nimport { useTranslations } from 'next-intl'\r\nimport { useSession } from 'next-auth/react'\r\nimport { CollapsibleSection } from '../../../../../../../ui/CollapsibleSection'\r\nimport { FileItemComponent } from '../../../../../../../ui/FilesManager/src/FileItemComponent'\r\nimport { useFileActions } from '../../../../../../../ui/FilesManager/src/useFileActions'\r\nimport ConfirmDialog from '../../../../../../../ConfirmDialog'\r\nimport { Button } from '../../../../../../../ui/Button'\r\nimport { DbFile } from '../../../../../../../../types/dbTypes'\r\nimport { PointCloudContext } from '../../../../../../../../store'\r\n// import { useAppConfigContext } from '../../../../../../../../store/AppConfig/context'\r\n\r\ntype CreatePointCloudResponse = {\r\n pointCloud: {\r\n id: string\r\n name: string\r\n }\r\n upload: {\r\n uploadUrl: string\r\n }\r\n}\r\n\r\ntype StartConversionResponse = {\r\n jobId: string\r\n}\r\n\r\nconst POINT_CLOUD_OPTIONS: import('../../../../../../../../types/global').FileAction[] = ['view', 'delete', 'info']\r\n\r\ninterface PointCloudsSectionProps {\r\n files: DbFile[]\r\n pointcloudApiUrl?: string\r\n}\r\n\r\nexport function PointCloudsSection({ files, pointcloudApiUrl }: PointCloudsSectionProps) {\r\n // Translation\r\n const t = useTranslations('PointCloudManagement')\r\n const API_BASE = pointcloudApiUrl ?? 'http://localhost:5101'\r\n\r\n // Get current user session (needed for uploadedBy)\r\n const { data: session } = useSession()\r\n\r\n // Point cloud context for loading/unloading clouds in the viewer\r\n const { dispatch: pointCloudDispatch } = React.useContext(PointCloudContext)\r\n\r\n // State for point clouds — includes isVisible to track which are loaded in viewer\r\n const [pointcloudsFiles, setPointcloudsFiles] = React.useState<(DbFile & { isVisible?: boolean })[]>(\r\n () => files.map(f => ({ ...f, isVisible: false }))\r\n )\r\n const esRef = React.useRef<EventSource | null>(null)\r\n const autoLoadedRef = React.useRef(false)\r\n\r\n const fileSyncKey = files\r\n .map(\r\n (file) =>\r\n `${file.id}:${file.name}:${Boolean(file.pointCloudUploaded)}:${Boolean(file.pointCloudPotreeConverted)}:${file.uploadedAt}:${file.potreeMetadataFileKey ?? ''}`\r\n )\r\n .join('|')\r\n\r\n // Sync incoming files from props while preserving isVisible state for known files\r\n React.useEffect(() => {\r\n setPointcloudsFiles(prev => {\r\n const prevMap = new Map(prev.map(f => [String(f.id), f]))\r\n return files.map(f => ({\r\n ...f,\r\n isVisible: prevMap.get(String(f.id))?.isVisible ?? false,\r\n }))\r\n })\r\n }, [fileSyncKey])\r\n\r\n // Auto-load the most recently uploaded converted file on first access\r\n React.useEffect(() => {\r\n if (autoLoadedRef.current || files.length === 0) return\r\n\r\n const readyFiles = files.filter(f => Boolean(f.pointCloudPotreeConverted))\r\n if (readyFiles.length === 0) return\r\n\r\n const lastFile = readyFiles.reduce((latest, f) =>\r\n new Date(f.uploadedAt) > new Date(latest.uploadedAt) ? f : latest\r\n )\r\n\r\n autoLoadedRef.current = true\r\n pointCloudDispatch({ type: 'LOAD_POINT_CLOUD', payload: { id: String(lastFile.id) } })\r\n setPointcloudsFiles(prev =>\r\n prev.map(f => ({ ...f, isVisible: String(f.id) === String(lastFile.id) }))\r\n )\r\n }, [fileSyncKey, files, pointCloudDispatch])\r\n\r\n React.useEffect(() => {\r\n // Cleanup SSE on unmount\r\n return () => {\r\n if (esRef.current) {\r\n esRef.current.close()\r\n }\r\n }\r\n }, [])\r\n\r\n // Toggle a point cloud on/off in the viewer via context\r\n const handleView = React.useCallback((file: DbFile, newVisibility: boolean) => {\r\n const id = String(file.id)\r\n if (newVisibility) {\r\n pointCloudDispatch({ type: 'LOAD_POINT_CLOUD', payload: { id } })\r\n } else {\r\n pointCloudDispatch({ type: 'UNLOAD_POINT_CLOUD', payload: { id } })\r\n }\r\n }, [pointCloudDispatch])\r\n\r\n const handleDeleteFile = React.useCallback(async (file: DbFile) => {\r\n const res = await fetch(`${API_BASE}/point-cloud/${encodeURIComponent(String(file.id))}`, {\r\n method: 'DELETE',\r\n })\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(msg || `Delete failed (${res.status})`)\r\n }\r\n }, [])\r\n\r\n const handleConvert = React.useCallback(async (file: DbFile) => {\r\n const res = await fetch(\r\n `${API_BASE}/point-cloud/${encodeURIComponent(String(file.id))}/convert-to-potree`,\r\n {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ options: { sampling_method: 'poisson' } }),\r\n }\r\n )\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(msg || `Conversion failed (${res.status})`)\r\n }\r\n\r\n const { jobId } = await res.json() as StartConversionResponse\r\n subscribeToProgress(jobId, file.id)\r\n }, [])\r\n\r\n const handleDeletePointCloud = React.useCallback((file: DbFile) => {\r\n pointCloudDispatch({ type: 'UNLOAD_POINT_CLOUD', payload: { id: String(file.id) } })\r\n }, [pointCloudDispatch])\r\n\r\n // Use the generic file actions hook\r\n const { handleAction, deleteDialog } = useFileActions({\r\n files: pointcloudsFiles,\r\n setFiles: setPointcloudsFiles as React.Dispatch<React.SetStateAction<(DbFile & { isVisible?: boolean })[]>>,\r\n buildingId: 0,\r\n handleDeleteFile,\r\n onView: handleView,\r\n onDelete: handleDeletePointCloud,\r\n })\r\n\r\n // Sort: visible (loaded) files at the top, preserving relative order within each group\r\n const sortedFiles = React.useMemo(() => {\r\n return [...pointcloudsFiles].sort((a, b) => {\r\n const aLoaded = a.isVisible !== false ? 1 : 0\r\n const bLoaded = b.isVisible !== false ? 1 : 0\r\n return bLoaded - aLoaded\r\n })\r\n }, [pointcloudsFiles])\r\n\r\n // Create point cloud entry — routed through the CDT proxy so that\r\n // organizationId is injected server-side from the authenticated session.\r\n async function createPointCloud(name: string, uploadedBy: string | null): Promise<CreatePointCloudResponse> {\r\n const res = await fetch('/api/point-cloud', {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ name, uploadedBy }),\r\n })\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(`Create point-cloud failed: ${msg || res.status}`)\r\n }\r\n\r\n return res.json()\r\n }\r\n\r\n // Upload to presigned URL\r\n function uploadToPresignedUrl(\r\n url: string,\r\n file: File,\r\n onProgress?: (pct: number) => void\r\n ): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n try {\r\n const xhr = new XMLHttpRequest()\r\n xhr.open('PUT', url, true)\r\n\r\n if (onProgress) {\r\n xhr.upload.onprogress = (evt) => {\r\n if (evt.lengthComputable) {\r\n const pct = Math.round((evt.loaded / evt.total) * 100)\r\n onProgress(pct)\r\n }\r\n }\r\n }\r\n\r\n xhr.onload = () => {\r\n if (xhr.status >= 200 && xhr.status < 300) resolve()\r\n else reject(new Error(`Upload failed with status ${xhr.status}`))\r\n }\r\n\r\n xhr.onerror = () => reject(new Error('Network error during upload'))\r\n xhr.send(file)\r\n } catch (err) {\r\n reject(err)\r\n }\r\n })\r\n }\r\n\r\n // Start conversion\r\n async function startConversion(pointCloudId: string): Promise<StartConversionResponse> {\r\n const res = await fetch(\r\n `${API_BASE}/point-cloud/${encodeURIComponent(pointCloudId)}/convert-to-potree`,\r\n {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ options: { sampling_method: 'poisson' } }),\r\n }\r\n )\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(`convert-to-potree failed: ${msg || res.status}`)\r\n }\r\n\r\n return res.json()\r\n }\r\n\r\n // Subscribe to conversion progress\r\n function subscribeToProgress(jobId: string, pointCloudId: number) {\r\n if (!jobId) return\r\n\r\n if (esRef.current) {\r\n esRef.current.close()\r\n }\r\n\r\n const es = new EventSource(\r\n `${API_BASE}/events/convert-progress/${encodeURIComponent(jobId)}`\r\n )\r\n esRef.current = es\r\n\r\n es.onopen = () => console.log('SSE connection opened')\r\n\r\n es.onerror = (event) => {\r\n console.error('SSE error:', event)\r\n // Update to failed state\r\n setPointcloudsFiles((prev) =>\r\n prev.map((pc) =>\r\n Number(pc.id) === pointCloudId\r\n ? { ...pc, status: 'idle', conversionProgress: 0 }\r\n : pc\r\n )\r\n )\r\n }\r\n\r\n es.addEventListener('progress', (event) => {\r\n try {\r\n const msgEvent = event as MessageEvent\r\n const data = JSON.parse(msgEvent.data)\r\n\r\n console.log('[progress]', data.status, data.message, data.progress)\r\n\r\n if (typeof data.progress === 'number') {\r\n setPointcloudsFiles((prev) =>\r\n prev.map((pc) =>\r\n Number(pc.id) === pointCloudId\r\n ? { ...pc, conversionProgress: data.progress }\r\n : pc\r\n )\r\n )\r\n }\r\n } catch (err) {\r\n console.error('Failed to parse progress event', err)\r\n }\r\n })\r\n\r\n es.addEventListener('finished', () => {\r\n console.log('[finished] Conversion completed')\r\n setPointcloudsFiles((prev) =>\r\n prev.map((pc) =>\r\n Number(pc.id) === pointCloudId\r\n ? { ...pc, status: 'idle', conversionProgress: 100 }\r\n : pc\r\n )\r\n )\r\n es.close()\r\n esRef.current = null\r\n })\r\n\r\n es.addEventListener('failed', () => {\r\n console.log('[failed] Conversion failed')\r\n setPointcloudsFiles((prev) =>\r\n prev.map((pc) =>\r\n Number(pc.id) === pointCloudId\r\n ? { ...pc, status: 'idle', conversionProgress: 0 }\r\n : pc\r\n )\r\n )\r\n es.close()\r\n esRef.current = null\r\n })\r\n }\r\n\r\n // Handle file upload\r\n const handleFileUpload = async (file: File) => {\r\n if (!file) return\r\n\r\n const originalFileName = file.name\r\n const fileExt = originalFileName.toLowerCase()\r\n const baseFileName = originalFileName.replace(/\\.(laz|las)$/i, '')\r\n\r\n // Validate file extension\r\n if (!fileExt.endsWith('.laz') && !fileExt.endsWith('.las')) {\r\n alert('Please select a LAZ or LAS file.')\r\n return\r\n }\r\n\r\n // Check for duplicate names and append (1), (2), etc.\r\n let fileName = baseFileName\r\n let counter = 1\r\n const existingNames = pointcloudsFiles.map((pc) => pc.name)\r\n \r\n while (existingNames.includes(fileName)) {\r\n fileName = `${baseFileName} (${counter})`\r\n counter++\r\n }\r\n\r\n try {\r\n console.log('Creating point cloud entry...')\r\n const userEmail = session?.user?.email || null\r\n const { pointCloud, upload } = await createPointCloud(fileName, userEmail)\r\n console.log('Point cloud created:', pointCloud.id)\r\n\r\n console.log('Uploading file...')\r\n await uploadToPresignedUrl(upload.uploadUrl, file, (pct) => {\r\n console.log(`Upload progress: ${pct}%`)\r\n })\r\n console.log('Upload complete!')\r\n\r\n console.log('Starting conversion...')\r\n const conversion = await startConversion(pointCloud.id)\r\n console.log('Conversion started:', conversion.jobId)\r\n\r\n // Subscribe to progress\r\n subscribeToProgress(conversion.jobId, Number(pointCloud.id))\r\n } catch (error) {\r\n console.error('Error uploading point cloud:', error)\r\n \r\n alert(\r\n error instanceof Error\r\n ? `Failed to upload: ${error.message}`\r\n : 'Failed to upload point cloud'\r\n )\r\n }\r\n }\r\n\r\n // Handle upload button click\r\n const handleUpload = () => {\r\n const input = document.createElement('input')\r\n input.type = 'file'\r\n input.accept = '.laz,.las'\r\n input.onchange = (e) => {\r\n const file = (e.target as HTMLInputElement).files?.[0]\r\n if (file) {\r\n handleFileUpload(file)\r\n }\r\n }\r\n input.click()\r\n }\r\n\r\n return (\r\n <>\r\n <CollapsibleSection\r\n title={t('title')}\r\n icon={LR.Grip}\r\n className=\"h-1/2 overflow-y-auto\"\r\n itemCount={pointcloudsFiles.length}\r\n onAddItem={handleUpload}\r\n addItemTitle={t('uploadTitle')}\r\n >\r\n <div className=\"space-y-1\">\r\n {sortedFiles.length === 0 ? (\r\n <div className=\"px-2 py-3 text-sm text-muted-foreground text-center\">\r\n {t('noPointClouds')}\r\n </div>\r\n ) : (\r\n sortedFiles.map((file) => (\r\n <div key={file.id} className=\"flex items-center gap-1\">\r\n <div className=\"flex-1 min-w-0\">\r\n <FileItemComponent\r\n file={file}\r\n onAction={handleAction}\r\n options={POINT_CLOUD_OPTIONS}\r\n />\r\n </div>\r\n {Boolean(file.pointCloudUploaded) && !Boolean(file.pointCloudPotreeConverted) && (\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"h-6 w-6 p-0\"\r\n onClick={() => void handleConvert(file)}\r\n title={t('convertTitle')}\r\n >\r\n <LR.RefreshCw className=\"h-3 w-3\" />\r\n </Button>\r\n )}\r\n </div>\r\n ))\r\n )}\r\n </div>\r\n </CollapsibleSection>\r\n\r\n <ConfirmDialog\r\n isOpen={deleteDialog.isOpen}\r\n isDeleting={deleteDialog.isDeleting}\r\n onOpenChange={deleteDialog.onOpenChange}\r\n handleConfirm={deleteDialog.onConfirm}\r\n itemName={deleteDialog.itemName}\r\n dataType=\"file\"\r\n />\r\n </>\r\n )\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../../../../../../../src/core/components/viewers/pointcloud/src/PointCloudSidebar/src/FileTab/src/PointCloudSection.tsx"],"sourcesContent":["'use client'\r\n\r\n// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport * as React from 'react'\r\nimport * as LR from 'lucide-react'\r\nimport { useTranslations } from 'next-intl'\r\nimport { useSession } from 'next-auth/react'\r\nimport { CollapsibleSection } from '../../../../../../../ui/CollapsibleSection'\r\nimport { FileItemComponent } from '../../../../../../../ui/FilesManager/src/FileItemComponent'\r\nimport { useFileActions } from '../../../../../../../ui/FilesManager/src/useFileActions'\r\nimport ConfirmDialog from '../../../../../../../ConfirmDialog'\r\nimport { Button } from '../../../../../../../ui/Button'\r\nimport { DbFile } from '../../../../../../../../types/dbTypes'\r\nimport { PointCloudContext } from '../../../../../../../../store'\r\n// import { useAppConfigContext } from '../../../../../../../../store/AppConfig/context'\r\n\r\ntype CreatePointCloudResponse = {\r\n pointCloud: {\r\n id: string\r\n name: string\r\n }\r\n upload: {\r\n uploadUrl: string\r\n }\r\n}\r\n\r\ntype StartConversionResponse = {\r\n jobId: string\r\n}\r\n\r\nconst POINT_CLOUD_OPTIONS: import('../../../../../../../../types/global').FileAction[] = ['view', 'delete', 'info']\r\n\r\ninterface PointCloudsSectionProps {\r\n files: DbFile[]\r\n pointcloudApiUrl?: string\r\n buildingId?: number\r\n}\r\n\r\nexport function PointCloudsSection({ files, pointcloudApiUrl, buildingId }: PointCloudsSectionProps) {\r\n // Translation\r\n const t = useTranslations('PointCloudManagement')\r\n const API_BASE = pointcloudApiUrl ?? 'http://localhost:5101'\r\n\r\n // Get current user session (needed for uploadedBy)\r\n const { data: session } = useSession()\r\n\r\n // Point cloud context for loading/unloading clouds in the viewer\r\n const { dispatch: pointCloudDispatch } = React.useContext(PointCloudContext)\r\n\r\n // State for point clouds — includes isVisible to track which are loaded in viewer\r\n const [pointcloudsFiles, setPointcloudsFiles] = React.useState<(DbFile & { isVisible?: boolean })[]>(\r\n () => files.map(f => ({ ...f, isVisible: false }))\r\n )\r\n const esRef = React.useRef<EventSource | null>(null)\r\n const autoLoadedRef = React.useRef(false)\r\n\r\n const fileSyncKey = files\r\n .map(\r\n (file) =>\r\n `${file.id}:${file.name}:${Boolean(file.pointCloudUploaded)}:${Boolean(file.pointCloudPotreeConverted)}:${file.uploadedAt}:${file.potreeMetadataFileKey ?? ''}`\r\n )\r\n .join('|')\r\n\r\n // Sync incoming files from props while preserving isVisible state for known files\r\n React.useEffect(() => {\r\n setPointcloudsFiles(prev => {\r\n const prevMap = new Map(prev.map(f => [String(f.id), f]))\r\n return files.map(f => ({\r\n ...f,\r\n isVisible: prevMap.get(String(f.id))?.isVisible ?? false,\r\n }))\r\n })\r\n }, [fileSyncKey])\r\n\r\n // Auto-load the most recently uploaded converted file on first access\r\n React.useEffect(() => {\r\n if (autoLoadedRef.current || files.length === 0) return\r\n\r\n const readyFiles = files.filter(f => Boolean(f.pointCloudPotreeConverted))\r\n if (readyFiles.length === 0) return\r\n\r\n const lastFile = readyFiles.reduce((latest, f) =>\r\n new Date(f.uploadedAt) > new Date(latest.uploadedAt) ? f : latest\r\n )\r\n\r\n autoLoadedRef.current = true\r\n pointCloudDispatch({ type: 'LOAD_POINT_CLOUD', payload: { id: String(lastFile.id) } })\r\n setPointcloudsFiles(prev =>\r\n prev.map(f => ({ ...f, isVisible: String(f.id) === String(lastFile.id) }))\r\n )\r\n }, [fileSyncKey, files, pointCloudDispatch])\r\n\r\n React.useEffect(() => {\r\n // Cleanup SSE on unmount\r\n return () => {\r\n if (esRef.current) {\r\n esRef.current.close()\r\n }\r\n }\r\n }, [])\r\n\r\n // Toggle a point cloud on/off in the viewer via context\r\n const handleView = React.useCallback((file: DbFile, newVisibility: boolean) => {\r\n const id = String(file.id)\r\n if (newVisibility) {\r\n pointCloudDispatch({ type: 'LOAD_POINT_CLOUD', payload: { id } })\r\n } else {\r\n pointCloudDispatch({ type: 'UNLOAD_POINT_CLOUD', payload: { id } })\r\n }\r\n }, [pointCloudDispatch])\r\n\r\n const handleDeleteFile = React.useCallback(async (file: DbFile) => {\r\n const res = await fetch(`${API_BASE}/point-cloud/${encodeURIComponent(String(file.id))}`, {\r\n method: 'DELETE',\r\n })\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(msg || `Delete failed (${res.status})`)\r\n }\r\n }, [])\r\n\r\n const handleConvert = React.useCallback(async (file: DbFile) => {\r\n const res = await fetch(\r\n `${API_BASE}/point-cloud/${encodeURIComponent(String(file.id))}/convert-to-potree`,\r\n {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ options: { sampling_method: 'poisson' } }),\r\n }\r\n )\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(msg || `Conversion failed (${res.status})`)\r\n }\r\n\r\n const { jobId } = await res.json() as StartConversionResponse\r\n subscribeToProgress(jobId, file.id)\r\n }, [])\r\n\r\n const handleDeletePointCloud = React.useCallback((file: DbFile) => {\r\n pointCloudDispatch({ type: 'UNLOAD_POINT_CLOUD', payload: { id: String(file.id) } })\r\n }, [pointCloudDispatch])\r\n\r\n // Use the generic file actions hook\r\n const { handleAction, deleteDialog } = useFileActions({\r\n files: pointcloudsFiles,\r\n setFiles: setPointcloudsFiles as React.Dispatch<React.SetStateAction<(DbFile & { isVisible?: boolean })[]>>,\r\n buildingId: 0,\r\n handleDeleteFile,\r\n onView: handleView,\r\n onDelete: handleDeletePointCloud,\r\n })\r\n\r\n // Sort: visible (loaded) files at the top, preserving relative order within each group\r\n const sortedFiles = React.useMemo(() => {\r\n return [...pointcloudsFiles].sort((a, b) => {\r\n const aLoaded = a.isVisible !== false ? 1 : 0\r\n const bLoaded = b.isVisible !== false ? 1 : 0\r\n return bLoaded - aLoaded\r\n })\r\n }, [pointcloudsFiles])\r\n\r\n // Create point cloud entry — routed through the CDT proxy so that\r\n // organizationId is injected server-side from the authenticated session.\r\n async function createPointCloud(name: string, buildingId?: number): Promise<CreatePointCloudResponse> {\r\n const res = await fetch('/api/point-cloud', {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ name, buildingId }),\r\n })\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(`Create point-cloud failed: ${msg || res.status}`)\r\n }\r\n\r\n return res.json()\r\n }\r\n\r\n // Upload to presigned URL\r\n function uploadToPresignedUrl(\r\n url: string,\r\n file: File,\r\n onProgress?: (pct: number) => void\r\n ): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n try {\r\n const xhr = new XMLHttpRequest()\r\n xhr.open('PUT', url, true)\r\n\r\n if (onProgress) {\r\n xhr.upload.onprogress = (evt) => {\r\n if (evt.lengthComputable) {\r\n const pct = Math.round((evt.loaded / evt.total) * 100)\r\n onProgress(pct)\r\n }\r\n }\r\n }\r\n\r\n xhr.onload = () => {\r\n if (xhr.status >= 200 && xhr.status < 300) resolve()\r\n else reject(new Error(`Upload failed with status ${xhr.status}`))\r\n }\r\n\r\n xhr.onerror = () => reject(new Error('Network error during upload'))\r\n xhr.send(file)\r\n } catch (err) {\r\n reject(err)\r\n }\r\n })\r\n }\r\n\r\n // Start conversion\r\n async function startConversion(pointCloudId: string): Promise<StartConversionResponse> {\r\n const res = await fetch(\r\n `${API_BASE}/point-cloud/${encodeURIComponent(pointCloudId)}/convert-to-potree`,\r\n {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ options: { sampling_method: 'poisson' } }),\r\n }\r\n )\r\n\r\n if (!res.ok) {\r\n const msg = await res.text()\r\n throw new Error(`convert-to-potree failed: ${msg || res.status}`)\r\n }\r\n\r\n return res.json()\r\n }\r\n\r\n // Subscribe to conversion progress\r\n function subscribeToProgress(jobId: string, pointCloudId: number) {\r\n if (!jobId) return\r\n\r\n if (esRef.current) {\r\n esRef.current.close()\r\n }\r\n\r\n const es = new EventSource(\r\n `${API_BASE}/events/convert-progress/${encodeURIComponent(jobId)}`\r\n )\r\n esRef.current = es\r\n\r\n es.onopen = () => console.log('SSE connection opened')\r\n\r\n es.onerror = (event) => {\r\n console.error('SSE error:', event)\r\n // Update to failed state\r\n setPointcloudsFiles((prev) =>\r\n prev.map((pc) =>\r\n Number(pc.id) === pointCloudId\r\n ? { ...pc, status: 'idle', conversionProgress: 0 }\r\n : pc\r\n )\r\n )\r\n }\r\n\r\n es.addEventListener('progress', (event) => {\r\n try {\r\n const msgEvent = event as MessageEvent\r\n const data = JSON.parse(msgEvent.data)\r\n\r\n console.log('[progress]', data.status, data.message, data.progress)\r\n\r\n if (typeof data.progress === 'number') {\r\n setPointcloudsFiles((prev) =>\r\n prev.map((pc) =>\r\n Number(pc.id) === pointCloudId\r\n ? { ...pc, conversionProgress: data.progress }\r\n : pc\r\n )\r\n )\r\n }\r\n } catch (err) {\r\n console.error('Failed to parse progress event', err)\r\n }\r\n })\r\n\r\n es.addEventListener('finished', () => {\r\n console.log('[finished] Conversion completed')\r\n setPointcloudsFiles((prev) =>\r\n prev.map((pc) =>\r\n Number(pc.id) === pointCloudId\r\n ? { ...pc, status: 'idle', conversionProgress: 100 }\r\n : pc\r\n )\r\n )\r\n es.close()\r\n esRef.current = null\r\n window.location.reload()\r\n })\r\n\r\n es.addEventListener('failed', () => {\r\n console.log('[failed] Conversion failed')\r\n setPointcloudsFiles((prev) =>\r\n prev.map((pc) =>\r\n Number(pc.id) === pointCloudId\r\n ? { ...pc, status: 'idle', conversionProgress: 0 }\r\n : pc\r\n )\r\n )\r\n es.close()\r\n esRef.current = null\r\n })\r\n }\r\n\r\n // Handle file upload\r\n const handleFileUpload = async (file: File) => {\r\n if (!file) return\r\n\r\n const originalFileName = file.name\r\n const fileExt = originalFileName.toLowerCase()\r\n const baseFileName = originalFileName.replace(/\\.(laz|las)$/i, '')\r\n\r\n // Validate file extension\r\n if (!fileExt.endsWith('.laz') && !fileExt.endsWith('.las')) {\r\n alert('Please select a LAZ or LAS file.')\r\n return\r\n }\r\n\r\n // Check for duplicate names and append (1), (2), etc.\r\n let fileName = baseFileName\r\n let counter = 1\r\n const existingNames = pointcloudsFiles.map((pc) => pc.name)\r\n \r\n while (existingNames.includes(fileName)) {\r\n fileName = `${baseFileName} (${counter})`\r\n counter++\r\n }\r\n\r\n try {\r\n console.log('Creating point cloud entry...')\r\n const userEmail = session?.user?.email || null\r\n const { pointCloud, upload } = await createPointCloud(fileName, buildingId)\r\n console.log('Point cloud created:', pointCloud.id)\r\n\r\n console.log('Uploading file...')\r\n await uploadToPresignedUrl(upload.uploadUrl, file, (pct) => {\r\n console.log(`Upload progress: ${pct}%`)\r\n })\r\n console.log('Upload complete!')\r\n\r\n console.log('Starting conversion...')\r\n const conversion = await startConversion(pointCloud.id)\r\n console.log('Conversion started:', conversion.jobId)\r\n\r\n subscribeToProgress(conversion.jobId, Number(pointCloud.id))\r\n } catch (error) {\r\n console.error('Error uploading point cloud:', error)\r\n \r\n alert(\r\n error instanceof Error\r\n ? `Failed to upload: ${error.message}`\r\n : 'Failed to upload point cloud'\r\n )\r\n }\r\n }\r\n\r\n // Handle upload button click\r\n const handleUpload = () => {\r\n const input = document.createElement('input')\r\n input.type = 'file'\r\n input.accept = '.laz,.las'\r\n input.onchange = (e) => {\r\n const file = (e.target as HTMLInputElement).files?.[0]\r\n if (file) {\r\n handleFileUpload(file)\r\n }\r\n }\r\n input.click()\r\n }\r\n\r\n return (\r\n <>\r\n <CollapsibleSection\r\n title={t('title')}\r\n icon={LR.Grip}\r\n className=\"h-1/2 overflow-y-auto\"\r\n itemCount={pointcloudsFiles.length}\r\n onAddItem={handleUpload}\r\n addItemTitle={t('uploadTitle')}\r\n >\r\n <div className=\"space-y-1\">\r\n {sortedFiles.length === 0 ? (\r\n <div className=\"px-2 py-3 text-sm text-muted-foreground text-center\">\r\n {t('noPointClouds')}\r\n </div>\r\n ) : (\r\n sortedFiles.map((file) => (\r\n <div key={file.id} className=\"flex items-center gap-1\">\r\n <div className=\"flex-1 min-w-0\">\r\n <FileItemComponent\r\n file={file}\r\n onAction={handleAction}\r\n options={POINT_CLOUD_OPTIONS}\r\n />\r\n </div>\r\n {Boolean(file.pointCloudUploaded) && !Boolean(file.pointCloudPotreeConverted) && (\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"h-6 w-6 p-0\"\r\n onClick={() => void handleConvert(file)}\r\n title={t('convertTitle')}\r\n >\r\n <LR.RefreshCw className=\"h-3 w-3\" />\r\n </Button>\r\n )}\r\n </div>\r\n ))\r\n )}\r\n </div>\r\n </CollapsibleSection>\r\n\r\n <ConfirmDialog\r\n isOpen={deleteDialog.isOpen}\r\n isDeleting={deleteDialog.isDeleting}\r\n onOpenChange={deleteDialog.onOpenChange}\r\n handleConfirm={deleteDialog.onConfirm}\r\n itemName={deleteDialog.itemName}\r\n dataType=\"file\"\r\n />\r\n </>\r\n )\r\n}\r\n"],"mappings":"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","names":["buildingId"]}
@@ -1 +1 @@
1
- {"version":3,"file":"useCaptureScreenshot.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/useCaptureScreenshot.ts"],"names":[],"mappings":"AAuBA,wBAAgB,oBAAoB,IAAI,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyFnE"}
1
+ {"version":3,"file":"useCaptureScreenshot.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/useCaptureScreenshot.ts"],"names":[],"mappings":"AAuBA,wBAAgB,oBAAoB,IAAI,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiInE"}
@@ -24,56 +24,85 @@ function useCaptureScreenshot() {
24
24
  const { map } = mapState.map;
25
25
  const { state: bimState } = React.useContext(BimContext);
26
26
  const { world } = bimState.bim;
27
- return React.useCallback(() => {
28
- const capture = async () => {
27
+ return React.useCallback(async () => {
28
+ try {
29
29
  let viewerDataUrl = null;
30
+ let viewerCanvas = null;
30
31
  let viewerBounds = null;
31
- let viewerCanvasElement = null;
32
32
  if (currentViewer === ViewerNames.map && map) {
33
- await new Promise((res) => {
34
- map.once("render", () => {
35
- const canvas = map.getCanvas();
36
- viewerCanvasElement = canvas;
37
- viewerBounds = canvas.getBoundingClientRect();
38
- try {
39
- viewerDataUrl = canvas.toDataURL("image/png");
40
- } catch (e) {
41
- }
42
- res();
43
- });
44
- map.triggerRepaint();
45
- });
33
+ await Promise.race([
34
+ new Promise((res) => {
35
+ map.once("render", () => {
36
+ viewerCanvas = map.getCanvas();
37
+ viewerBounds = viewerCanvas.getBoundingClientRect();
38
+ try {
39
+ viewerDataUrl = viewerCanvas.toDataURL("image/png");
40
+ } catch (e) {
41
+ }
42
+ res();
43
+ });
44
+ map.triggerRepaint();
45
+ }),
46
+ new Promise((r) => setTimeout(r, 3e3))
47
+ ]);
46
48
  } else if (currentViewer === ViewerNames.bim && (world == null ? void 0 : world.renderer) && (world == null ? void 0 : world.scene) && (world == null ? void 0 : world.camera)) {
47
49
  world.renderer.three.render(world.scene.three, world.camera.three);
48
- const canvas = document.querySelector("#bim-viewer-container canvas");
49
- if (canvas) {
50
- viewerCanvasElement = canvas;
51
- viewerBounds = canvas.getBoundingClientRect();
50
+ viewerCanvas = document.querySelector("#bim-viewer-container canvas");
51
+ if (viewerCanvas) {
52
+ viewerBounds = viewerCanvas.getBoundingClientRect();
52
53
  try {
53
- viewerDataUrl = canvas.toDataURL("image/png");
54
+ viewerDataUrl = viewerCanvas.toDataURL("image/png");
54
55
  } catch (e) {
55
56
  }
56
57
  }
57
58
  } else {
58
- const canvas = currentViewer === ViewerNames.pointcloud ? document.querySelector("#pointcloud-viewer-container canvas") : document.querySelector("canvas");
59
- if (canvas) {
60
- viewerCanvasElement = canvas;
61
- viewerBounds = canvas.getBoundingClientRect();
59
+ const sel = currentViewer === ViewerNames.pointcloud ? "#pointcloud-viewer-container" : null;
60
+ viewerCanvas = sel ? document.querySelector(`${sel} canvas`) : document.querySelector("canvas");
61
+ if (viewerCanvas) {
62
+ viewerBounds = viewerCanvas.getBoundingClientRect();
62
63
  try {
63
- viewerDataUrl = canvas.toDataURL("image/png");
64
+ viewerDataUrl = viewerCanvas.toDataURL("image/png");
64
65
  } catch (e) {
65
66
  }
66
67
  }
67
68
  }
68
69
  const { default: html2canvas } = await import("html2canvas");
69
- const uiCanvas = await html2canvas(document.body, {
70
+ let dialogPortal = null;
71
+ const dialogEl = document.querySelector('[role="dialog"]');
72
+ if (dialogEl) {
73
+ let node = dialogEl;
74
+ while (node && node.parentElement !== document.body) {
75
+ node = node.parentElement;
76
+ }
77
+ if (node && node !== document.body) dialogPortal = node;
78
+ }
79
+ if (dialogPortal) dialogPortal.style.display = "none";
80
+ const captureUi = (ignoreImages) => html2canvas(document.body, {
70
81
  useCORS: true,
71
82
  allowTaint: false,
72
83
  logging: false,
73
- removeContainer: true,
74
84
  scale: window.devicePixelRatio || 1,
75
- ignoreElements: (el) => el === viewerCanvasElement
85
+ width: window.innerWidth,
86
+ height: window.innerHeight,
87
+ x: window.scrollX,
88
+ y: window.scrollY,
89
+ windowWidth: window.innerWidth,
90
+ windowHeight: window.innerHeight,
91
+ scrollX: -window.scrollX,
92
+ scrollY: -window.scrollY,
93
+ ignoreElements: (el) => el === viewerCanvas || ignoreImages && el.tagName === "IMG"
76
94
  });
95
+ let uiCanvas;
96
+ try {
97
+ try {
98
+ uiCanvas = await captureUi(false);
99
+ } catch (err) {
100
+ console.warn("[useCaptureScreenshot] retrying without <img> elements after taint error:", err);
101
+ uiCanvas = await captureUi(true);
102
+ }
103
+ } finally {
104
+ if (dialogPortal) dialogPortal.style.display = "";
105
+ }
77
106
  if (viewerDataUrl && viewerBounds) {
78
107
  const ctx = uiCanvas.getContext("2d");
79
108
  if (ctx) {
@@ -100,8 +129,10 @@ function useCaptureScreenshot() {
100
129
  blobToBase64(blob).then(resolve);
101
130
  }, "image/png", 1);
102
131
  });
103
- };
104
- return capture().catch(() => null);
132
+ } catch (err) {
133
+ console.error("[useCaptureScreenshot] failed:", err);
134
+ throw err instanceof Error ? err : new Error(String(err));
135
+ }
105
136
  }, [currentViewer, map, world]);
106
137
  }
107
138
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/hooks/useCaptureScreenshot.ts"],"sourcesContent":["'use client'\r\n\r\nimport * as React from 'react'\r\nimport { MenusContext, MapContext, BimContext } from '../store'\r\nimport { ViewerNames } from '../types'\r\n\r\nfunction blobToBase64(blob: Blob): Promise<string> {\r\n return new Promise((resolve) => {\r\n const reader = new FileReader()\r\n reader.onload = () => resolve(reader.result as string)\r\n reader.readAsDataURL(blob)\r\n })\r\n}\r\n\r\nfunction loadImage(src: string): Promise<HTMLImageElement> {\r\n return new Promise((resolve, reject) => {\r\n const img = new Image()\r\n img.onload = () => resolve(img)\r\n img.onerror = reject\r\n img.src = src\r\n })\r\n}\r\n\r\nexport function useCaptureScreenshot(): () => Promise<string | null> {\r\n const { state: menusState } = React.useContext(MenusContext)\r\n const { currentViewer } = menusState.menus\r\n\r\n const { state: mapState } = React.useContext(MapContext)\r\n const { map } = mapState.map\r\n\r\n const { state: bimState } = React.useContext(BimContext)\r\n const { world } = bimState.bim\r\n\r\n return React.useCallback((): Promise<string | null> => {\r\n const capture = async (): Promise<string | null> => {\r\n let viewerDataUrl: string | null = null\r\n let viewerBounds: DOMRect | null = null\r\n let viewerCanvasElement: HTMLCanvasElement | null = null\r\n\r\n // WebGL canvases clear their back-buffer after each frame, so we must\r\n // read them synchronously at the exact moment they have valid content —\r\n // before yielding to the event loop for html2canvas.\r\n if (currentViewer === ViewerNames.map && map) {\r\n await new Promise<void>((res) => {\r\n map.once('render', () => {\r\n const canvas = map.getCanvas()\r\n viewerCanvasElement = canvas\r\n viewerBounds = canvas.getBoundingClientRect()\r\n try { viewerDataUrl = canvas.toDataURL('image/png') } catch { /* cross-origin taint */ }\r\n res()\r\n })\r\n map.triggerRepaint()\r\n })\r\n } else if (currentViewer === ViewerNames.bim && world?.renderer && world?.scene && world?.camera) {\r\n world.renderer.three.render(world.scene.three, world.camera.three)\r\n const canvas = document.querySelector<HTMLCanvasElement>('#bim-viewer-container canvas')\r\n if (canvas) {\r\n viewerCanvasElement = canvas\r\n viewerBounds = canvas.getBoundingClientRect()\r\n try { viewerDataUrl = canvas.toDataURL('image/png') } catch { /* ignore */ }\r\n }\r\n } else {\r\n const canvas = currentViewer === ViewerNames.pointcloud\r\n ? document.querySelector<HTMLCanvasElement>('#pointcloud-viewer-container canvas')\r\n : document.querySelector<HTMLCanvasElement>('canvas')\r\n if (canvas) {\r\n viewerCanvasElement = canvas\r\n viewerBounds = canvas.getBoundingClientRect()\r\n try { viewerDataUrl = canvas.toDataURL('image/png') } catch { /* ignore */ }\r\n }\r\n }\r\n\r\n // Capture the full DOM sidebar, toolbar, all UI panels but skip the\r\n // viewer canvas (we have its pixels already; html2canvas can't read WebGL).\r\n const { default: html2canvas } = await import('html2canvas')\r\n const uiCanvas = await html2canvas(document.body, {\r\n useCORS: true,\r\n allowTaint: false,\r\n logging: false,\r\n removeContainer: true,\r\n scale: window.devicePixelRatio || 1,\r\n ignoreElements: (el) => el === viewerCanvasElement,\r\n })\r\n\r\n // Draw the pre-captured viewer image into the blank region.\r\n if (viewerDataUrl && viewerBounds) {\r\n const ctx = uiCanvas.getContext('2d')\r\n if (ctx) {\r\n try {\r\n const img = await loadImage(viewerDataUrl)\r\n const dpr = window.devicePixelRatio || 1\r\n ctx.drawImage(\r\n img,\r\n viewerBounds.left * dpr,\r\n viewerBounds.top * dpr,\r\n viewerBounds.width * dpr,\r\n viewerBounds.height * dpr,\r\n )\r\n } catch { /* skip compositing on error */ }\r\n }\r\n }\r\n\r\n return new Promise<string | null>((resolve) => {\r\n uiCanvas.toBlob((blob) => {\r\n if (!blob) { resolve(null); return }\r\n blobToBase64(blob).then(resolve)\r\n }, 'image/png', 1.0)\r\n })\r\n }\r\n\r\n return capture().catch(() => null)\r\n }, [currentViewer, map, world])\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/core/hooks/useCaptureScreenshot.ts"],"sourcesContent":["'use client'\r\n\r\nimport * as React from 'react'\r\nimport { MenusContext, MapContext, BimContext } from '../store'\r\nimport { ViewerNames } from '../types'\r\n\r\nfunction blobToBase64(blob: Blob): Promise<string> {\r\n return new Promise((resolve) => {\r\n const reader = new FileReader()\r\n reader.onload = () => resolve(reader.result as string)\r\n reader.readAsDataURL(blob)\r\n })\r\n}\r\n\r\nfunction loadImage(src: string): Promise<HTMLImageElement> {\r\n return new Promise((resolve, reject) => {\r\n const img = new Image()\r\n img.onload = () => resolve(img)\r\n img.onerror = reject\r\n img.src = src\r\n })\r\n}\r\n\r\nexport function useCaptureScreenshot(): () => Promise<string | null> {\r\n const { state: menusState } = React.useContext(MenusContext)\r\n const { currentViewer } = menusState.menus\r\n\r\n const { state: mapState } = React.useContext(MapContext)\r\n const { map } = mapState.map\r\n\r\n const { state: bimState } = React.useContext(BimContext)\r\n const { world } = bimState.bim\r\n\r\n return React.useCallback(async (): Promise<string | null> => {\r\n try {\r\n let viewerDataUrl: string | null = null\r\n let viewerCanvas: HTMLCanvasElement | null = null\r\n let viewerBounds: DOMRect | null = null\r\n\r\n if (currentViewer === ViewerNames.map && map) {\r\n await Promise.race([\r\n new Promise<void>((res) => {\r\n map.once('render', () => {\r\n viewerCanvas = map.getCanvas()\r\n viewerBounds = viewerCanvas.getBoundingClientRect()\r\n try { viewerDataUrl = viewerCanvas.toDataURL('image/png') } catch { /* cross-origin */ }\r\n res()\r\n })\r\n map.triggerRepaint()\r\n }),\r\n new Promise<void>((r) => setTimeout(r, 3000)),\r\n ])\r\n } else if (currentViewer === ViewerNames.bim && world?.renderer && world?.scene && world?.camera) {\r\n world.renderer.three.render(world.scene.three, world.camera.three)\r\n viewerCanvas = document.querySelector<HTMLCanvasElement>('#bim-viewer-container canvas')\r\n if (viewerCanvas) {\r\n viewerBounds = viewerCanvas.getBoundingClientRect()\r\n try { viewerDataUrl = viewerCanvas.toDataURL('image/png') } catch { /* ignore */ }\r\n }\r\n } else {\r\n const sel = currentViewer === ViewerNames.pointcloud ? '#pointcloud-viewer-container' : null\r\n viewerCanvas = sel\r\n ? document.querySelector<HTMLCanvasElement>(`${sel} canvas`)\r\n : document.querySelector<HTMLCanvasElement>('canvas')\r\n if (viewerCanvas) {\r\n viewerBounds = viewerCanvas.getBoundingClientRect()\r\n try { viewerDataUrl = viewerCanvas.toDataURL('image/png') } catch { /* ignore */ }\r\n }\r\n }\r\n\r\n const { default: html2canvas } = await import('html2canvas')\r\n\r\n // Find the dialog portal: walk up from [role=\"dialog\"] to its direct\r\n // child-of-body ancestor. We hide it with display:none before capture\r\n // and restore it after. This avoids ignoreElements on a body child,\r\n // which causes \"Unable to find element in cloned iframe\".\r\n let dialogPortal: HTMLElement | null = null\r\n const dialogEl = document.querySelector('[role=\"dialog\"]')\r\n if (dialogEl) {\r\n let node: Element | null = dialogEl\r\n while (node && node.parentElement !== document.body) {\r\n node = node.parentElement\r\n }\r\n if (node && node !== document.body) dialogPortal = node as HTMLElement\r\n }\r\n\r\n if (dialogPortal) dialogPortal.style.display = 'none'\r\n\r\n const captureUi = (ignoreImages: boolean) => html2canvas(document.body, {\r\n useCORS: true,\r\n allowTaint: false,\r\n logging: false,\r\n scale: window.devicePixelRatio || 1,\r\n width: window.innerWidth,\r\n height: window.innerHeight,\r\n x: window.scrollX,\r\n y: window.scrollY,\r\n windowWidth: window.innerWidth,\r\n windowHeight: window.innerHeight,\r\n scrollX: -window.scrollX,\r\n scrollY: -window.scrollY,\r\n ignoreElements: (el: Element) =>\r\n el === viewerCanvas || (ignoreImages && el.tagName === 'IMG'),\r\n })\r\n\r\n let uiCanvas: HTMLCanvasElement\r\n try {\r\n try {\r\n uiCanvas = await captureUi(false)\r\n } catch (err) {\r\n // Safari throws \"The operation is insecure\" when a cross-origin\r\n // image (org logo, user avatar, ...) taints html2canvas's internal\r\n // canvas even with allowTaint:false — retry once with all <img>\r\n // elements excluded so the capture still succeeds, just without\r\n // those images. Chrome is more lenient here, which is why this\r\n // only shows up on mobile Safari.\r\n console.warn('[useCaptureScreenshot] retrying without <img> elements after taint error:', err)\r\n uiCanvas = await captureUi(true)\r\n }\r\n } finally {\r\n if (dialogPortal) dialogPortal.style.display = ''\r\n }\r\n\r\n // Composite the pre-captured viewer image at the exact canvas bounds.\r\n if (viewerDataUrl && viewerBounds) {\r\n const ctx = uiCanvas.getContext('2d')\r\n if (ctx) {\r\n try {\r\n const img = await loadImage(viewerDataUrl)\r\n const dpr = window.devicePixelRatio || 1\r\n ctx.drawImage(\r\n img,\r\n viewerBounds.left * dpr,\r\n viewerBounds.top * dpr,\r\n viewerBounds.width * dpr,\r\n viewerBounds.height * dpr,\r\n )\r\n } catch { /* skip compositing on error */ }\r\n }\r\n }\r\n\r\n return new Promise<string | null>((resolve) => {\r\n uiCanvas.toBlob((blob) => {\r\n if (!blob) { resolve(null); return }\r\n blobToBase64(blob).then(resolve)\r\n }, 'image/png', 1.0)\r\n })\r\n } catch (err) {\r\n console.error('[useCaptureScreenshot] failed:', err)\r\n throw err instanceof Error ? err : new Error(String(err))\r\n }\r\n }, [currentViewer, map, world])\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,18 @@
1
+ export type ViewerPosition = {
2
+ type: 'map';
3
+ lat: number;
4
+ lng: number;
5
+ zoom: number;
6
+ bearing: number;
7
+ pitch: number;
8
+ } | {
9
+ type: 'bim' | 'pointcloud';
10
+ camX: number;
11
+ camY: number;
12
+ camZ: number;
13
+ tarX: number;
14
+ tarY: number;
15
+ tarZ: number;
16
+ };
17
+ export declare function useCurrentViewerPosition(): () => ViewerPosition | null;
18
+ //# sourceMappingURL=useCurrentViewerPosition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCurrentViewerPosition.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/useCurrentViewerPosition.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,IAAI,EAAE,KAAK,GAAG,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtH,wBAAgB,wBAAwB,IAAI,MAAM,cAAc,GAAG,IAAI,CA4DtE"}
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as THREE from "three";
4
+ import { MenusContext, MapContext, BimContext, PointCloudContext } from "../store";
5
+ import { ViewerNames } from "../types";
6
+ import { getCameraPosition } from "../components/viewers/pointcloud/src/tools/SharePointCloudTool/src/getCameraPosition";
7
+ function useCurrentViewerPosition() {
8
+ const { state: menusState } = React.useContext(MenusContext);
9
+ const { currentViewer } = menusState.menus;
10
+ const { state: mapState } = React.useContext(MapContext);
11
+ const { map } = mapState.map;
12
+ const { state: bimState } = React.useContext(BimContext);
13
+ const { world } = bimState.bim;
14
+ const { state: pcState } = React.useContext(PointCloudContext);
15
+ const { viewer } = pcState.pointcloud;
16
+ return React.useCallback(() => {
17
+ if (currentViewer === ViewerNames.map && map) {
18
+ const center = map.getCenter();
19
+ return {
20
+ type: "map",
21
+ lat: parseFloat(center.lat.toFixed(7)),
22
+ lng: parseFloat(center.lng.toFixed(7)),
23
+ zoom: parseFloat(map.getZoom().toFixed(3)),
24
+ bearing: parseFloat(map.getBearing().toFixed(1)),
25
+ pitch: parseFloat(map.getPitch().toFixed(1))
26
+ };
27
+ }
28
+ if (currentViewer === ViewerNames.bim && (world == null ? void 0 : world.camera)) {
29
+ try {
30
+ const pos = new THREE.Vector3();
31
+ const tar = new THREE.Vector3();
32
+ world.camera.controls.getPosition(pos);
33
+ world.camera.controls.getTarget(tar);
34
+ return {
35
+ type: "bim",
36
+ camX: parseFloat(pos.x.toFixed(3)),
37
+ camY: parseFloat(pos.y.toFixed(3)),
38
+ camZ: parseFloat(pos.z.toFixed(3)),
39
+ tarX: parseFloat(tar.x.toFixed(3)),
40
+ tarY: parseFloat(tar.y.toFixed(3)),
41
+ tarZ: parseFloat(tar.z.toFixed(3))
42
+ };
43
+ } catch (e) {
44
+ return null;
45
+ }
46
+ }
47
+ if (currentViewer === ViewerNames.pointcloud && viewer) {
48
+ const cam = getCameraPosition(viewer);
49
+ if (!cam) return null;
50
+ return {
51
+ type: "pointcloud",
52
+ camX: parseFloat(cam.position.x.toFixed(3)),
53
+ camY: parseFloat(cam.position.y.toFixed(3)),
54
+ camZ: parseFloat(cam.position.z.toFixed(3)),
55
+ tarX: parseFloat(cam.target.x.toFixed(3)),
56
+ tarY: parseFloat(cam.target.y.toFixed(3)),
57
+ tarZ: parseFloat(cam.target.z.toFixed(3))
58
+ };
59
+ }
60
+ return null;
61
+ }, [currentViewer, map, world, viewer]);
62
+ }
63
+ export {
64
+ useCurrentViewerPosition
65
+ };
66
+ //# sourceMappingURL=useCurrentViewerPosition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/hooks/useCurrentViewerPosition.ts"],"sourcesContent":["'use client'\r\n\r\nimport * as React from 'react'\r\nimport * as THREE from 'three'\r\nimport { MenusContext, MapContext, BimContext, PointCloudContext } from '../store'\r\nimport { ViewerNames } from '../types'\r\nimport { getCameraPosition } from '../components/viewers/pointcloud/src/tools/SharePointCloudTool/src/getCameraPosition'\r\n\r\nexport type ViewerPosition =\r\n | { type: 'map'; lat: number; lng: number; zoom: number; bearing: number; pitch: number }\r\n | { type: 'bim' | 'pointcloud'; camX: number; camY: number; camZ: number; tarX: number; tarY: number; tarZ: number }\r\n\r\nexport function useCurrentViewerPosition(): () => ViewerPosition | null {\r\n const { state: menusState } = React.useContext(MenusContext)\r\n const { currentViewer } = menusState.menus\r\n\r\n const { state: mapState } = React.useContext(MapContext)\r\n const { map } = mapState.map\r\n\r\n const { state: bimState } = React.useContext(BimContext)\r\n const { world } = bimState.bim\r\n\r\n const { state: pcState } = React.useContext(PointCloudContext)\r\n const { viewer } = pcState.pointcloud\r\n\r\n return React.useCallback((): ViewerPosition | null => {\r\n if (currentViewer === ViewerNames.map && map) {\r\n const center = map.getCenter()\r\n return {\r\n type: 'map',\r\n lat: parseFloat(center.lat.toFixed(7)),\r\n lng: parseFloat(center.lng.toFixed(7)),\r\n zoom: parseFloat(map.getZoom().toFixed(3)),\r\n bearing: parseFloat(map.getBearing().toFixed(1)),\r\n pitch: parseFloat(map.getPitch().toFixed(1)),\r\n }\r\n }\r\n\r\n if (currentViewer === ViewerNames.bim && world?.camera) {\r\n try {\r\n const pos = new THREE.Vector3()\r\n const tar = new THREE.Vector3()\r\n world.camera.controls.getPosition(pos)\r\n world.camera.controls.getTarget(tar)\r\n return {\r\n type: 'bim',\r\n camX: parseFloat(pos.x.toFixed(3)),\r\n camY: parseFloat(pos.y.toFixed(3)),\r\n camZ: parseFloat(pos.z.toFixed(3)),\r\n tarX: parseFloat(tar.x.toFixed(3)),\r\n tarY: parseFloat(tar.y.toFixed(3)),\r\n tarZ: parseFloat(tar.z.toFixed(3)),\r\n }\r\n } catch { return null }\r\n }\r\n\r\n if (currentViewer === ViewerNames.pointcloud && viewer) {\r\n const cam = getCameraPosition(viewer)\r\n if (!cam) return null\r\n return {\r\n type: 'pointcloud',\r\n camX: parseFloat(cam.position.x.toFixed(3)),\r\n camY: parseFloat(cam.position.y.toFixed(3)),\r\n camZ: parseFloat(cam.position.z.toFixed(3)),\r\n tarX: parseFloat(cam.target.x.toFixed(3)),\r\n tarY: parseFloat(cam.target.y.toFixed(3)),\r\n tarZ: parseFloat(cam.target.z.toFixed(3)),\r\n }\r\n }\r\n\r\n return null\r\n }, [currentViewer, map, world, viewer])\r\n}\r\n"],"mappings":";AAEA,YAAY,WAAW;AACvB,YAAY,WAAW;AACvB,SAAS,cAAc,YAAY,YAAY,yBAAyB;AACxE,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAM3B,SAAS,2BAAwD;AACtE,QAAM,EAAE,OAAO,WAAW,IAAI,MAAM,WAAW,YAAY;AAC3D,QAAM,EAAE,cAAc,IAAI,WAAW;AAErC,QAAM,EAAE,OAAO,SAAS,IAAI,MAAM,WAAW,UAAU;AACvD,QAAM,EAAE,IAAI,IAAI,SAAS;AAEzB,QAAM,EAAE,OAAO,SAAS,IAAI,MAAM,WAAW,UAAU;AACvD,QAAM,EAAE,MAAM,IAAI,SAAS;AAE3B,QAAM,EAAE,OAAO,QAAQ,IAAI,MAAM,WAAW,iBAAiB;AAC7D,QAAM,EAAE,OAAO,IAAI,QAAQ;AAE3B,SAAO,MAAM,YAAY,MAA6B;AACpD,QAAI,kBAAkB,YAAY,OAAO,KAAK;AAC5C,YAAM,SAAS,IAAI,UAAU;AAC7B,aAAO;AAAA,QACL,MAAM;AAAA,QACN,KAAK,WAAW,OAAO,IAAI,QAAQ,CAAC,CAAC;AAAA,QACrC,KAAK,WAAW,OAAO,IAAI,QAAQ,CAAC,CAAC;AAAA,QACrC,MAAM,WAAW,IAAI,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAAA,QACzC,SAAS,WAAW,IAAI,WAAW,EAAE,QAAQ,CAAC,CAAC;AAAA,QAC/C,OAAO,WAAW,IAAI,SAAS,EAAE,QAAQ,CAAC,CAAC;AAAA,MAC7C;AAAA,IACF;AAEA,QAAI,kBAAkB,YAAY,QAAO,+BAAO,SAAQ;AACtD,UAAI;AACF,cAAM,MAAM,IAAI,MAAM,QAAQ;AAC9B,cAAM,MAAM,IAAI,MAAM,QAAQ;AAC9B,cAAM,OAAO,SAAS,YAAY,GAAG;AACrC,cAAM,OAAO,SAAS,UAAU,GAAG;AACnC,eAAO;AAAA,UACL,MAAM;AAAA,UACN,MAAM,WAAW,IAAI,EAAE,QAAQ,CAAC,CAAC;AAAA,UACjC,MAAM,WAAW,IAAI,EAAE,QAAQ,CAAC,CAAC;AAAA,UACjC,MAAM,WAAW,IAAI,EAAE,QAAQ,CAAC,CAAC;AAAA,UACjC,MAAM,WAAW,IAAI,EAAE,QAAQ,CAAC,CAAC;AAAA,UACjC,MAAM,WAAW,IAAI,EAAE,QAAQ,CAAC,CAAC;AAAA,UACjC,MAAM,WAAW,IAAI,EAAE,QAAQ,CAAC,CAAC;AAAA,QACnC;AAAA,MACF,SAAQ;AAAE,eAAO;AAAA,MAAK;AAAA,IACxB;AAEA,QAAI,kBAAkB,YAAY,cAAc,QAAQ;AACtD,YAAM,MAAM,kBAAkB,MAAM;AACpC,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO;AAAA,QACL,MAAM;AAAA,QACN,MAAM,WAAW,IAAI,SAAS,EAAE,QAAQ,CAAC,CAAC;AAAA,QAC1C,MAAM,WAAW,IAAI,SAAS,EAAE,QAAQ,CAAC,CAAC;AAAA,QAC1C,MAAM,WAAW,IAAI,SAAS,EAAE,QAAQ,CAAC,CAAC;AAAA,QAC1C,MAAM,WAAW,IAAI,OAAO,EAAE,QAAQ,CAAC,CAAC;AAAA,QACxC,MAAM,WAAW,IAAI,OAAO,EAAE,QAAQ,CAAC,CAAC;AAAA,QACxC,MAAM,WAAW,IAAI,OAAO,EAAE,QAAQ,CAAC,CAAC;AAAA,MAC1C;AAAA,IACF;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,KAAK,OAAO,MAAM,CAAC;AACxC;","names":[]}
@@ -0,0 +1,2 @@
1
+ export declare function useShareUrl(): () => Promise<string>;
2
+ //# sourceMappingURL=useShareUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useShareUrl.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/useShareUrl.ts"],"names":[],"mappings":"AAQA,wBAAgB,WAAW,IAAI,MAAM,OAAO,CAAC,MAAM,CAAC,CAwEnD"}
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as THREE from "three";
4
+ import { MenusContext, MapContext, BimContext, PointCloudContext, BuildingsContext } from "../store";
5
+ import { ViewerNames } from "../types";
6
+ import { getCameraPosition } from "../components/viewers/pointcloud/src/tools/SharePointCloudTool/src/getCameraPosition";
7
+ function useShareUrl() {
8
+ const { state: menusState } = React.useContext(MenusContext);
9
+ const { currentViewer } = menusState.menus;
10
+ const { state: mapState } = React.useContext(MapContext);
11
+ const { map } = mapState.map;
12
+ const { state: bimState } = React.useContext(BimContext);
13
+ const { world } = bimState.bim;
14
+ const { state: pcState } = React.useContext(PointCloudContext);
15
+ const { viewer } = pcState.pointcloud;
16
+ const { state: buildingState } = React.useContext(BuildingsContext);
17
+ const { building } = buildingState.buildings;
18
+ return React.useCallback(async () => {
19
+ const origin = window.location.origin;
20
+ const pathname = window.location.pathname;
21
+ const fallback = window.location.href;
22
+ if (currentViewer === ViewerNames.map && map) {
23
+ const params = new URLSearchParams({
24
+ viewer: "map",
25
+ lat: map.getCenter().lat.toFixed(7),
26
+ lng: map.getCenter().lng.toFixed(7),
27
+ zoom: map.getZoom().toFixed(3),
28
+ bearing: map.getBearing().toFixed(1),
29
+ pitch: map.getPitch().toFixed(1)
30
+ });
31
+ return `${origin}${pathname}?${params.toString()}`;
32
+ }
33
+ if (currentViewer === ViewerNames.bim && (world == null ? void 0 : world.camera)) {
34
+ try {
35
+ const pos = new THREE.Vector3();
36
+ const tar = new THREE.Vector3();
37
+ world.camera.controls.getPosition(pos);
38
+ world.camera.controls.getTarget(tar);
39
+ const params = new URLSearchParams(new URL(window.location.href).search);
40
+ params.set("viewer", "bim");
41
+ params.set("camX", pos.x.toFixed(3));
42
+ params.set("camY", pos.y.toFixed(3));
43
+ params.set("camZ", pos.z.toFixed(3));
44
+ params.set("tarX", tar.x.toFixed(3));
45
+ params.set("tarY", tar.y.toFixed(3));
46
+ params.set("tarZ", tar.z.toFixed(3));
47
+ if (building) params.set("buildingId", String(building.id));
48
+ return `${origin}${pathname}?${params.toString()}`;
49
+ } catch (e) {
50
+ return fallback;
51
+ }
52
+ }
53
+ if (currentViewer === ViewerNames.pointcloud && viewer) {
54
+ const cam = getCameraPosition(viewer);
55
+ if (cam) {
56
+ const params = new URLSearchParams(new URL(window.location.href).search);
57
+ params.set("viewer", "pointcloud");
58
+ params.set("camX", cam.position.x.toFixed(3));
59
+ params.set("camY", cam.position.y.toFixed(3));
60
+ params.set("camZ", cam.position.z.toFixed(3));
61
+ params.set("tarX", cam.target.x.toFixed(3));
62
+ params.set("tarY", cam.target.y.toFixed(3));
63
+ params.set("tarZ", cam.target.z.toFixed(3));
64
+ if (building) params.set("buildingId", String(building.id));
65
+ return `${origin}${pathname}?${params.toString()}`;
66
+ }
67
+ }
68
+ return fallback;
69
+ }, [currentViewer, map, world, viewer, building]);
70
+ }
71
+ export {
72
+ useShareUrl
73
+ };
74
+ //# sourceMappingURL=useShareUrl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/hooks/useShareUrl.ts"],"sourcesContent":["'use client'\r\n\r\nimport * as React from 'react'\r\nimport * as THREE from 'three'\r\nimport { MenusContext, MapContext, BimContext, PointCloudContext, BuildingsContext } from '../store'\r\nimport { ViewerNames } from '../types'\r\nimport { getCameraPosition } from '../components/viewers/pointcloud/src/tools/SharePointCloudTool/src/getCameraPosition'\r\n\r\nexport function useShareUrl(): () => Promise<string> {\r\n const { state: menusState } = React.useContext(MenusContext)\r\n const { currentViewer } = menusState.menus\r\n\r\n const { state: mapState } = React.useContext(MapContext)\r\n const { map } = mapState.map\r\n\r\n const { state: bimState } = React.useContext(BimContext)\r\n const { world } = bimState.bim\r\n\r\n const { state: pcState } = React.useContext(PointCloudContext)\r\n const { viewer } = pcState.pointcloud\r\n\r\n const { state: buildingState } = React.useContext(BuildingsContext)\r\n const { building } = buildingState.buildings\r\n\r\n return React.useCallback(async (): Promise<string> => {\r\n const origin = window.location.origin\r\n const pathname = window.location.pathname\r\n const fallback = window.location.href\r\n\r\n if (currentViewer === ViewerNames.map && map) {\r\n const params = new URLSearchParams({\r\n viewer: 'map',\r\n lat: map.getCenter().lat.toFixed(7),\r\n lng: map.getCenter().lng.toFixed(7),\r\n zoom: map.getZoom().toFixed(3),\r\n bearing: map.getBearing().toFixed(1),\r\n pitch: map.getPitch().toFixed(1),\r\n })\r\n return `${origin}${pathname}?${params.toString()}`\r\n }\r\n\r\n if (currentViewer === ViewerNames.bim && world?.camera) {\r\n try {\r\n const pos = new THREE.Vector3()\r\n const tar = new THREE.Vector3()\r\n world.camera.controls.getPosition(pos)\r\n world.camera.controls.getTarget(tar)\r\n const params = new URLSearchParams(new URL(window.location.href).search)\r\n params.set('viewer', 'bim')\r\n params.set('camX', pos.x.toFixed(3))\r\n params.set('camY', pos.y.toFixed(3))\r\n params.set('camZ', pos.z.toFixed(3))\r\n params.set('tarX', tar.x.toFixed(3))\r\n params.set('tarY', tar.y.toFixed(3))\r\n params.set('tarZ', tar.z.toFixed(3))\r\n if (building) params.set('buildingId', String(building.id))\r\n return `${origin}${pathname}?${params.toString()}`\r\n } catch {\r\n return fallback\r\n }\r\n }\r\n\r\n if (currentViewer === ViewerNames.pointcloud && viewer) {\r\n const cam = getCameraPosition(viewer)\r\n if (cam) {\r\n const params = new URLSearchParams(new URL(window.location.href).search)\r\n params.set('viewer', 'pointcloud')\r\n params.set('camX', cam.position.x.toFixed(3))\r\n params.set('camY', cam.position.y.toFixed(3))\r\n params.set('camZ', cam.position.z.toFixed(3))\r\n params.set('tarX', cam.target.x.toFixed(3))\r\n params.set('tarY', cam.target.y.toFixed(3))\r\n params.set('tarZ', cam.target.z.toFixed(3))\r\n if (building) params.set('buildingId', String(building.id))\r\n return `${origin}${pathname}?${params.toString()}`\r\n }\r\n }\r\n\r\n return fallback\r\n }, [currentViewer, map, world, viewer, building])\r\n}\r\n"],"mappings":"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","names":[]}
@@ -963,29 +963,6 @@ ul {
963
963
  color: white;
964
964
  }
965
965
 
966
- /* smooths any reverse transitions */
967
- .maplibregl-ctrl-bottom-left,
968
- .maplibregl-ctrl-bottom-right {
969
- transform: translateY(-75px);
970
- transition: transform 400ms ease-in-out;
971
- }
972
-
973
- .map-ctrls-init {
974
- transform: translateY(0px);
975
- transition: transform 400ms ease-in-out;
976
- }
977
-
978
- /* sliding to match toolbar behavior */
979
- .mobile-map-ctrls {
980
- transform: translateY(-75px);
981
- transition: transform 300ms ease-in-out;
982
- }
983
-
984
- .mobile-map-ctrls-expanded {
985
- transform: translateY(-255px);
986
- transition: transform 250ms ease-in-out;
987
- }
988
-
989
966
  .center-align {
990
967
  display: flex;
991
968
  flex-direction: column;
@@ -998,7 +975,6 @@ ul {
998
975
  white-space: nowrap !important;
999
976
  overflow: hidden !important;
1000
977
  text-overflow: ellipsis !important;
1001
- z-index: -1 !important;
1002
978
  }
1003
979
 
1004
980
  .maplibregl-ctrl-bottom-right details:hover {
@@ -1010,15 +986,6 @@ ul {
1010
986
  background-color: white !important;
1011
987
  }
1012
988
 
1013
- @media only screen and (min-width: 475px) {
1014
- /* smooths any reverse transitions */
1015
- .maplibregl-ctrl-bottom-left,
1016
- .maplibregl-ctrl-bottom-right {
1017
- transform: translateY(0px);
1018
- transition: transform 400ms ease-in-out;
1019
- }
1020
- }
1021
-
1022
989
  @media only screen and (max-width: 600px) {
1023
990
  .maplibregl-ctrl-zoom-in {
1024
991
  opacity: 0;
@@ -1052,6 +1019,52 @@ ul {
1052
1019
  }
1053
1020
  }
1054
1021
 
1022
+ @media (max-width: 768px) {
1023
+ .maplibregl-ctrl-bottom-right {
1024
+ margin-bottom: 48px !important;
1025
+ font-size: 10px !important;
1026
+ /* The floating map toolbar (bottom, centered) sits at z-10 — without
1027
+ this, it renders on top of (hides) the attribution popup instead of
1028
+ the popup coming forward/in front of it when tapped open. This
1029
+ container is already `position: absolute` via MapLibre's own base
1030
+ CSS, so z-index applies directly here. */
1031
+ z-index: 30 !important;
1032
+ }
1033
+
1034
+ /* MapLibre auto-adds `open` + `.maplibregl-compact-show` together once on
1035
+ mount for narrow viewports (attribution_control.ts _updateCompact()).
1036
+ Trying to distinguish that from a genuine tap via those same
1037
+ classes/attributes is unreliable — tapping <summary> fires BOTH the
1038
+ browser's native <details> toggle AND MapLibre's own JS toggle at once,
1039
+ which can reproduce the same "both present" combination on a real tap
1040
+ too. Side-step MapLibre's class bookkeeping entirely: force hidden
1041
+ always, reveal only on real, live interaction (:hover for pointer
1042
+ devices, :focus-within once tapped/focused, :active as a fallback for
1043
+ iOS Safari's unreliable tap-to-focus on button-like elements). */
1044
+ .maplibregl-ctrl-bottom-right .maplibregl-ctrl-attrib-inner {
1045
+ display: none !important;
1046
+ }
1047
+
1048
+ .maplibregl-ctrl-bottom-right details:hover .maplibregl-ctrl-attrib-inner,
1049
+ .maplibregl-ctrl-bottom-right details:focus-within .maplibregl-ctrl-attrib-inner,
1050
+ .maplibregl-ctrl-bottom-right details:active .maplibregl-ctrl-attrib-inner {
1051
+ display: block !important;
1052
+ max-width: calc(100vw - 20px) !important;
1053
+ width: auto !important;
1054
+ white-space: normal !important;
1055
+ word-break: break-word !important;
1056
+ overflow-wrap: break-word !important;
1057
+ font-size: 10px !important;
1058
+ box-sizing: border-box !important;
1059
+ }
1060
+
1061
+ .maplibregl-ctrl-bottom-right details:hover,
1062
+ .maplibregl-ctrl-bottom-right details:focus-within,
1063
+ .maplibregl-ctrl-bottom-right details:active {
1064
+ overflow: visible !important;
1065
+ }
1066
+ }
1067
+
1055
1068
  /* Home page styles */
1056
1069
  @layer utilities {
1057
1070
  .gradient-button {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@collabdt/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CDT Core contains all the core technology for your CDT platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",