super-three 0.181.0 → 0.184.0

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 (539) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -4
  3. package/build/three.cjs +13152 -11354
  4. package/build/three.core.js +12319 -11539
  5. package/build/three.core.min.js +2 -2
  6. package/build/three.module.js +1808 -781
  7. package/build/three.module.min.js +2 -2
  8. package/build/three.tsl.js +27 -13
  9. package/build/three.tsl.min.js +2 -2
  10. package/build/three.webgpu.js +10523 -6263
  11. package/build/three.webgpu.min.js +2 -2
  12. package/build/three.webgpu.nodes.js +10484 -6262
  13. package/build/three.webgpu.nodes.min.js +2 -2
  14. package/examples/jsm/Addons.js +11 -3
  15. package/examples/jsm/animation/CCDIKSolver.js +7 -3
  16. package/examples/jsm/controls/ArcballControls.js +7 -4
  17. package/examples/jsm/controls/DragControls.js +2 -2
  18. package/examples/jsm/controls/FirstPersonControls.js +58 -54
  19. package/examples/jsm/controls/FlyControls.js +4 -0
  20. package/examples/jsm/controls/MapControls.js +55 -1
  21. package/examples/jsm/controls/OrbitControls.js +111 -8
  22. package/examples/jsm/controls/TrackballControls.js +8 -8
  23. package/examples/jsm/controls/TransformControls.js +34 -2
  24. package/examples/jsm/csm/CSM.js +2 -1
  25. package/examples/jsm/csm/CSMShadowNode.js +6 -2
  26. package/examples/jsm/effects/AnaglyphEffect.js +102 -7
  27. package/examples/jsm/environments/ColorEnvironment.js +59 -0
  28. package/examples/jsm/environments/RoomEnvironment.js +3 -0
  29. package/examples/jsm/exporters/EXRExporter.js +1 -1
  30. package/examples/jsm/exporters/GLTFExporter.js +152 -9
  31. package/examples/jsm/exporters/USDZExporter.js +22 -3
  32. package/examples/jsm/geometries/DecalGeometry.js +1 -1
  33. package/examples/jsm/geometries/TextGeometry.js +18 -0
  34. package/examples/jsm/helpers/AnimationPathHelper.js +302 -0
  35. package/examples/jsm/helpers/LightProbeGridHelper.js +221 -0
  36. package/examples/jsm/helpers/LightProbeHelperGPU.js +1 -1
  37. package/examples/jsm/helpers/TextureHelperGPU.js +1 -1
  38. package/examples/jsm/helpers/ViewHelper.js +67 -8
  39. package/examples/jsm/inspector/Extension.js +13 -0
  40. package/examples/jsm/inspector/Inspector.js +203 -88
  41. package/examples/jsm/inspector/RendererInspector.js +14 -4
  42. package/examples/jsm/inspector/extensions/extensions.json +6 -0
  43. package/examples/jsm/inspector/extensions/tsl-graph/TSLGraphEditor.js +916 -0
  44. package/examples/jsm/inspector/extensions/tsl-graph/TSLGraphLoader.js +281 -0
  45. package/examples/jsm/inspector/tabs/Console.js +41 -7
  46. package/examples/jsm/inspector/tabs/Memory.js +128 -0
  47. package/examples/jsm/inspector/tabs/Parameters.js +51 -3
  48. package/examples/jsm/inspector/tabs/Performance.js +4 -4
  49. package/examples/jsm/inspector/tabs/Settings.js +264 -0
  50. package/examples/jsm/inspector/tabs/Timeline.js +1611 -0
  51. package/examples/jsm/inspector/tabs/Viewer.js +107 -5
  52. package/examples/jsm/inspector/ui/Graph.js +2 -2
  53. package/examples/jsm/inspector/ui/List.js +1 -1
  54. package/examples/jsm/inspector/ui/Profiler.js +1936 -34
  55. package/examples/jsm/inspector/ui/Style.js +1044 -31
  56. package/examples/jsm/inspector/ui/Tab.js +222 -3
  57. package/examples/jsm/inspector/ui/Values.js +55 -2
  58. package/examples/jsm/inspector/ui/utils.js +13 -0
  59. package/examples/jsm/interaction/InteractionManager.js +226 -0
  60. package/examples/jsm/libs/meshopt_decoder.module.js +9 -8
  61. package/examples/jsm/lighting/DynamicLighting.js +82 -0
  62. package/examples/jsm/lighting/LightProbeGrid.js +651 -0
  63. package/examples/jsm/lines/LineMaterial.js +7 -1
  64. package/examples/jsm/loaders/3DMLoader.js +5 -4
  65. package/examples/jsm/loaders/3MFLoader.js +2 -2
  66. package/examples/jsm/loaders/AMFLoader.js +2 -2
  67. package/examples/jsm/loaders/ColladaLoader.js +24 -4026
  68. package/examples/jsm/loaders/DRACOLoader.js +5 -5
  69. package/examples/jsm/loaders/EXRLoader.js +687 -48
  70. package/examples/jsm/loaders/FBXLoader.js +235 -37
  71. package/examples/jsm/loaders/GCodeLoader.js +34 -8
  72. package/examples/jsm/loaders/GLTFLoader.js +146 -178
  73. package/examples/jsm/loaders/HDRLoader.js +1 -2
  74. package/examples/jsm/loaders/KMZLoader.js +5 -5
  75. package/examples/jsm/loaders/KTX2Loader.js +27 -5
  76. package/examples/jsm/loaders/LDrawLoader.js +41 -50
  77. package/examples/jsm/loaders/LWOLoader.js +7 -39
  78. package/examples/jsm/loaders/NRRDLoader.js +2 -2
  79. package/examples/jsm/loaders/PCDLoader.js +4 -2
  80. package/examples/jsm/loaders/SVGLoader.js +2 -2
  81. package/examples/jsm/loaders/TDSLoader.js +0 -2
  82. package/examples/jsm/loaders/TGALoader.js +0 -2
  83. package/examples/jsm/loaders/USDLoader.js +100 -40
  84. package/examples/jsm/loaders/UltraHDRLoader.js +285 -160
  85. package/examples/jsm/loaders/VOXLoader.js +660 -117
  86. package/examples/jsm/loaders/VRMLLoader.js +79 -2
  87. package/examples/jsm/loaders/VTKLoader.js +42 -25
  88. package/examples/jsm/loaders/collada/ColladaComposer.js +3044 -0
  89. package/examples/jsm/loaders/collada/ColladaParser.js +1977 -0
  90. package/examples/jsm/loaders/usd/USDAParser.js +450 -363
  91. package/examples/jsm/loaders/usd/USDCParser.js +1867 -6
  92. package/examples/jsm/loaders/usd/USDComposer.js +4594 -0
  93. package/examples/jsm/materials/LDrawConditionalLineNodeMaterial.js +2 -2
  94. package/examples/jsm/materials/WoodNodeMaterial.js +11 -11
  95. package/examples/jsm/math/Octree.js +131 -1
  96. package/examples/jsm/misc/GPUComputationRenderer.js +2 -0
  97. package/examples/jsm/misc/RollerCoaster.js +42 -4
  98. package/examples/jsm/misc/Volume.js +0 -1
  99. package/examples/jsm/misc/VolumeSlice.js +0 -1
  100. package/examples/jsm/modifiers/TessellateModifier.js +1 -1
  101. package/examples/jsm/objects/LensflareMesh.js +1 -1
  102. package/examples/jsm/objects/Reflector.js +73 -25
  103. package/examples/jsm/objects/Sky.js +90 -6
  104. package/examples/jsm/objects/SkyMesh.js +150 -16
  105. package/examples/jsm/objects/Water.js +4 -3
  106. package/examples/jsm/objects/Water2.js +5 -3
  107. package/examples/jsm/objects/WaterMesh.js +5 -7
  108. package/examples/jsm/physics/AmmoPhysics.js +12 -7
  109. package/examples/jsm/physics/JoltPhysics.js +10 -6
  110. package/examples/jsm/physics/RapierPhysics.js +9 -5
  111. package/examples/jsm/postprocessing/EffectComposer.js +7 -5
  112. package/examples/jsm/postprocessing/OutputPass.js +9 -0
  113. package/examples/jsm/postprocessing/RenderPass.js +10 -0
  114. package/examples/jsm/postprocessing/RenderTransitionPass.js +1 -1
  115. package/examples/jsm/postprocessing/UnrealBloomPass.js +48 -18
  116. package/examples/jsm/renderers/CSS3DRenderer.js +1 -1
  117. package/examples/jsm/renderers/Projector.js +246 -28
  118. package/examples/jsm/renderers/SVGRenderer.js +174 -60
  119. package/examples/jsm/shaders/GTAOShader.js +19 -6
  120. package/examples/jsm/shaders/HalftoneShader.js +12 -1
  121. package/examples/jsm/shaders/PoissonDenoiseShader.js +6 -2
  122. package/examples/jsm/shaders/SAOShader.js +17 -4
  123. package/examples/jsm/shaders/SSAOShader.js +11 -1
  124. package/examples/jsm/shaders/SSRShader.js +6 -5
  125. package/examples/jsm/shaders/UnpackDepthRGBAShader.js +2 -4
  126. package/examples/jsm/shaders/VignetteShader.js +1 -1
  127. package/examples/jsm/transpiler/AST.js +44 -0
  128. package/examples/jsm/transpiler/GLSLDecoder.js +63 -6
  129. package/examples/jsm/transpiler/ShaderToyDecoder.js +2 -0
  130. package/examples/jsm/transpiler/TSLEncoder.js +46 -3
  131. package/examples/jsm/transpiler/TranspilerUtils.js +3 -3
  132. package/examples/jsm/transpiler/WGSLEncoder.js +27 -0
  133. package/examples/jsm/tsl/WebGLNodesHandler.js +605 -0
  134. package/examples/jsm/tsl/display/AfterImageNode.js +11 -1
  135. package/examples/jsm/tsl/display/AnaglyphPassNode.js +458 -16
  136. package/examples/jsm/tsl/display/AnamorphicNode.js +12 -1
  137. package/examples/jsm/tsl/display/BilateralBlurNode.js +374 -0
  138. package/examples/jsm/tsl/display/BloomNode.js +16 -6
  139. package/examples/jsm/tsl/display/CRT.js +150 -0
  140. package/examples/jsm/tsl/display/ChromaticAberrationNode.js +3 -36
  141. package/examples/jsm/tsl/display/DenoiseNode.js +1 -1
  142. package/examples/jsm/tsl/display/DepthOfFieldNode.js +1 -1
  143. package/examples/jsm/tsl/display/DotScreenNode.js +1 -1
  144. package/examples/jsm/tsl/display/FSR1Node.js +477 -0
  145. package/examples/jsm/tsl/display/FXAANode.js +2 -2
  146. package/examples/jsm/tsl/display/GTAONode.js +6 -4
  147. package/examples/jsm/tsl/display/GaussianBlurNode.js +21 -2
  148. package/examples/jsm/tsl/display/GodraysNode.js +615 -0
  149. package/examples/jsm/tsl/display/LensflareNode.js +1 -1
  150. package/examples/jsm/tsl/display/Lut3DNode.js +1 -1
  151. package/examples/jsm/tsl/display/OutlineNode.js +69 -19
  152. package/examples/jsm/tsl/display/ParallaxBarrierPassNode.js +2 -2
  153. package/examples/jsm/tsl/display/PixelationPassNode.js +7 -6
  154. package/examples/jsm/tsl/display/RGBShiftNode.js +2 -2
  155. package/examples/jsm/tsl/display/RetroPassNode.js +263 -0
  156. package/examples/jsm/tsl/display/SMAANode.js +2 -2
  157. package/examples/jsm/tsl/display/SSAAPassNode.js +2 -2
  158. package/examples/jsm/tsl/display/SSGINode.js +8 -24
  159. package/examples/jsm/tsl/display/SSRNode.js +8 -8
  160. package/examples/jsm/tsl/display/SSSNode.js +6 -4
  161. package/examples/jsm/tsl/display/Shape.js +29 -0
  162. package/examples/jsm/tsl/display/SharpenNode.js +283 -0
  163. package/examples/jsm/tsl/display/SobelOperatorNode.js +2 -2
  164. package/examples/jsm/tsl/display/StereoCompositePassNode.js +8 -1
  165. package/examples/jsm/tsl/display/StereoPassNode.js +1 -2
  166. package/examples/jsm/tsl/display/TAAUNode.js +835 -0
  167. package/examples/jsm/tsl/display/TRAANode.js +315 -126
  168. package/examples/jsm/tsl/display/TransitionNode.js +1 -1
  169. package/examples/jsm/tsl/display/depthAwareBlend.js +80 -0
  170. package/examples/jsm/tsl/display/radialBlur.js +68 -0
  171. package/examples/jsm/tsl/lighting/DynamicLightsNode.js +300 -0
  172. package/examples/jsm/tsl/lighting/data/AmbientLightDataNode.js +61 -0
  173. package/examples/jsm/tsl/lighting/data/DirectionalLightDataNode.js +111 -0
  174. package/examples/jsm/tsl/lighting/data/HemisphereLightDataNode.js +99 -0
  175. package/examples/jsm/tsl/lighting/data/PointLightDataNode.js +134 -0
  176. package/examples/jsm/tsl/lighting/data/SpotLightDataNode.js +161 -0
  177. package/examples/jsm/tsl/math/Bayer.js +53 -3
  178. package/examples/jsm/utils/BufferGeometryUtils.js +2 -3
  179. package/examples/jsm/utils/ColorUtils.js +76 -0
  180. package/examples/jsm/utils/LDrawUtils.js +1 -1
  181. package/examples/jsm/utils/ShadowMapViewer.js +24 -10
  182. package/examples/jsm/utils/ShadowMapViewerGPU.js +1 -1
  183. package/examples/jsm/utils/SkeletonUtils.js +14 -8
  184. package/examples/jsm/utils/WebGPUTextureUtils.js +1 -1
  185. package/examples/jsm/webxr/XRHandMeshModel.js +36 -10
  186. package/examples/jsm/webxr/XRHandModelFactory.js +2 -1
  187. package/package.json +24 -30
  188. package/src/Three.Core.js +3 -1
  189. package/src/Three.TSL.js +25 -11
  190. package/src/Three.WebGPU.Nodes.js +5 -0
  191. package/src/Three.WebGPU.js +9 -0
  192. package/src/Three.js +1 -0
  193. package/src/animation/AnimationAction.js +12 -2
  194. package/src/animation/AnimationClip.js +1 -1
  195. package/src/animation/AnimationMixer.js +6 -0
  196. package/src/animation/AnimationUtils.js +1 -12
  197. package/src/animation/KeyframeTrack.js +47 -8
  198. package/src/animation/PropertyMixer.js +4 -4
  199. package/src/animation/tracks/BooleanKeyframeTrack.js +1 -1
  200. package/src/animation/tracks/ColorKeyframeTrack.js +1 -1
  201. package/src/animation/tracks/NumberKeyframeTrack.js +1 -1
  202. package/src/animation/tracks/QuaternionKeyframeTrack.js +1 -1
  203. package/src/animation/tracks/StringKeyframeTrack.js +1 -1
  204. package/src/animation/tracks/VectorKeyframeTrack.js +1 -1
  205. package/src/audio/Audio.js +1 -1
  206. package/src/audio/AudioContext.js +2 -2
  207. package/src/audio/AudioListener.js +5 -3
  208. package/src/cameras/Camera.js +32 -2
  209. package/src/cameras/CubeCamera.js +20 -0
  210. package/src/constants.js +90 -5
  211. package/src/core/BufferAttribute.js +13 -1
  212. package/src/core/BufferGeometry.js +14 -2
  213. package/src/core/Clock.js +7 -0
  214. package/src/core/Object3D.js +52 -4
  215. package/src/core/Raycaster.js +2 -2
  216. package/src/core/RenderTarget.js +4 -4
  217. package/src/extras/PMREMGenerator.js +7 -18
  218. package/src/extras/TextureUtils.js +5 -1
  219. package/src/extras/curves/CatmullRomCurve3.js +3 -2
  220. package/src/geometries/ExtrudeGeometry.js +2 -2
  221. package/src/geometries/PolyhedronGeometry.js +1 -1
  222. package/src/geometries/TorusGeometry.js +8 -3
  223. package/src/helpers/CameraHelper.js +3 -0
  224. package/src/helpers/DirectionalLightHelper.js +4 -1
  225. package/src/helpers/HemisphereLightHelper.js +3 -0
  226. package/src/helpers/PointLightHelper.js +1 -25
  227. package/src/helpers/SpotLightHelper.js +3 -0
  228. package/src/lights/DirectionalLight.js +13 -0
  229. package/src/lights/HemisphereLight.js +10 -0
  230. package/src/lights/Light.js +1 -11
  231. package/src/lights/LightProbe.js +0 -15
  232. package/src/lights/LightShadow.js +15 -6
  233. package/src/lights/PointLight.js +15 -0
  234. package/src/lights/PointLightShadow.js +0 -86
  235. package/src/lights/SpotLight.js +22 -1
  236. package/src/lights/webgpu/IESSpotLight.js +2 -1
  237. package/src/loaders/AudioLoader.js +11 -1
  238. package/src/loaders/Cache.js +28 -0
  239. package/src/loaders/DataTextureLoader.js +6 -4
  240. package/src/loaders/FileLoader.js +2 -3
  241. package/src/loaders/ImageBitmapLoader.js +12 -9
  242. package/src/loaders/Loader.js +6 -0
  243. package/src/loaders/MaterialLoader.js +3 -2
  244. package/src/loaders/ObjectLoader.js +46 -6
  245. package/src/loaders/nodes/NodeLoader.js +2 -2
  246. package/src/loaders/nodes/NodeObjectLoader.js +18 -0
  247. package/src/materials/Material.js +2 -0
  248. package/src/materials/MeshLambertMaterial.js +9 -0
  249. package/src/materials/MeshPhongMaterial.js +9 -0
  250. package/src/materials/MeshToonMaterial.js +1 -1
  251. package/src/materials/ShaderMaterial.js +20 -1
  252. package/src/materials/nodes/Line2NodeMaterial.js +34 -7
  253. package/src/materials/nodes/MeshPhysicalNodeMaterial.js +5 -2
  254. package/src/materials/nodes/MeshStandardNodeMaterial.js +5 -4
  255. package/src/materials/nodes/NodeMaterial.js +45 -25
  256. package/src/materials/nodes/manager/NodeMaterialObserver.js +191 -86
  257. package/src/math/Line3.js +6 -5
  258. package/src/math/MathUtils.js +10 -10
  259. package/src/math/Matrix2.js +13 -9
  260. package/src/math/Matrix3.js +13 -9
  261. package/src/math/Matrix4.js +87 -74
  262. package/src/math/Plane.js +4 -3
  263. package/src/math/Quaternion.js +3 -29
  264. package/src/math/Sphere.js +1 -1
  265. package/src/math/Triangle.js +1 -1
  266. package/src/math/Vector2.js +11 -7
  267. package/src/math/Vector3.js +15 -13
  268. package/src/math/Vector4.js +13 -9
  269. package/src/math/interpolants/BezierInterpolant.js +108 -0
  270. package/src/nodes/Nodes.js +86 -68
  271. package/src/nodes/TSL.js +7 -7
  272. package/src/nodes/accessors/Arrays.js +1 -1
  273. package/src/nodes/accessors/BatchNode.js +10 -10
  274. package/src/nodes/accessors/Bitangent.js +5 -5
  275. package/src/nodes/accessors/BufferAttributeNode.js +107 -15
  276. package/src/nodes/accessors/BufferNode.js +29 -2
  277. package/src/nodes/accessors/Camera.js +149 -28
  278. package/src/nodes/accessors/ClippingNode.js +4 -4
  279. package/src/nodes/accessors/CubeTextureNode.js +27 -2
  280. package/src/nodes/accessors/InstanceNode.js +148 -43
  281. package/src/nodes/accessors/MaterialNode.js +9 -1
  282. package/src/nodes/accessors/MaterialProperties.js +2 -5
  283. package/src/nodes/accessors/MaterialReferenceNode.js +1 -2
  284. package/src/nodes/accessors/ModelNode.js +1 -1
  285. package/src/nodes/accessors/Normal.js +11 -11
  286. package/src/nodes/accessors/Object3DNode.js +1 -1
  287. package/src/nodes/accessors/Position.js +34 -2
  288. package/src/nodes/accessors/ReferenceBaseNode.js +6 -6
  289. package/src/nodes/accessors/ReferenceNode.js +8 -8
  290. package/src/nodes/accessors/RendererReferenceNode.js +1 -2
  291. package/src/nodes/accessors/SceneProperties.js +47 -0
  292. package/src/nodes/accessors/SkinningNode.js +27 -26
  293. package/src/nodes/accessors/StorageBufferNode.js +14 -25
  294. package/src/nodes/accessors/StorageTextureNode.js +40 -9
  295. package/src/nodes/accessors/Tangent.js +4 -14
  296. package/src/nodes/accessors/Texture3DNode.js +32 -35
  297. package/src/nodes/accessors/TextureNode.js +67 -23
  298. package/src/nodes/accessors/UniformArrayNode.js +6 -4
  299. package/src/nodes/accessors/UserDataNode.js +1 -2
  300. package/src/nodes/accessors/VertexColorNode.js +1 -2
  301. package/src/nodes/code/FunctionCallNode.js +1 -1
  302. package/src/nodes/code/FunctionNode.js +2 -3
  303. package/src/nodes/core/ArrayNode.js +21 -2
  304. package/src/nodes/core/AssignNode.js +3 -3
  305. package/src/nodes/core/AttributeNode.js +3 -3
  306. package/src/nodes/core/BypassNode.js +1 -1
  307. package/src/nodes/core/ContextNode.js +104 -5
  308. package/src/nodes/core/IndexNode.js +2 -1
  309. package/src/nodes/core/InputNode.js +1 -1
  310. package/src/nodes/core/InspectorNode.js +1 -1
  311. package/src/nodes/core/IsolateNode.js +1 -1
  312. package/src/nodes/core/MRTNode.js +48 -2
  313. package/src/nodes/core/Node.js +112 -15
  314. package/src/nodes/core/NodeBuilder.js +284 -66
  315. package/src/nodes/core/NodeError.js +28 -0
  316. package/src/nodes/core/NodeFrame.js +12 -4
  317. package/src/nodes/core/NodeUtils.js +11 -9
  318. package/src/nodes/core/OutputStructNode.js +12 -10
  319. package/src/nodes/core/ParameterNode.js +4 -4
  320. package/src/nodes/core/PropertyNode.js +19 -3
  321. package/src/nodes/core/StackNode.js +66 -17
  322. package/src/nodes/core/StackTrace.js +139 -0
  323. package/src/nodes/core/StructNode.js +17 -3
  324. package/src/nodes/core/StructTypeNode.js +12 -18
  325. package/src/nodes/core/SubBuildNode.js +2 -2
  326. package/src/nodes/core/UniformGroupNode.js +36 -6
  327. package/src/nodes/core/UniformNode.js +21 -5
  328. package/src/nodes/core/VarNode.js +48 -23
  329. package/src/nodes/core/VaryingNode.js +2 -19
  330. package/src/nodes/display/BlendModes.js +0 -64
  331. package/src/nodes/display/ColorAdjustment.js +17 -0
  332. package/src/nodes/display/ColorSpaceNode.js +3 -3
  333. package/src/nodes/display/NormalMapNode.js +39 -4
  334. package/src/nodes/display/PassNode.js +125 -16
  335. package/src/nodes/display/RenderOutputNode.js +7 -7
  336. package/src/nodes/display/ScreenNode.js +4 -2
  337. package/src/nodes/display/ToneMappingNode.js +1 -1
  338. package/src/nodes/display/ToonOutlinePassNode.js +2 -2
  339. package/src/nodes/display/ViewportDepthNode.js +52 -4
  340. package/src/nodes/display/ViewportDepthTextureNode.js +11 -15
  341. package/src/nodes/display/ViewportTextureNode.js +39 -11
  342. package/src/nodes/fog/Fog.js +18 -35
  343. package/src/nodes/functions/BSDF/BRDF_GGX_Multiscatter.js +3 -3
  344. package/src/nodes/functions/BSDF/DFGLUT.js +56 -0
  345. package/src/nodes/functions/BSDF/EnvironmentBRDF.js +2 -2
  346. package/src/nodes/functions/BSDF/V_GGX_SmithCorrelated_Anisotropic.js +2 -2
  347. package/src/nodes/functions/PhysicalLightingModel.js +126 -45
  348. package/src/nodes/geometry/RangeNode.js +5 -3
  349. package/src/nodes/gpgpu/AtomicFunctionNode.js +1 -1
  350. package/src/nodes/gpgpu/BarrierNode.js +9 -0
  351. package/src/nodes/gpgpu/ComputeBuiltinNode.js +2 -3
  352. package/src/nodes/gpgpu/ComputeNode.js +74 -48
  353. package/src/nodes/gpgpu/SubgroupFunctionNode.js +2 -2
  354. package/src/nodes/gpgpu/WorkgroupInfoNode.js +4 -4
  355. package/src/nodes/lighting/AnalyticLightNode.js +53 -0
  356. package/src/nodes/lighting/EnvironmentNode.js +28 -3
  357. package/src/nodes/lighting/LightsNode.js +7 -28
  358. package/src/nodes/lighting/PointShadowNode.js +162 -149
  359. package/src/nodes/lighting/ShadowFilterNode.js +53 -65
  360. package/src/nodes/lighting/ShadowNode.js +120 -42
  361. package/src/nodes/math/BitcastNode.js +1 -1
  362. package/src/nodes/math/BitcountNode.js +433 -0
  363. package/src/nodes/math/ConditionalNode.js +3 -3
  364. package/src/nodes/math/MathNode.js +76 -41
  365. package/src/nodes/math/OperatorNode.js +3 -2
  366. package/src/nodes/math/PackFloatNode.js +98 -0
  367. package/src/nodes/math/UnpackFloatNode.js +96 -0
  368. package/src/nodes/pmrem/PMREMNode.js +1 -1
  369. package/src/nodes/pmrem/PMREMUtils.js +9 -15
  370. package/src/nodes/tsl/TSLBase.js +1 -1
  371. package/src/nodes/tsl/TSLCore.js +38 -17
  372. package/src/nodes/utils/ArrayElementNode.js +14 -1
  373. package/src/nodes/utils/ConvertNode.js +1 -1
  374. package/src/nodes/utils/DebugNode.js +12 -12
  375. package/src/nodes/utils/EventNode.js +31 -2
  376. package/src/nodes/utils/FlipNode.js +1 -1
  377. package/src/nodes/utils/FunctionOverloadingNode.js +1 -1
  378. package/src/nodes/utils/JoinNode.js +3 -3
  379. package/src/nodes/utils/LoopNode.js +1 -1
  380. package/src/nodes/utils/MemberNode.js +2 -2
  381. package/src/nodes/utils/Packing.js +13 -1
  382. package/src/nodes/utils/PostProcessingUtils.js +33 -1
  383. package/src/nodes/utils/RTTNode.js +1 -1
  384. package/src/nodes/utils/ReflectorNode.js +3 -4
  385. package/src/nodes/utils/Remap.js +48 -0
  386. package/src/nodes/utils/RotateNode.js +1 -1
  387. package/src/nodes/utils/SampleNode.js +1 -1
  388. package/src/nodes/utils/SetNode.js +1 -1
  389. package/src/nodes/utils/SplitNode.js +1 -1
  390. package/src/nodes/utils/SpriteSheetUV.js +35 -0
  391. package/src/nodes/utils/UVUtils.js +28 -0
  392. package/src/objects/BatchedMesh.js +44 -16
  393. package/src/objects/InstancedMesh.js +30 -3
  394. package/src/objects/Line.js +1 -1
  395. package/src/objects/Mesh.js +1 -1
  396. package/src/objects/Points.js +1 -1
  397. package/src/objects/Skeleton.js +9 -0
  398. package/src/objects/SkinnedMesh.js +26 -9
  399. package/src/renderers/WebGLRenderer.js +329 -145
  400. package/src/renderers/common/Animation.js +3 -3
  401. package/src/renderers/common/Attributes.js +15 -1
  402. package/src/renderers/common/Backend.js +29 -8
  403. package/src/renderers/common/Background.js +26 -13
  404. package/src/renderers/common/BindGroup.js +1 -16
  405. package/src/renderers/common/Binding.js +11 -0
  406. package/src/renderers/common/Bindings.js +29 -14
  407. package/src/renderers/common/BlendMode.js +143 -0
  408. package/src/renderers/common/Buffer.js +40 -0
  409. package/src/renderers/common/BundleGroup.js +1 -1
  410. package/src/renderers/common/ChainMap.js +30 -6
  411. package/src/renderers/common/ComputePipeline.js +1 -1
  412. package/src/renderers/common/CubeRenderTarget.js +51 -7
  413. package/src/renderers/common/Geometries.js +29 -3
  414. package/src/renderers/common/Info.js +333 -4
  415. package/src/renderers/common/InspectorBase.js +6 -1
  416. package/src/renderers/common/Lighting.js +5 -21
  417. package/src/renderers/common/Pipelines.js +40 -7
  418. package/src/renderers/common/PostProcessing.js +8 -206
  419. package/src/renderers/common/ReadbackBuffer.js +78 -0
  420. package/src/renderers/common/RenderBundle.js +3 -1
  421. package/src/renderers/common/RenderBundles.js +7 -3
  422. package/src/renderers/common/RenderContext.js +16 -0
  423. package/src/renderers/common/RenderContexts.js +33 -49
  424. package/src/renderers/common/RenderLists.js +2 -1
  425. package/src/renderers/common/RenderObject.js +17 -5
  426. package/src/renderers/common/RenderObjectPipeline.js +40 -0
  427. package/src/renderers/common/RenderObjects.js +21 -5
  428. package/src/renderers/common/RenderPipeline.js +232 -17
  429. package/src/renderers/common/Renderer.js +481 -117
  430. package/src/renderers/common/Sampler.js +4 -4
  431. package/src/renderers/common/StorageBuffer.js +13 -1
  432. package/src/renderers/common/Textures.js +89 -4
  433. package/src/renderers/common/TimestampQueryPool.js +5 -3
  434. package/src/renderers/common/Uniform.js +8 -0
  435. package/src/renderers/common/UniformsGroup.js +62 -1
  436. package/src/renderers/common/XRManager.js +37 -29
  437. package/src/renderers/common/extras/PMREMGenerator.js +25 -23
  438. package/src/renderers/common/nodes/NodeBuilderState.js +1 -1
  439. package/src/renderers/common/nodes/{Nodes.js → NodeManager.js} +242 -99
  440. package/src/renderers/common/nodes/NodeStorageBuffer.js +13 -2
  441. package/src/renderers/common/nodes/NodeUniformBuffer.js +52 -0
  442. package/src/renderers/shaders/DFGLUTData.js +19 -34
  443. package/src/renderers/shaders/ShaderChunk/batching_pars_vertex.glsl.js +2 -2
  444. package/src/renderers/shaders/ShaderChunk/color_fragment.glsl.js +1 -5
  445. package/src/renderers/shaders/ShaderChunk/color_pars_fragment.glsl.js +1 -5
  446. package/src/renderers/shaders/ShaderChunk/color_pars_vertex.glsl.js +1 -5
  447. package/src/renderers/shaders/ShaderChunk/color_vertex.glsl.js +8 -10
  448. package/src/renderers/shaders/ShaderChunk/envmap_common_pars_fragment.glsl.js +0 -1
  449. package/src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js +8 -12
  450. package/src/renderers/shaders/ShaderChunk/lightprobes_pars_fragment.glsl.js +80 -0
  451. package/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js +13 -2
  452. package/src/renderers/shaders/ShaderChunk/lights_fragment_end.glsl.js +6 -0
  453. package/src/renderers/shaders/ShaderChunk/lights_fragment_maps.glsl.js +6 -2
  454. package/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js +2 -0
  455. package/src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js +8 -4
  456. package/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js +112 -53
  457. package/src/renderers/shaders/ShaderChunk/normal_fragment_maps.glsl.js +7 -0
  458. package/src/renderers/shaders/ShaderChunk/packing.glsl.js +20 -4
  459. package/src/renderers/shaders/ShaderChunk/premultiplied_alpha_fragment.glsl.js +0 -1
  460. package/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js +225 -186
  461. package/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js +12 -2
  462. package/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js +1 -1
  463. package/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js +1 -1
  464. package/src/renderers/shaders/ShaderChunk.js +5 -3
  465. package/src/renderers/shaders/ShaderLib/backgroundCube.glsl.js +1 -2
  466. package/src/renderers/shaders/ShaderLib/depth.glsl.js +3 -0
  467. package/src/renderers/shaders/ShaderLib/{distanceRGBA.glsl.js → distance.glsl.js} +1 -2
  468. package/src/renderers/shaders/ShaderLib/meshlambert.glsl.js +2 -1
  469. package/src/renderers/shaders/ShaderLib/meshnormal.glsl.js +1 -2
  470. package/src/renderers/shaders/ShaderLib/meshphong.glsl.js +2 -1
  471. package/src/renderers/shaders/ShaderLib/meshphysical.glsl.js +4 -9
  472. package/src/renderers/shaders/ShaderLib/meshtoon.glsl.js +0 -1
  473. package/src/renderers/shaders/ShaderLib/shadow.glsl.js +1 -1
  474. package/src/renderers/shaders/ShaderLib/vsm.glsl.js +4 -6
  475. package/src/renderers/shaders/ShaderLib.js +7 -6
  476. package/src/renderers/shaders/UniformsLib.js +7 -5
  477. package/src/renderers/shaders/UniformsUtils.js +27 -5
  478. package/src/renderers/webgl/WebGLAnimation.js +2 -1
  479. package/src/renderers/webgl/WebGLBackground.js +15 -15
  480. package/src/renderers/webgl/WebGLBindingStates.js +99 -27
  481. package/src/renderers/webgl/WebGLBufferRenderer.js +0 -32
  482. package/src/renderers/webgl/WebGLCapabilities.js +9 -4
  483. package/src/renderers/webgl/WebGLEnvironments.js +228 -0
  484. package/src/renderers/webgl/WebGLGeometries.js +10 -7
  485. package/src/renderers/webgl/WebGLIndexedBufferRenderer.js +0 -32
  486. package/src/renderers/webgl/WebGLLights.js +18 -1
  487. package/src/renderers/webgl/WebGLMaterials.js +24 -13
  488. package/src/renderers/webgl/WebGLObjects.js +3 -1
  489. package/src/renderers/webgl/WebGLOutput.js +270 -0
  490. package/src/renderers/webgl/WebGLProgram.js +49 -109
  491. package/src/renderers/webgl/WebGLPrograms.js +63 -51
  492. package/src/renderers/webgl/WebGLRenderLists.js +15 -0
  493. package/src/renderers/webgl/WebGLRenderStates.js +13 -2
  494. package/src/renderers/webgl/WebGLShadowMap.js +188 -24
  495. package/src/renderers/webgl/WebGLState.js +75 -37
  496. package/src/renderers/webgl/WebGLTextures.js +215 -54
  497. package/src/renderers/webgl/WebGLUniforms.js +40 -3
  498. package/src/renderers/webgl/WebGLUniformsGroups.js +19 -0
  499. package/src/renderers/webgl/WebGLUtils.js +6 -2
  500. package/src/renderers/webgl-fallback/WebGLBackend.js +253 -82
  501. package/src/renderers/webgl-fallback/WebGLBufferRenderer.js +0 -41
  502. package/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +173 -74
  503. package/src/renderers/webgl-fallback/utils/WebGLAttributeUtils.js +53 -19
  504. package/src/renderers/webgl-fallback/utils/WebGLCapabilities.js +25 -0
  505. package/src/renderers/webgl-fallback/utils/WebGLState.js +223 -6
  506. package/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js +68 -53
  507. package/src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js +10 -10
  508. package/src/renderers/webgl-fallback/utils/WebGLUtils.js +6 -2
  509. package/src/renderers/webgpu/WebGPUBackend.js +260 -140
  510. package/src/renderers/webgpu/WebGPURenderer.js +2 -1
  511. package/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +366 -90
  512. package/src/renderers/webgpu/utils/WebGPUAttributeUtils.js +107 -34
  513. package/src/renderers/webgpu/utils/WebGPUBindingUtils.js +357 -200
  514. package/src/renderers/webgpu/utils/WebGPUCapabilities.js +48 -0
  515. package/src/renderers/webgpu/utils/WebGPUConstants.js +10 -0
  516. package/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +61 -23
  517. package/src/renderers/webgpu/utils/WebGPUTexturePassUtils.js +152 -200
  518. package/src/renderers/webgpu/utils/WebGPUTextureUtils.js +156 -59
  519. package/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js +7 -7
  520. package/src/renderers/webgpu/utils/WebGPUUtils.js +33 -11
  521. package/src/renderers/webxr/WebXRController.js +12 -0
  522. package/src/renderers/webxr/WebXRManager.js +4 -2
  523. package/src/textures/CubeDepthTexture.js +76 -0
  524. package/src/textures/HTMLTexture.js +74 -0
  525. package/src/textures/Source.js +2 -2
  526. package/src/textures/Texture.js +16 -5
  527. package/src/utils.js +280 -3
  528. package/examples/jsm/materials/MeshGouraudMaterial.js +0 -434
  529. package/examples/jsm/materials/MeshPostProcessingMaterial.js +0 -167
  530. package/examples/jsm/shaders/GodRaysShader.js +0 -333
  531. package/src/nodes/accessors/SceneNode.js +0 -145
  532. package/src/nodes/code/ScriptableNode.js +0 -726
  533. package/src/nodes/code/ScriptableValueNode.js +0 -253
  534. package/src/nodes/display/PosterizeNode.js +0 -65
  535. package/src/nodes/functions/BSDF/DFGApprox.js +0 -71
  536. package/src/nodes/utils/RemapNode.js +0 -125
  537. package/src/nodes/utils/SpriteSheetUVNode.js +0 -90
  538. package/src/renderers/webgl/WebGLCubeMaps.js +0 -99
  539. package/src/renderers/webgl/WebGLCubeUVMaps.js +0 -134
@@ -1,19 +1,237 @@
1
+ import { EventDispatcher } from 'three';
1
2
  import { Style } from './Style.js';
3
+ import { getItem, setItem } from '../Inspector.js';
2
4
 
3
- export class Profiler {
5
+ export class Profiler extends EventDispatcher {
4
6
 
5
- constructor() {
7
+ constructor( inspector ) {
6
8
 
9
+ super();
10
+
11
+ this.inspector = inspector;
7
12
  this.tabs = {};
8
13
  this.activeTabId = null;
9
14
  this.isResizing = false;
10
- this.lastHeight = 350;
15
+ this.lastHeightBottom = 350; // Height for bottom position
16
+ this.lastWidthRight = 450; // Width for right position
17
+ this.position = 'bottom'; // 'bottom' or 'right'
18
+ this.detachedWindows = []; // Array to store detached tab windows
19
+ this.maxZIndex = 1002; // Track the highest z-index for detached windows (starts at base z-index from CSS)
20
+ this.nextTabOriginalIndex = 0; // Track the original order of tabs as they are added
11
21
 
12
22
  Style.init();
13
23
 
14
24
  this.setupShell();
15
25
  this.setupResizing();
16
26
 
27
+ // Setup window resize listener and update mobile status
28
+ this.setupWindowResizeListener();
29
+
30
+ // Setup orientation change listener for mobile devices
31
+ this.setupOrientationListener();
32
+
33
+ }
34
+
35
+ getSize() {
36
+
37
+ if ( this.panel.classList.contains( 'visible' ) === false || this.panel.classList.contains( 'no-tabs' ) ) {
38
+
39
+ return { width: 0, height: 0 };
40
+
41
+ }
42
+
43
+ if ( this.position === 'right' ) {
44
+
45
+ return { width: this.panel.offsetWidth, height: 0 };
46
+
47
+ } else {
48
+
49
+ return { width: 0, height: this.panel.offsetHeight };
50
+
51
+ }
52
+
53
+ }
54
+
55
+ get isMobile() {
56
+
57
+ return this.detectMobile();
58
+
59
+ }
60
+
61
+ get isSmallScreen() {
62
+
63
+ return window.innerWidth <= 768;
64
+
65
+ }
66
+
67
+ detectMobile() {
68
+
69
+ // Check for mobile devices
70
+ const userAgent = navigator.userAgent || navigator.vendor || window.opera;
71
+ const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( userAgent );
72
+ const isTouchDevice = ( 'ontouchstart' in window ) || ( navigator.maxTouchPoints > 0 );
73
+
74
+ return isMobileUA || ( isTouchDevice && this.isSmallScreen );
75
+
76
+ }
77
+
78
+ setupOrientationListener() {
79
+
80
+ const handleOrientationChange = () => {
81
+
82
+ if ( ! this.isMobile ) return;
83
+
84
+ // Check if device is in landscape or portrait mode
85
+ const isLandscape = window.innerWidth > window.innerHeight;
86
+
87
+ // In landscape mode, use right position (vertical panel)
88
+ // In portrait mode, use bottom position (horizontal panel)
89
+ const targetPosition = isLandscape ? 'right' : 'bottom';
90
+
91
+ if ( this.position !== targetPosition ) {
92
+
93
+ this.setPosition( targetPosition );
94
+
95
+ }
96
+
97
+ };
98
+
99
+ // Initial check
100
+ handleOrientationChange();
101
+
102
+ // Listen for orientation changes
103
+ window.addEventListener( 'orientationchange', handleOrientationChange );
104
+ window.addEventListener( 'resize', handleOrientationChange );
105
+
106
+ }
107
+
108
+ setupWindowResizeListener() {
109
+
110
+ const constrainDetachedWindows = () => {
111
+
112
+ this.detachedWindows.forEach( detachedWindow => {
113
+
114
+ this.constrainWindowToBounds( detachedWindow.panel );
115
+
116
+ } );
117
+
118
+ };
119
+
120
+ const constrainMainPanel = () => {
121
+
122
+ // Skip if panel is maximized (it should always fill the screen)
123
+ if ( this.panel.classList.contains( 'maximized' ) ) return;
124
+
125
+ const windowWidth = window.innerWidth;
126
+ const windowHeight = window.innerHeight;
127
+
128
+ if ( this.position === 'bottom' ) {
129
+
130
+ const currentHeight = this.panel.offsetHeight;
131
+ const maxHeight = windowHeight - 50; // Leave 50px margin
132
+
133
+ if ( currentHeight > maxHeight ) {
134
+
135
+ this.panel.style.height = `${ maxHeight }px`;
136
+ this.lastHeightBottom = maxHeight;
137
+
138
+ }
139
+
140
+ } else if ( this.position === 'right' ) {
141
+
142
+ const currentWidth = this.panel.offsetWidth;
143
+ const maxWidth = windowWidth - 50; // Leave 50px margin
144
+
145
+ if ( currentWidth > maxWidth ) {
146
+
147
+ this.panel.style.width = `${ maxWidth }px`;
148
+ this.lastWidthRight = maxWidth;
149
+
150
+ }
151
+
152
+ }
153
+
154
+ };
155
+
156
+ // Listen for window resize events
157
+ window.addEventListener( 'resize', () => {
158
+
159
+ if ( this.isSmallScreen ) {
160
+
161
+ this.floatingBtn.style.display = 'none';
162
+ this.panel.classList.add( 'hide-position-toggle' );
163
+
164
+ } else {
165
+
166
+ this.floatingBtn.style.display = '';
167
+ this.panel.classList.remove( 'hide-position-toggle' );
168
+
169
+ }
170
+
171
+ if ( this.isMobile ) {
172
+
173
+ this.panel.classList.add( 'is-mobile' );
174
+
175
+ } else {
176
+
177
+ this.panel.classList.remove( 'is-mobile' );
178
+
179
+ }
180
+
181
+ constrainDetachedWindows();
182
+ constrainMainPanel();
183
+
184
+ } );
185
+
186
+ }
187
+
188
+ constrainWindowToBounds( windowPanel ) {
189
+
190
+ const windowWidth = window.innerWidth;
191
+ const windowHeight = window.innerHeight;
192
+
193
+ const panelWidth = windowPanel.offsetWidth;
194
+ const panelHeight = windowPanel.offsetHeight;
195
+
196
+ let left = parseFloat( windowPanel.style.left ) || windowPanel.offsetLeft || 0;
197
+ let top = parseFloat( windowPanel.style.top ) || windowPanel.offsetTop || 0;
198
+
199
+ // Allow window to extend half its width/height outside the screen
200
+ const halfWidth = panelWidth / 2;
201
+ const halfHeight = panelHeight / 2;
202
+
203
+ // Constrain horizontal position (allow half width to extend beyond right edge)
204
+ if ( left + panelWidth > windowWidth + halfWidth ) {
205
+
206
+ left = windowWidth + halfWidth - panelWidth;
207
+
208
+ }
209
+
210
+ // Constrain horizontal position (allow half width to extend beyond left edge)
211
+ if ( left < - halfWidth ) {
212
+
213
+ left = - halfWidth;
214
+
215
+ }
216
+
217
+ // Constrain vertical position (allow half height to extend beyond bottom edge)
218
+ if ( top + panelHeight > windowHeight + halfHeight ) {
219
+
220
+ top = windowHeight + halfHeight - panelHeight;
221
+
222
+ }
223
+
224
+ // Constrain vertical position (allow half height to extend beyond top edge)
225
+ if ( top < - halfHeight ) {
226
+
227
+ top = - halfHeight;
228
+
229
+ }
230
+
231
+ // Apply constrained position
232
+ windowPanel.style.left = `${ left }px`;
233
+ windowPanel.style.top = `${ top }px`;
234
+
17
235
  }
18
236
 
19
237
  setupShell() {
@@ -24,28 +242,68 @@ export class Profiler {
24
242
  this.toggleButton = document.createElement( 'button' );
25
243
  this.toggleButton.id = 'profiler-toggle';
26
244
  this.toggleButton.innerHTML = `
245
+ <span id="builtin-tabs-container"></span>
27
246
  <span id="toggle-text">
28
247
  <span id="fps-counter">-</span>
29
248
  <span class="fps-label">FPS</span>
30
249
  </span>
31
- <!-- <span class="toggle-separator"></span> -->
32
250
  <span id="toggle-icon">
33
251
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-ipad-horizontal-search"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11.5 20h-6.5a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" /><path d="M9 17h2" /><path d="M18 18m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M20.2 20.2l1.8 1.8" /></svg>
34
252
  </span>
35
253
  `;
36
254
  this.toggleButton.onclick = () => this.togglePanel();
37
255
 
256
+ this.builtinTabsContainer = this.toggleButton.querySelector( '#builtin-tabs-container' );
257
+
258
+ // Create mini-panel for builtin tabs (shown when panel is hidden)
259
+ this.miniPanel = document.createElement( 'div' );
260
+ this.miniPanel.id = 'profiler-mini-panel';
261
+ this.miniPanel.className = 'profiler-mini-panel';
262
+
38
263
  this.panel = document.createElement( 'div' );
39
264
  this.panel.id = 'profiler-panel';
40
265
 
41
266
  const header = document.createElement( 'div' );
42
267
  header.className = 'profiler-header';
268
+
269
+ // Enable horizontal scrolling with vertical mouse wheel
270
+ header.addEventListener( 'wheel', ( e ) => {
271
+
272
+ if ( e.deltaY !== 0 ) {
273
+
274
+ e.preventDefault();
275
+ header.scrollLeft += e.deltaY * .25;
276
+
277
+ }
278
+
279
+ }, { passive: false } );
280
+
43
281
  this.tabsContainer = document.createElement( 'div' );
44
282
  this.tabsContainer.className = 'profiler-tabs';
45
283
 
46
284
  const controls = document.createElement( 'div' );
47
285
  controls.className = 'profiler-controls';
48
286
 
287
+ this.floatingBtn = document.createElement( 'button' );
288
+ this.floatingBtn.id = 'floating-btn';
289
+ this.floatingBtn.title = 'Switch to Right Side';
290
+ this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="15" y1="3" x2="15" y2="21"></line></svg>';
291
+ this.floatingBtn.onclick = () => this.togglePosition();
292
+
293
+ // Hide position toggle button on small screens
294
+ if ( this.isSmallScreen ) {
295
+
296
+ this.floatingBtn.style.display = 'none';
297
+ this.panel.classList.add( 'hide-position-toggle' );
298
+
299
+ }
300
+
301
+ if ( this.isMobile ) {
302
+
303
+ this.panel.classList.add( 'is-mobile' );
304
+
305
+ }
306
+
49
307
  this.maximizeBtn = document.createElement( 'button' );
50
308
  this.maximizeBtn.id = 'maximize-btn';
51
309
  this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
@@ -56,7 +314,7 @@ export class Profiler {
56
314
  hideBtn.textContent = '-';
57
315
  hideBtn.onclick = () => this.togglePanel();
58
316
 
59
- controls.append( this.maximizeBtn, hideBtn );
317
+ controls.append( this.floatingBtn, this.maximizeBtn, hideBtn );
60
318
  header.append( this.tabsContainer, controls );
61
319
 
62
320
  this.contentWrapper = document.createElement( 'div' );
@@ -67,7 +325,17 @@ export class Profiler {
67
325
 
68
326
  this.panel.append( resizer, header, this.contentWrapper );
69
327
 
70
- this.domElement.append( this.toggleButton, this.panel );
328
+ this.domElement.append( this.toggleButton, this.miniPanel, this.panel );
329
+
330
+ // Set initial position class
331
+ this.panel.classList.add( `position-${this.position}` );
332
+
333
+ if ( this.position === 'right' ) {
334
+
335
+ this.toggleButton.classList.add( 'position-right' );
336
+ this.miniPanel.classList.add( 'position-right' );
337
+
338
+ }
71
339
 
72
340
  }
73
341
 
@@ -79,48 +347,81 @@ export class Profiler {
79
347
 
80
348
  this.isResizing = true;
81
349
  this.panel.classList.add( 'resizing' );
82
- const startY = e.clientY || e.touches[ 0 ].clientY;
350
+ resizer.setPointerCapture( e.pointerId );
351
+ const startX = e.clientX;
352
+ const startY = e.clientY;
83
353
  const startHeight = this.panel.offsetHeight;
354
+ const startWidth = this.panel.offsetWidth;
84
355
 
85
356
  const onMove = ( moveEvent ) => {
86
357
 
87
358
  if ( ! this.isResizing ) return;
88
359
  moveEvent.preventDefault();
89
- const currentY = moveEvent.clientY || moveEvent.touches[ 0 ].clientY;
90
- const newHeight = startHeight - ( currentY - startY );
91
- if ( newHeight > 100 && newHeight < window.innerHeight - 50 ) {
360
+ const currentX = moveEvent.clientX;
361
+ const currentY = moveEvent.clientY;
362
+
363
+ if ( this.position === 'bottom' ) {
364
+
365
+ // Vertical resize for bottom position
366
+ const newHeight = startHeight - ( currentY - startY );
367
+
368
+ if ( newHeight > 100 && newHeight < window.innerHeight - 50 ) {
369
+
370
+ this.panel.style.height = `${ newHeight }px`;
371
+
372
+ }
373
+
374
+ } else if ( this.position === 'right' ) {
92
375
 
93
- this.panel.style.height = `${newHeight}px`;
376
+ // Horizontal resize for right position
377
+ const newWidth = startWidth - ( currentX - startX );
378
+
379
+ if ( newWidth > 200 && newWidth < window.innerWidth - 50 ) {
380
+
381
+ this.panel.style.width = `${ newWidth }px`;
382
+
383
+ }
94
384
 
95
385
  }
96
386
 
387
+ this.dispatchEvent( { type: 'resize' } );
388
+
97
389
  };
98
390
 
99
391
  const onEnd = () => {
100
392
 
101
393
  this.isResizing = false;
102
394
  this.panel.classList.remove( 'resizing' );
103
- document.removeEventListener( 'mousemove', onMove );
104
- document.removeEventListener( 'mouseup', onEnd );
105
- document.removeEventListener( 'touchmove', onMove );
106
- document.removeEventListener( 'touchend', onEnd );
395
+ resizer.removeEventListener( 'pointermove', onMove );
396
+ resizer.removeEventListener( 'pointerup', onEnd );
397
+ resizer.removeEventListener( 'pointercancel', onEnd );
107
398
  if ( ! this.panel.classList.contains( 'maximized' ) ) {
108
399
 
109
- this.lastHeight = this.panel.offsetHeight;
400
+ // Save dimensions based on current position
401
+ if ( this.position === 'bottom' ) {
402
+
403
+ this.lastHeightBottom = this.panel.offsetHeight;
404
+
405
+ } else if ( this.position === 'right' ) {
406
+
407
+ this.lastWidthRight = this.panel.offsetWidth;
408
+
409
+ }
410
+
411
+ // Save layout after resize
412
+ this.saveLayout();
110
413
 
111
414
  }
112
415
 
113
416
  };
114
417
 
115
- document.addEventListener( 'mousemove', onMove );
116
- document.addEventListener( 'mouseup', onEnd );
117
- document.addEventListener( 'touchmove', onMove, { passive: false } );
118
- document.addEventListener( 'touchend', onEnd );
418
+ resizer.addEventListener( 'pointermove', onMove );
419
+ resizer.addEventListener( 'pointerup', onEnd );
420
+ resizer.addEventListener( 'pointercancel', onEnd );
119
421
 
120
422
  };
121
423
 
122
- resizer.addEventListener( 'mousedown', onStart );
123
- resizer.addEventListener( 'touchstart', onStart );
424
+ resizer.addEventListener( 'pointerdown', onStart );
124
425
 
125
426
  }
126
427
 
@@ -129,41 +430,1642 @@ export class Profiler {
129
430
  if ( this.panel.classList.contains( 'maximized' ) ) {
130
431
 
131
432
  this.panel.classList.remove( 'maximized' );
132
- this.panel.style.height = `${ this.lastHeight }px`;
433
+
434
+ // Restore size based on current position
435
+ if ( this.position === 'bottom' ) {
436
+
437
+ this.panel.style.height = `${ this.lastHeightBottom }px`;
438
+ this.panel.style.width = '100%';
439
+
440
+ } else if ( this.position === 'right' ) {
441
+
442
+ this.panel.style.height = '100%';
443
+ this.panel.style.width = `${ this.lastWidthRight }px`;
444
+
445
+ }
446
+
133
447
  this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
134
448
 
135
449
  } else {
136
450
 
137
- this.lastHeight = this.panel.offsetHeight;
451
+ // Save current size before maximizing
452
+ if ( this.position === 'bottom' ) {
453
+
454
+ this.lastHeightBottom = this.panel.offsetHeight;
455
+
456
+ } else if ( this.position === 'right' ) {
457
+
458
+ this.lastWidthRight = this.panel.offsetWidth;
459
+
460
+ }
461
+
138
462
  this.panel.classList.add( 'maximized' );
139
- this.panel.style.height = '100vh';
463
+
464
+ // Maximize based on current position
465
+ if ( this.position === 'bottom' ) {
466
+
467
+ this.panel.style.height = '100vh';
468
+ this.panel.style.width = '100%';
469
+
470
+ } else if ( this.position === 'right' ) {
471
+
472
+ this.panel.style.height = '100%';
473
+ this.panel.style.width = '100vw';
474
+
475
+ }
476
+
140
477
  this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="12" height="12" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>';
141
478
 
142
479
  }
143
480
 
481
+ this.dispatchEvent( { type: 'resize' } );
482
+
483
+ }
484
+
485
+ hide() {
486
+
487
+ this.miniPanel.classList.remove( 'visible' );
488
+
489
+ this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
490
+
491
+ content.style.display = 'none';
492
+
493
+ } );
494
+
495
+ this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ).forEach( btn => {
496
+
497
+ btn.classList.remove( 'active' );
498
+
499
+ } );
500
+
501
+ }
502
+
503
+ show( tab ) {
504
+
505
+ this.hide();
506
+
507
+ tab.builtinButton.classList.add( 'active' );
508
+
509
+ if ( ! tab.miniContent.firstChild ) {
510
+
511
+ while ( tab.content.firstChild ) {
512
+
513
+ tab.miniContent.appendChild( tab.content.firstChild );
514
+
515
+ }
516
+
517
+ }
518
+
519
+ tab.miniContent.style.display = 'block';
520
+ this.miniPanel.classList.add( 'visible' );
521
+
144
522
  }
145
523
 
146
524
  addTab( tab ) {
147
525
 
148
526
  this.tabs[ tab.id ] = tab;
149
- tab.button.onclick = () => this.setActiveTab( tab.id );
150
- this.tabsContainer.appendChild( tab.button );
527
+
528
+ // Assign a permanent original index to this tab
529
+ tab.originalIndex = this.nextTabOriginalIndex ++;
530
+
531
+ // Add visual indicator for tabs that cannot be detached
532
+ if ( tab.allowDetach === false ) {
533
+
534
+ tab.button.classList.add( 'no-detach' );
535
+
536
+ }
537
+
538
+ // Set visibility change callback
539
+ tab.onVisibilityChange = () => this.updatePanelSize();
540
+
541
+ this.setupTabDragAndDrop( tab );
542
+
543
+ if ( ! tab.builtin ) {
544
+
545
+ this.tabsContainer.appendChild( tab.button );
546
+
547
+ }
548
+
151
549
  this.contentWrapper.appendChild( tab.content );
152
550
 
551
+ // Apply the current visibility state to the DOM elements
552
+ if ( ! tab.isVisible ) {
553
+
554
+ tab.button.style.display = 'none';
555
+ tab.content.style.display = 'none';
556
+
557
+ }
558
+
559
+ // If tab is builtin, add it to the profiler-toggle button
560
+ if ( tab.builtin ) {
561
+
562
+ this.addBuiltinTab( tab );
563
+
564
+ }
565
+
566
+ // Update panel size when tabs change
567
+ this.updatePanelSize();
568
+
569
+ // Set profiler reference
570
+ tab.profiler = this;
571
+
153
572
  }
154
573
 
155
- setActiveTab( id ) {
574
+ addBuiltinTab( tab ) {
156
575
 
157
- if ( this.activeTabId ) this.tabs[ this.activeTabId ].setActive( false );
158
- this.activeTabId = id;
159
- this.tabs[ id ].setActive( true );
576
+ // Create a button for the builtin tab in the profiler-toggle
577
+ const builtinButton = document.createElement( 'button' );
578
+ builtinButton.className = 'builtin-tab-btn';
579
+
580
+ // Use icon if provided, otherwise use first letter
581
+ if ( tab.icon ) {
582
+
583
+ builtinButton.innerHTML = tab.icon;
584
+
585
+ } else {
586
+
587
+ builtinButton.textContent = tab.button.textContent.charAt( 0 ).toUpperCase();
588
+
589
+ }
590
+
591
+ builtinButton.title = tab.button.textContent;
592
+
593
+ // Create mini-panel content container for this tab
594
+ const miniContent = document.createElement( 'div' );
595
+ miniContent.className = 'mini-panel-content';
596
+ miniContent.style.display = 'none';
597
+
598
+ // Store references in the tab object
599
+ tab.builtinButton = builtinButton;
600
+ tab.miniContent = miniContent;
601
+
602
+ this.miniPanel.appendChild( miniContent );
603
+
604
+ builtinButton.onclick = ( e ) => {
605
+
606
+ e.stopPropagation(); // Prevent toggle panel from triggering
607
+
608
+ // Toggle mini-panel for this tab
609
+ const isCurrentlyActive = miniContent.style.display !== 'none' && miniContent.children.length > 0;
610
+
611
+ if ( isCurrentlyActive ) {
612
+
613
+ this.hide();
614
+
615
+ } else {
616
+
617
+ this.show( tab );
618
+
619
+ }
620
+
621
+ };
622
+
623
+ this.builtinTabsContainer.appendChild( builtinButton );
624
+
625
+ // Store references
626
+ tab.builtinButton = builtinButton;
627
+ tab.miniContent = miniContent;
628
+
629
+ // If the tab was hidden before being added, hide the builtin button
630
+ if ( ! tab.isVisible ) {
631
+
632
+ builtinButton.style.display = 'none';
633
+ miniContent.style.display = 'none';
634
+
635
+ // Hide the builtin-tabs-container if all builtin buttons are hidden
636
+ const hasVisibleBuiltinButtons = Array.from( this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ) )
637
+ .some( btn => btn.style.display !== 'none' );
638
+
639
+ if ( ! hasVisibleBuiltinButtons ) {
640
+
641
+ this.builtinTabsContainer.style.display = 'none';
642
+
643
+ }
644
+
645
+ }
160
646
 
161
647
  }
162
648
 
163
- togglePanel() {
649
+ removeTab( tab ) {
164
650
 
165
- this.panel.classList.toggle( 'visible' );
166
- this.toggleButton.classList.toggle( 'hidden' );
651
+ if ( ! tab || this.tabs[ tab.id ] === undefined ) return;
652
+
653
+ delete this.tabs[ tab.id ];
654
+
655
+ if ( tab.isDetached && tab.detachedWindow ) {
656
+
657
+ if ( tab.detachedWindow.panel && tab.detachedWindow.panel.parentNode ) {
658
+
659
+ tab.detachedWindow.panel.parentNode.removeChild( tab.detachedWindow.panel );
660
+
661
+ }
662
+
663
+ const index = this.detachedWindows.indexOf( tab.detachedWindow );
664
+
665
+ if ( index !== - 1 ) {
666
+
667
+ this.detachedWindows.splice( index, 1 );
668
+
669
+ }
670
+
671
+ }
672
+
673
+ if ( ! tab.builtin ) {
674
+
675
+ if ( tab.button && tab.button.parentNode ) {
676
+
677
+ tab.button.parentNode.removeChild( tab.button );
678
+
679
+ }
680
+
681
+ } else {
682
+
683
+ if ( tab.builtinButton && tab.builtinButton.parentNode ) {
684
+
685
+ tab.builtinButton.parentNode.removeChild( tab.builtinButton );
686
+
687
+ }
688
+
689
+ if ( tab.miniContent && tab.miniContent.parentNode ) {
690
+
691
+ tab.miniContent.parentNode.removeChild( tab.miniContent );
692
+
693
+ }
694
+
695
+ // Clean up builtin container if empty
696
+ const hasVisibleBuiltinButtons = Array.from( this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ) )
697
+ .some( btn => btn.style.display !== 'none' );
698
+
699
+ if ( ! hasVisibleBuiltinButtons ) {
700
+
701
+ this.builtinTabsContainer.style.display = 'none';
702
+
703
+ }
704
+
705
+ }
706
+
707
+ if ( tab.content && tab.content.parentNode ) {
708
+
709
+ tab.content.parentNode.removeChild( tab.content );
710
+
711
+ }
712
+
713
+ if ( this.activeTabId === tab.id ) {
714
+
715
+ this.activeTabId = null;
716
+
717
+ // Try to activate another tab
718
+ const remainingTabs = Object.values( this.tabs ).filter( t => ! t.isDetached && t.isVisible );
719
+
720
+ if ( remainingTabs.length > 0 ) {
721
+
722
+ this.setActiveTab( remainingTabs[ 0 ].id );
723
+
724
+ } else {
725
+
726
+ this.updatePanelSize();
727
+
728
+ }
729
+
730
+ } else {
731
+
732
+ this.updatePanelSize();
733
+
734
+ }
735
+
736
+ tab.onVisibilityChange = null;
737
+ tab.profiler = null;
738
+
739
+ }
740
+
741
+ updatePanelSize() {
742
+
743
+ // Check if there are any visible tabs in the panel
744
+ const hasVisibleTabs = Object.values( this.tabs ).some( tab => ! tab.isDetached && tab.isVisible );
745
+
746
+ // Add or remove CSS class to indicate no tabs state
747
+ if ( ! hasVisibleTabs ) {
748
+
749
+ this.panel.classList.add( 'no-tabs' );
750
+
751
+ // If maximized and no tabs, restore to normal size
752
+ if ( this.panel.classList.contains( 'maximized' ) ) {
753
+
754
+ this.panel.classList.remove( 'maximized' );
755
+ this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
756
+
757
+ }
758
+
759
+ // No tabs visible - set to minimum size
760
+ if ( this.position === 'bottom' ) {
761
+
762
+ this.panel.style.height = '38px';
763
+
764
+ } else if ( this.position === 'right' ) {
765
+
766
+ // 45px = width of one button column
767
+ this.panel.style.width = '45px';
768
+
769
+ }
770
+
771
+ } else {
772
+
773
+ this.panel.classList.remove( 'no-tabs' );
774
+
775
+ if ( Object.keys( this.tabs ).length > 0 ) {
776
+
777
+ // Has tabs - restore to saved size only if we had set it to minimum before
778
+ if ( this.position === 'bottom' ) {
779
+
780
+ const currentHeight = parseInt( this.panel.style.height );
781
+ if ( currentHeight === 38 ) {
782
+
783
+ this.panel.style.height = `${ this.lastHeightBottom }px`;
784
+
785
+ }
786
+
787
+ } else if ( this.position === 'right' ) {
788
+
789
+ const currentWidth = parseInt( this.panel.style.width );
790
+ if ( currentWidth === 45 ) {
791
+
792
+ this.panel.style.width = `${ this.lastWidthRight }px`;
793
+
794
+ }
795
+
796
+ }
797
+
798
+ }
799
+
800
+ }
801
+
802
+ this.dispatchEvent( { type: 'resize' } );
803
+
804
+ }
805
+
806
+ setupTabDragAndDrop( tab ) {
807
+
808
+ // Always handle basic click
809
+ tab.button.addEventListener( 'click', () => {
810
+
811
+ if ( ! isDragging ) {
812
+
813
+ this.setActiveTab( tab.id );
814
+
815
+ }
816
+
817
+ } );
818
+
819
+ // Disable drag and drop if tab doesn't allow detach
820
+ if ( tab.allowDetach === false ) {
821
+
822
+ tab.button.style.cursor = 'default';
823
+
824
+ return;
825
+
826
+ }
827
+
828
+ let isDragging = false;
829
+ let startX, startY;
830
+ let hasMoved = false;
831
+ let previewWindow = null;
832
+ const dragThreshold = 10; // pixels to move before starting drag
833
+
834
+ const onDragStart = ( e ) => {
835
+
836
+ startX = e.clientX;
837
+ startY = e.clientY;
838
+ isDragging = false;
839
+ hasMoved = false;
840
+ tab.button.setPointerCapture( e.pointerId );
841
+
842
+ };
843
+
844
+ const onDragMove = ( e ) => {
845
+
846
+ const currentX = e.clientX;
847
+ const currentY = e.clientY;
848
+
849
+ const deltaX = Math.abs( currentX - startX );
850
+ const deltaY = Math.abs( currentY - startY );
851
+
852
+ if ( ! isDragging && ( deltaX > dragThreshold || deltaY > dragThreshold ) ) {
853
+
854
+ isDragging = true;
855
+ tab.button.style.cursor = 'grabbing';
856
+ tab.button.style.opacity = '0.5';
857
+ tab.button.style.transform = 'scale(1.05)';
858
+
859
+ previewWindow = this.createPreviewWindow( tab, currentX, currentY );
860
+ previewWindow.style.opacity = '0.8';
861
+
862
+ }
863
+
864
+ if ( isDragging && previewWindow ) {
865
+
866
+ hasMoved = true;
867
+ e.preventDefault();
868
+
869
+ previewWindow.style.left = `${ currentX - 200 }px`;
870
+ previewWindow.style.top = `${ currentY - 20 }px`;
871
+
872
+ }
873
+
874
+ };
875
+
876
+ const onDragEnd = () => {
877
+
878
+ if ( isDragging && hasMoved && previewWindow ) {
879
+
880
+ if ( previewWindow.parentNode ) {
881
+
882
+ previewWindow.parentNode.removeChild( previewWindow );
883
+
884
+ }
885
+
886
+ const finalX = parseInt( previewWindow.style.left ) + 200;
887
+ const finalY = parseInt( previewWindow.style.top ) + 20;
888
+
889
+ this.detachTab( tab, finalX, finalY );
890
+
891
+ } else if ( ! hasMoved ) {
892
+
893
+ this.setActiveTab( tab.id );
894
+
895
+ if ( previewWindow && previewWindow.parentNode ) {
896
+
897
+ previewWindow.parentNode.removeChild( previewWindow );
898
+
899
+ }
900
+
901
+ } else if ( previewWindow ) {
902
+
903
+ if ( previewWindow.parentNode ) {
904
+
905
+ previewWindow.parentNode.removeChild( previewWindow );
906
+
907
+ }
908
+
909
+ }
910
+
911
+ tab.button.style.opacity = '';
912
+ tab.button.style.transform = '';
913
+ tab.button.style.cursor = '';
914
+ isDragging = false;
915
+ hasMoved = false;
916
+ previewWindow = null;
917
+
918
+ tab.button.removeEventListener( 'pointermove', onDragMove );
919
+ tab.button.removeEventListener( 'pointerup', onDragEnd );
920
+ tab.button.removeEventListener( 'pointercancel', onDragEnd );
921
+
922
+ };
923
+
924
+ tab.button.addEventListener( 'pointerdown', ( e ) => {
925
+
926
+ if ( this.isMobile && e.pointerType !== 'mouse' ) return;
927
+
928
+ onDragStart( e );
929
+ tab.button.addEventListener( 'pointermove', onDragMove );
930
+ tab.button.addEventListener( 'pointerup', onDragEnd );
931
+ tab.button.addEventListener( 'pointercancel', onDragEnd );
932
+
933
+ } );
934
+
935
+ // Set cursor to grab for tabs that can be detached
936
+ tab.button.style.cursor = 'grab';
937
+
938
+ }
939
+
940
+ createPreviewWindow( tab, x, y ) {
941
+
942
+ const windowPanel = document.createElement( 'div' );
943
+ windowPanel.className = 'detached-tab-panel';
944
+ windowPanel.style.left = `${ x - 200 }px`;
945
+ windowPanel.style.top = `${ y - 20 }px`;
946
+ windowPanel.style.pointerEvents = 'none'; // Preview only
947
+
948
+ // Set z-index for preview window to be on top
949
+ this.maxZIndex ++;
950
+ windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
951
+
952
+ const windowHeader = document.createElement( 'div' );
953
+ windowHeader.className = 'detached-tab-header';
954
+
955
+ const title = document.createElement( 'span' );
956
+ title.textContent = tab.button.textContent.replace( '⇱', '' ).trim();
957
+ windowHeader.appendChild( title );
958
+
959
+ const headerControls = document.createElement( 'div' );
960
+ headerControls.className = 'detached-header-controls';
961
+
962
+ const reattachBtn = document.createElement( 'button' );
963
+ reattachBtn.className = 'detached-reattach-btn';
964
+ reattachBtn.innerHTML = '↩';
965
+ headerControls.appendChild( reattachBtn );
966
+ windowHeader.appendChild( headerControls );
967
+
968
+ const windowContent = document.createElement( 'div' );
969
+ windowContent.className = 'detached-tab-content';
970
+
971
+ const resizer = document.createElement( 'div' );
972
+ resizer.className = 'detached-tab-resizer';
973
+
974
+ windowPanel.appendChild( resizer );
975
+ windowPanel.appendChild( windowHeader );
976
+ windowPanel.appendChild( windowContent );
977
+
978
+ document.body.appendChild( windowPanel );
979
+
980
+ return windowPanel;
981
+
982
+ }
983
+
984
+ detachTab( tab, x, y ) {
985
+
986
+ if ( tab.isDetached ) return;
987
+
988
+ // Check if tab allows detachment
989
+ if ( tab.allowDetach === false ) return;
990
+
991
+ const allButtons = Array.from( this.tabsContainer.children );
992
+
993
+ const tabIdsInOrder = allButtons.map( btn => {
994
+
995
+ return Object.keys( this.tabs ).find( id => this.tabs[ id ].button === btn );
996
+
997
+ } ).filter( id => id !== undefined );
998
+
999
+ const currentIndex = tabIdsInOrder.indexOf( tab.id );
1000
+
1001
+ let newActiveTab = null;
1002
+
1003
+ if ( this.activeTabId === tab.id ) {
1004
+
1005
+ tab.setActive( false );
1006
+
1007
+ const remainingTabs = tabIdsInOrder.filter( id =>
1008
+ id !== tab.id &&
1009
+ ! this.tabs[ id ].isDetached &&
1010
+ this.tabs[ id ].isVisible
1011
+ );
1012
+
1013
+ if ( remainingTabs.length > 0 ) {
1014
+
1015
+ for ( let i = currentIndex - 1; i >= 0; i -- ) {
1016
+
1017
+ if ( remainingTabs.includes( tabIdsInOrder[ i ] ) ) {
1018
+
1019
+ newActiveTab = tabIdsInOrder[ i ];
1020
+ break;
1021
+
1022
+ }
1023
+
1024
+ }
1025
+
1026
+ if ( ! newActiveTab ) {
1027
+
1028
+ for ( let i = currentIndex + 1; i < tabIdsInOrder.length; i ++ ) {
1029
+
1030
+ if ( remainingTabs.includes( tabIdsInOrder[ i ] ) ) {
1031
+
1032
+ newActiveTab = tabIdsInOrder[ i ];
1033
+ break;
1034
+
1035
+ }
1036
+
1037
+ }
1038
+
1039
+ }
1040
+
1041
+ if ( ! newActiveTab ) {
1042
+
1043
+ newActiveTab = remainingTabs[ 0 ];
1044
+
1045
+ }
1046
+
1047
+ }
1048
+
1049
+ }
1050
+
1051
+ if ( tab.button.parentNode ) {
1052
+
1053
+ tab.button.parentNode.removeChild( tab.button );
1054
+
1055
+ }
1056
+
1057
+ if ( tab.content.parentNode ) {
1058
+
1059
+ tab.content.parentNode.removeChild( tab.content );
1060
+
1061
+ }
1062
+
1063
+ const detachedWindow = this.createDetachedWindow( tab, x, y );
1064
+ this.detachedWindows.push( detachedWindow );
1065
+
1066
+ tab.isDetached = true;
1067
+ tab.detachedWindow = detachedWindow;
1068
+
1069
+ if ( newActiveTab ) {
1070
+
1071
+ this.setActiveTab( newActiveTab );
1072
+
1073
+ } else if ( this.activeTabId === tab.id ) {
1074
+
1075
+ this.activeTabId = null;
1076
+
1077
+ }
1078
+
1079
+ // Update panel size after detaching
1080
+ this.updatePanelSize();
1081
+
1082
+ this.saveLayout();
1083
+
1084
+ }
1085
+
1086
+ createDetachedWindow( tab, x, y ) {
1087
+
1088
+ // Constrain initial position to window bounds
1089
+ const windowWidth = window.innerWidth;
1090
+ const windowHeight = window.innerHeight;
1091
+ const estimatedWidth = 400; // Default detached window width
1092
+ const estimatedHeight = 300; // Default detached window height
1093
+
1094
+ let constrainedX = x - 200;
1095
+ let constrainedY = y - 20;
1096
+
1097
+ if ( constrainedX + estimatedWidth > windowWidth ) {
1098
+
1099
+ constrainedX = windowWidth - estimatedWidth;
1100
+
1101
+ }
1102
+
1103
+ if ( constrainedX < 0 ) {
1104
+
1105
+ constrainedX = 0;
1106
+
1107
+ }
1108
+
1109
+ if ( constrainedY + estimatedHeight > windowHeight ) {
1110
+
1111
+ constrainedY = windowHeight - estimatedHeight;
1112
+
1113
+ }
1114
+
1115
+ if ( constrainedY < 0 ) {
1116
+
1117
+ constrainedY = 0;
1118
+
1119
+ }
1120
+
1121
+ const windowPanel = document.createElement( 'div' );
1122
+ windowPanel.className = 'detached-tab-panel';
1123
+ windowPanel.style.left = `${ constrainedX }px`;
1124
+ windowPanel.style.top = `${ constrainedY }px`;
1125
+
1126
+ if ( ! this.panel.classList.contains( 'visible' ) ) {
1127
+
1128
+ windowPanel.style.opacity = '0';
1129
+ windowPanel.style.visibility = 'hidden';
1130
+ windowPanel.style.pointerEvents = 'none';
1131
+
1132
+ }
1133
+
1134
+ // Hide detached window if tab is not visible
1135
+ if ( ! tab.isVisible ) {
1136
+
1137
+ windowPanel.style.display = 'none';
1138
+
1139
+ }
1140
+
1141
+ const windowHeader = document.createElement( 'div' );
1142
+ windowHeader.className = 'detached-tab-header';
1143
+
1144
+ const title = document.createElement( 'span' );
1145
+ title.textContent = tab.button.textContent.replace( '⇱', '' ).trim();
1146
+ windowHeader.appendChild( title );
1147
+
1148
+ const headerControls = document.createElement( 'div' );
1149
+ headerControls.className = 'detached-header-controls';
1150
+
1151
+ const reattachBtn = document.createElement( 'button' );
1152
+ reattachBtn.className = 'detached-reattach-btn';
1153
+ reattachBtn.innerHTML = '↩';
1154
+ reattachBtn.title = 'Reattach to main panel';
1155
+ reattachBtn.onclick = () => this.reattachTab( tab );
1156
+
1157
+ headerControls.appendChild( reattachBtn );
1158
+ windowHeader.appendChild( headerControls );
1159
+
1160
+ const windowContent = document.createElement( 'div' );
1161
+ windowContent.className = 'detached-tab-content';
1162
+ windowContent.appendChild( tab.content );
1163
+
1164
+ // Make sure content is visible
1165
+ tab.content.style.display = 'block';
1166
+ tab.content.classList.add( 'active' );
1167
+
1168
+ // Create resize handles for all edges
1169
+ const resizerTop = document.createElement( 'div' );
1170
+ resizerTop.className = 'detached-tab-resizer-top';
1171
+
1172
+ const resizerRight = document.createElement( 'div' );
1173
+ resizerRight.className = 'detached-tab-resizer-right';
1174
+
1175
+ const resizerBottom = document.createElement( 'div' );
1176
+ resizerBottom.className = 'detached-tab-resizer-bottom';
1177
+
1178
+ const resizerLeft = document.createElement( 'div' );
1179
+ resizerLeft.className = 'detached-tab-resizer-left';
1180
+
1181
+ const resizerCorner = document.createElement( 'div' );
1182
+ resizerCorner.className = 'detached-tab-resizer';
1183
+
1184
+ windowPanel.appendChild( resizerTop );
1185
+ windowPanel.appendChild( resizerRight );
1186
+ windowPanel.appendChild( resizerBottom );
1187
+ windowPanel.appendChild( resizerLeft );
1188
+ windowPanel.appendChild( resizerCorner );
1189
+ windowPanel.appendChild( windowHeader );
1190
+ windowPanel.appendChild( windowContent );
1191
+
1192
+ document.body.appendChild( windowPanel );
1193
+
1194
+ // Setup window dragging
1195
+ this.setupDetachedWindowDrag( windowPanel, windowHeader, tab );
1196
+
1197
+ // Setup window resizing
1198
+ this.setupDetachedWindowResize( windowPanel, resizerTop, resizerRight, resizerBottom, resizerLeft, resizerCorner );
1199
+
1200
+ // Use the same z-index that was set on the preview window
1201
+ windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
1202
+
1203
+ return { panel: windowPanel, tab: tab };
1204
+
1205
+ }
1206
+
1207
+ bringWindowToFront( windowPanel ) {
1208
+
1209
+ // Increment the max z-index and apply it to the clicked window
1210
+ this.maxZIndex ++;
1211
+ windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
1212
+
1213
+ }
1214
+
1215
+ setupDetachedWindowDrag( windowPanel, header, tab ) {
1216
+
1217
+ let isDragging = false;
1218
+ let startX, startY, startLeft, startTop;
1219
+
1220
+ // Bring window to front when clicking anywhere on it
1221
+ windowPanel.addEventListener( 'pointerdown', () => {
1222
+
1223
+ this.bringWindowToFront( windowPanel );
1224
+
1225
+ } );
1226
+
1227
+ const onDragStart = ( e ) => {
1228
+
1229
+ if ( e.target.classList.contains( 'detached-reattach-btn' ) ) {
1230
+
1231
+ return;
1232
+
1233
+ }
1234
+
1235
+ // Bring window to front when starting to drag
1236
+ this.bringWindowToFront( windowPanel );
1237
+
1238
+ isDragging = true;
1239
+ header.style.cursor = 'grabbing';
1240
+ header.setPointerCapture( e.pointerId );
1241
+
1242
+ startX = e.clientX;
1243
+ startY = e.clientY;
1244
+
1245
+ const rect = windowPanel.getBoundingClientRect();
1246
+ startLeft = rect.left;
1247
+ startTop = rect.top;
1248
+
1249
+ };
1250
+
1251
+ const onDragMove = ( e ) => {
1252
+
1253
+ if ( ! isDragging ) return;
1254
+
1255
+ e.preventDefault();
1256
+
1257
+ const currentX = e.clientX;
1258
+ const currentY = e.clientY;
1259
+
1260
+ const deltaX = currentX - startX;
1261
+ const deltaY = currentY - startY;
1262
+
1263
+ let newLeft = startLeft + deltaX;
1264
+ let newTop = startTop + deltaY;
1265
+
1266
+ // Constrain to window bounds (allow half width/height to extend outside)
1267
+ const windowWidth = window.innerWidth;
1268
+ const windowHeight = window.innerHeight;
1269
+ const panelWidth = windowPanel.offsetWidth;
1270
+ const panelHeight = windowPanel.offsetHeight;
1271
+ const halfWidth = panelWidth / 2;
1272
+ const halfHeight = panelHeight / 2;
1273
+
1274
+ // Allow window to extend half its width beyond right edge
1275
+ if ( newLeft + panelWidth > windowWidth + halfWidth ) {
1276
+
1277
+ newLeft = windowWidth + halfWidth - panelWidth;
1278
+
1279
+ }
1280
+
1281
+ // Allow window to extend half its width beyond left edge
1282
+ if ( newLeft < - halfWidth ) {
1283
+
1284
+ newLeft = - halfWidth;
1285
+
1286
+ }
1287
+
1288
+ // Allow window to extend half its height beyond bottom edge
1289
+ if ( newTop + panelHeight > windowHeight + halfHeight ) {
1290
+
1291
+ newTop = windowHeight + halfHeight - panelHeight;
1292
+
1293
+ }
1294
+
1295
+ // Allow window to extend half its height beyond top edge
1296
+ if ( newTop < - halfHeight ) {
1297
+
1298
+ newTop = - halfHeight;
1299
+
1300
+ }
1301
+
1302
+ windowPanel.style.left = `${ newLeft }px`;
1303
+ windowPanel.style.top = `${ newTop }px`;
1304
+
1305
+ // Check if cursor is over the inspector panel
1306
+ const panelRect = this.panel.getBoundingClientRect();
1307
+ const isOverPanel = currentX >= panelRect.left && currentX <= panelRect.right &&
1308
+ currentY >= panelRect.top && currentY <= panelRect.bottom;
1309
+
1310
+ if ( isOverPanel ) {
1311
+
1312
+ windowPanel.style.opacity = '0.5';
1313
+ this.panel.style.outline = '2px solid var(--accent-color)';
1314
+
1315
+ } else {
1316
+
1317
+ windowPanel.style.opacity = '';
1318
+ this.panel.style.outline = '';
1319
+
1320
+ }
1321
+
1322
+ };
1323
+
1324
+ const onDragEnd = ( e ) => {
1325
+
1326
+ if ( ! isDragging ) return;
1327
+
1328
+ isDragging = false;
1329
+ header.style.cursor = '';
1330
+ windowPanel.style.opacity = '';
1331
+ this.panel.style.outline = '';
1332
+
1333
+ // Check if dropped over the inspector panel
1334
+ const currentX = e.clientX;
1335
+ const currentY = e.clientY;
1336
+
1337
+ if ( currentX !== undefined && currentY !== undefined ) {
1338
+
1339
+ const panelRect = this.panel.getBoundingClientRect();
1340
+ const isOverPanel = currentX >= panelRect.left && currentX <= panelRect.right &&
1341
+ currentY >= panelRect.top && currentY <= panelRect.bottom;
1342
+
1343
+ if ( isOverPanel && tab ) {
1344
+
1345
+ // Reattach the tab
1346
+ this.reattachTab( tab );
1347
+
1348
+ } else {
1349
+
1350
+ // Save layout after moving detached window
1351
+ this.saveLayout();
1352
+
1353
+ }
1354
+
1355
+ }
1356
+
1357
+ header.removeEventListener( 'pointermove', onDragMove );
1358
+ header.removeEventListener( 'pointerup', onDragEnd );
1359
+ header.removeEventListener( 'pointercancel', onDragEnd );
1360
+
1361
+ };
1362
+
1363
+ header.addEventListener( 'pointerdown', ( e ) => {
1364
+
1365
+ onDragStart( e );
1366
+ header.addEventListener( 'pointermove', onDragMove );
1367
+ header.addEventListener( 'pointerup', onDragEnd );
1368
+ header.addEventListener( 'pointercancel', onDragEnd );
1369
+
1370
+ } );
1371
+
1372
+ header.style.cursor = 'grab';
1373
+
1374
+ }
1375
+
1376
+ setupDetachedWindowResize( windowPanel, resizerTop, resizerRight, resizerBottom, resizerLeft, resizerCorner ) {
1377
+
1378
+ const minWidth = 250;
1379
+ const minHeight = 150;
1380
+
1381
+ const setupResizer = ( resizer, direction ) => {
1382
+
1383
+ let isResizing = false;
1384
+ let startX, startY, startWidth, startHeight, startLeft, startTop;
1385
+
1386
+ const onResizeStart = ( e ) => {
1387
+
1388
+ e.preventDefault();
1389
+ e.stopPropagation();
1390
+ isResizing = true;
1391
+
1392
+ // Bring window to front when resizing
1393
+ this.bringWindowToFront( windowPanel );
1394
+
1395
+ resizer.setPointerCapture( e.pointerId );
1396
+
1397
+ startX = e.clientX;
1398
+ startY = e.clientY;
1399
+ startWidth = windowPanel.offsetWidth;
1400
+ startHeight = windowPanel.offsetHeight;
1401
+ startLeft = windowPanel.offsetLeft;
1402
+ startTop = windowPanel.offsetTop;
1403
+
1404
+ };
1405
+
1406
+ const onResizeMove = ( e ) => {
1407
+
1408
+ if ( ! isResizing ) return;
1409
+
1410
+ e.preventDefault();
1411
+
1412
+ const currentX = e.clientX;
1413
+ const currentY = e.clientY;
1414
+
1415
+ const deltaX = currentX - startX;
1416
+ const deltaY = currentY - startY;
1417
+
1418
+ const windowWidth = window.innerWidth;
1419
+ const windowHeight = window.innerHeight;
1420
+
1421
+ if ( direction === 'right' || direction === 'corner' ) {
1422
+
1423
+ const newWidth = startWidth + deltaX;
1424
+ const maxWidth = windowWidth - startLeft;
1425
+
1426
+ if ( newWidth >= minWidth && newWidth <= maxWidth ) {
1427
+
1428
+ windowPanel.style.width = `${ newWidth }px`;
1429
+
1430
+ }
1431
+
1432
+ }
1433
+
1434
+ if ( direction === 'bottom' || direction === 'corner' ) {
1435
+
1436
+ const newHeight = startHeight + deltaY;
1437
+ const maxHeight = windowHeight - startTop;
1438
+
1439
+ if ( newHeight >= minHeight && newHeight <= maxHeight ) {
1440
+
1441
+ windowPanel.style.height = `${ newHeight }px`;
1442
+
1443
+ }
1444
+
1445
+ }
1446
+
1447
+ if ( direction === 'left' ) {
1448
+
1449
+ const newWidth = startWidth - deltaX;
1450
+ const maxLeft = startLeft + startWidth - minWidth;
1451
+
1452
+ if ( newWidth >= minWidth ) {
1453
+
1454
+ const newLeft = startLeft + deltaX;
1455
+
1456
+ if ( newLeft >= 0 && newLeft <= maxLeft ) {
1457
+
1458
+ windowPanel.style.width = `${ newWidth }px`;
1459
+ windowPanel.style.left = `${ newLeft }px`;
1460
+
1461
+ }
1462
+
1463
+ }
1464
+
1465
+ }
1466
+
1467
+ if ( direction === 'top' ) {
1468
+
1469
+ const newHeight = startHeight - deltaY;
1470
+ const maxTop = startTop + startHeight - minHeight;
1471
+
1472
+ if ( newHeight >= minHeight ) {
1473
+
1474
+ const newTop = startTop + deltaY;
1475
+
1476
+ if ( newTop >= 0 && newTop <= maxTop ) {
1477
+
1478
+ windowPanel.style.height = `${ newHeight }px`;
1479
+ windowPanel.style.top = `${ newTop }px`;
1480
+
1481
+ }
1482
+
1483
+ }
1484
+
1485
+ }
1486
+
1487
+ };
1488
+
1489
+ const onResizeEnd = () => {
1490
+
1491
+ isResizing = false;
1492
+
1493
+ resizer.removeEventListener( 'pointermove', onResizeMove );
1494
+ resizer.removeEventListener( 'pointerup', onResizeEnd );
1495
+ resizer.removeEventListener( 'pointercancel', onResizeEnd );
1496
+
1497
+ // Save layout after resizing detached window
1498
+ this.saveLayout();
1499
+
1500
+ };
1501
+
1502
+ resizer.addEventListener( 'pointerdown', ( e ) => {
1503
+
1504
+ onResizeStart( e );
1505
+ resizer.addEventListener( 'pointermove', onResizeMove );
1506
+ resizer.addEventListener( 'pointerup', onResizeEnd );
1507
+ resizer.addEventListener( 'pointercancel', onResizeEnd );
1508
+
1509
+ } );
1510
+
1511
+ };
1512
+
1513
+ // Setup all resizers
1514
+ setupResizer( resizerTop, 'top' );
1515
+ setupResizer( resizerRight, 'right' );
1516
+ setupResizer( resizerBottom, 'bottom' );
1517
+ setupResizer( resizerLeft, 'left' );
1518
+ setupResizer( resizerCorner, 'corner' );
1519
+
1520
+ }
1521
+
1522
+ reattachTab( tab ) {
1523
+
1524
+ if ( ! tab.isDetached ) return;
1525
+
1526
+ if ( tab.detachedWindow ) {
1527
+
1528
+ const index = this.detachedWindows.indexOf( tab.detachedWindow );
1529
+
1530
+ if ( index > - 1 ) {
1531
+
1532
+ this.detachedWindows.splice( index, 1 );
1533
+
1534
+ }
1535
+
1536
+ if ( tab.detachedWindow.panel.parentNode ) {
1537
+
1538
+ tab.detachedWindow.panel.parentNode.removeChild( tab.detachedWindow.panel );
1539
+
1540
+ }
1541
+
1542
+ tab.detachedWindow = null;
1543
+
1544
+ }
1545
+
1546
+ tab.isDetached = false;
1547
+
1548
+ // Get all tabs and sort by their original index to determine the correct order
1549
+ const allTabs = Object.values( this.tabs );
1550
+ const allTabsSorted = allTabs
1551
+ .filter( t => t.originalIndex !== undefined && t.isVisible )
1552
+ .sort( ( a, b ) => a.originalIndex - b.originalIndex );
1553
+
1554
+ // Get currently attached tab buttons
1555
+ const currentButtons = Array.from( this.tabsContainer.children );
1556
+
1557
+ // Find the correct position for this tab
1558
+ let insertIndex = 0;
1559
+ for ( const t of allTabsSorted ) {
1560
+
1561
+ if ( t.id === tab.id ) {
1562
+
1563
+ break;
1564
+
1565
+ }
1566
+
1567
+ // Count only non-detached tabs that come before this one
1568
+ if ( ! t.isDetached ) {
1569
+
1570
+ insertIndex ++;
1571
+
1572
+ }
1573
+
1574
+ }
1575
+
1576
+ // Insert the button at the correct position
1577
+ if ( insertIndex >= currentButtons.length || currentButtons.length === 0 ) {
1578
+
1579
+ // If insert index is beyond current buttons, or no buttons exist, append at the end
1580
+ this.tabsContainer.appendChild( tab.button );
1581
+
1582
+ } else {
1583
+
1584
+ // Insert before the button at the insert index
1585
+ this.tabsContainer.insertBefore( tab.button, currentButtons[ insertIndex ] );
1586
+
1587
+ }
1588
+
1589
+ this.contentWrapper.appendChild( tab.content );
1590
+
1591
+ this.setActiveTab( tab.id );
1592
+
1593
+ // Update panel size after reattaching
1594
+ this.updatePanelSize();
1595
+
1596
+ this.saveLayout();
1597
+
1598
+ }
1599
+
1600
+ setActiveTab( id ) {
1601
+
1602
+ if ( this.activeTabId && this.tabs[ this.activeTabId ] && ! this.tabs[ this.activeTabId ].isDetached ) {
1603
+
1604
+ this.tabs[ this.activeTabId ].setActive( false );
1605
+
1606
+ }
1607
+
1608
+ this.activeTabId = id;
1609
+
1610
+ if ( this.tabs[ id ] ) {
1611
+
1612
+ this.tabs[ id ].setActive( true );
1613
+
1614
+ }
1615
+
1616
+ this.saveLayout();
1617
+
1618
+ }
1619
+
1620
+ togglePanel() {
1621
+
1622
+ this.panel.classList.toggle( 'visible' );
1623
+ this.toggleButton.classList.toggle( 'panel-open' );
1624
+ this.miniPanel.classList.toggle( 'panel-open' );
1625
+
1626
+ const isVisible = this.panel.classList.contains( 'visible' );
1627
+
1628
+ this.detachedWindows.forEach( detachedWindow => {
1629
+
1630
+ if ( isVisible ) {
1631
+
1632
+ detachedWindow.panel.style.opacity = '';
1633
+ detachedWindow.panel.style.visibility = '';
1634
+ detachedWindow.panel.style.pointerEvents = '';
1635
+
1636
+ } else {
1637
+
1638
+ detachedWindow.panel.style.opacity = '0';
1639
+ detachedWindow.panel.style.visibility = 'hidden';
1640
+ detachedWindow.panel.style.pointerEvents = 'none';
1641
+
1642
+ }
1643
+
1644
+ } );
1645
+
1646
+ this.dispatchEvent( { type: 'resize' } );
1647
+
1648
+ this.saveLayout();
1649
+
1650
+ }
1651
+
1652
+ togglePosition() {
1653
+
1654
+ const newPosition = this.position === 'bottom' ? 'right' : 'bottom';
1655
+ this.setPosition( newPosition );
1656
+
1657
+ }
1658
+
1659
+ setPosition( targetPosition ) {
1660
+
1661
+ if ( this.position === targetPosition ) return;
1662
+
1663
+ this.panel.style.transition = 'none';
1664
+
1665
+ // Check if panel is currently maximized
1666
+ const isMaximized = this.panel.classList.contains( 'maximized' );
1667
+
1668
+ if ( targetPosition === 'right' ) {
1669
+
1670
+ this.position = 'right';
1671
+ this.floatingBtn.classList.add( 'active' );
1672
+ this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><path d="M3 15h18"></path></svg>';
1673
+ this.floatingBtn.title = 'Switch to Bottom';
1674
+
1675
+ // Apply right position styles
1676
+ this.panel.classList.remove( 'position-bottom' );
1677
+ this.panel.classList.add( 'position-right' );
1678
+ this.toggleButton.classList.add( 'position-right' );
1679
+ this.miniPanel.classList.add( 'position-right' );
1680
+ this.panel.style.bottom = '';
1681
+ this.panel.style.top = '0';
1682
+ this.panel.style.right = '0';
1683
+ this.panel.style.left = '';
1684
+
1685
+ // Apply size based on maximized state
1686
+ if ( isMaximized ) {
1687
+
1688
+ this.panel.style.width = '100vw';
1689
+ this.panel.style.height = '100%';
1690
+
1691
+ } else {
1692
+
1693
+ this.panel.style.width = `${ this.lastWidthRight }px`;
1694
+ this.panel.style.height = '100%';
1695
+
1696
+ }
1697
+
1698
+ } else {
1699
+
1700
+ this.position = 'bottom';
1701
+ this.floatingBtn.classList.remove( 'active' );
1702
+ this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="15" y1="3" x2="15" y2="21"></line></svg>';
1703
+ this.floatingBtn.title = 'Switch to Right Side';
1704
+
1705
+ // Apply bottom position styles
1706
+ this.panel.classList.remove( 'position-right' );
1707
+ this.panel.classList.add( 'position-bottom' );
1708
+ this.toggleButton.classList.remove( 'position-right' );
1709
+ this.miniPanel.classList.remove( 'position-right' );
1710
+ this.panel.style.top = '';
1711
+ this.panel.style.right = '';
1712
+ this.panel.style.bottom = '0';
1713
+ this.panel.style.left = '0';
1714
+
1715
+ // Apply size based on maximized state
1716
+ if ( isMaximized ) {
1717
+
1718
+ this.panel.style.width = '100%';
1719
+ this.panel.style.height = '100vh';
1720
+
1721
+ } else {
1722
+
1723
+ this.panel.style.width = '100%';
1724
+ this.panel.style.height = `${ this.lastHeightBottom }px`;
1725
+
1726
+ }
1727
+
1728
+ }
1729
+
1730
+ // Re-enable transition after a brief delay
1731
+ setTimeout( () => {
1732
+
1733
+ this.panel.style.transition = '';
1734
+
1735
+ }, 50 );
1736
+
1737
+ // Update panel size based on visible tabs
1738
+ this.updatePanelSize();
1739
+
1740
+ // Save layout after position change
1741
+ this.saveLayout();
1742
+
1743
+ }
1744
+
1745
+ saveLayout() {
1746
+
1747
+ if ( this.isLoadingLayout ) return;
1748
+
1749
+ const layout = {
1750
+ position: this.position,
1751
+ lastHeightBottom: this.lastHeightBottom,
1752
+ lastWidthRight: this.lastWidthRight,
1753
+ activeTabId: this.activeTabId,
1754
+ detachedTabs: [],
1755
+ isVisible: this.panel.classList.contains( 'visible' )
1756
+ };
1757
+
1758
+ // Save detached windows state
1759
+ this.detachedWindows.forEach( detachedWindow => {
1760
+
1761
+ const tab = detachedWindow.tab;
1762
+ const panel = detachedWindow.panel;
1763
+
1764
+ // Get position values, ensuring they're valid numbers
1765
+ const left = parseFloat( panel.style.left ) || panel.offsetLeft || 0;
1766
+ const top = parseFloat( panel.style.top ) || panel.offsetTop || 0;
1767
+ const width = panel.offsetWidth;
1768
+ const height = panel.offsetHeight;
1769
+
1770
+ layout.detachedTabs.push( {
1771
+ tabId: tab.id,
1772
+ originalIndex: tab.originalIndex !== undefined ? tab.originalIndex : 0,
1773
+ left: left,
1774
+ top: top,
1775
+ width: width,
1776
+ height: height
1777
+ } );
1778
+
1779
+ } );
1780
+
1781
+ try {
1782
+
1783
+ setItem( 'layout', layout );
1784
+
1785
+ } catch ( e ) {
1786
+
1787
+ console.warn( 'Failed to save profiler layout:', e );
1788
+
1789
+ }
1790
+
1791
+ }
1792
+
1793
+ loadLayout() {
1794
+
1795
+ this.isLoadingLayout = true;
1796
+
1797
+ try {
1798
+
1799
+ const layout = getItem( 'layout' );
1800
+
1801
+ if ( Object.keys( layout ).length === 0 ) return;
1802
+
1803
+ // Constrain detached tabs positions to current screen bounds
1804
+ if ( layout.detachedTabs && layout.detachedTabs.length > 0 ) {
1805
+
1806
+ const windowWidth = window.innerWidth;
1807
+ const windowHeight = window.innerHeight;
1808
+
1809
+ layout.detachedTabs = layout.detachedTabs.map( detachedTabData => {
1810
+
1811
+ let { left, top, width, height } = detachedTabData;
1812
+
1813
+ // Ensure width and height are within bounds
1814
+ if ( width > windowWidth ) {
1815
+
1816
+ width = windowWidth - 100; // Leave some margin
1817
+
1818
+ }
1819
+
1820
+ if ( height > windowHeight ) {
1821
+
1822
+ height = windowHeight - 100; // Leave some margin
1823
+
1824
+ }
1825
+
1826
+ // Allow window to extend half its width/height outside the screen
1827
+ const halfWidth = width / 2;
1828
+ const halfHeight = height / 2;
1829
+
1830
+ // Constrain horizontal position (allow half width to extend beyond right edge)
1831
+ if ( left + width > windowWidth + halfWidth ) {
1832
+
1833
+ left = windowWidth + halfWidth - width;
1834
+
1835
+ }
1836
+
1837
+ // Constrain horizontal position (allow half width to extend beyond left edge)
1838
+ if ( left < - halfWidth ) {
1839
+
1840
+ left = - halfWidth;
1841
+
1842
+ }
1843
+
1844
+ // Constrain vertical position (allow half height to extend beyond bottom edge)
1845
+ if ( top + height > windowHeight + halfHeight ) {
1846
+
1847
+ top = windowHeight + halfHeight - height;
1848
+
1849
+ }
1850
+
1851
+ // Constrain vertical position (allow half height to extend beyond top edge)
1852
+ if ( top < - halfHeight ) {
1853
+
1854
+ top = - halfHeight;
1855
+
1856
+ }
1857
+
1858
+ return {
1859
+ ...detachedTabData,
1860
+ left,
1861
+ top,
1862
+ width,
1863
+ height
1864
+ };
1865
+
1866
+ } );
1867
+
1868
+ }
1869
+
1870
+ // Restore position and dimensions
1871
+ if ( layout.position ) {
1872
+
1873
+ this.position = layout.position;
1874
+
1875
+ }
1876
+
1877
+ if ( layout.lastHeightBottom ) {
1878
+
1879
+ this.lastHeightBottom = layout.lastHeightBottom;
1880
+
1881
+ }
1882
+
1883
+ if ( layout.lastWidthRight ) {
1884
+
1885
+ this.lastWidthRight = layout.lastWidthRight;
1886
+
1887
+ }
1888
+
1889
+ // Constrain saved dimensions to current screen bounds
1890
+ const windowWidth = window.innerWidth;
1891
+ const windowHeight = window.innerHeight;
1892
+
1893
+ if ( this.lastHeightBottom > windowHeight - 50 ) {
1894
+
1895
+ this.lastHeightBottom = windowHeight - 50;
1896
+
1897
+ }
1898
+
1899
+ if ( this.lastWidthRight > windowWidth - 50 ) {
1900
+
1901
+ this.lastWidthRight = windowWidth - 50;
1902
+
1903
+ }
1904
+
1905
+ // Apply the saved position after shell is set up
1906
+ if ( this.position === 'right' ) {
1907
+
1908
+ this.floatingBtn.classList.add( 'active' );
1909
+ this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><path d="M3 15h18"></path></svg>';
1910
+ this.floatingBtn.title = 'Switch to Bottom';
1911
+
1912
+ this.panel.classList.remove( 'position-bottom' );
1913
+ this.panel.classList.add( 'position-right' );
1914
+ this.toggleButton.classList.add( 'position-right' );
1915
+ this.miniPanel.classList.add( 'position-right' );
1916
+ this.panel.style.bottom = '';
1917
+ this.panel.style.top = '0';
1918
+ this.panel.style.right = '0';
1919
+ this.panel.style.left = '';
1920
+ this.panel.style.width = `${ this.lastWidthRight }px`;
1921
+ this.panel.style.height = '100%';
1922
+
1923
+ } else {
1924
+
1925
+ this.panel.style.height = `${ this.lastHeightBottom }px`;
1926
+
1927
+ }
1928
+
1929
+ if ( layout.isVisible ) {
1930
+
1931
+ this.panel.classList.add( 'visible' );
1932
+ this.toggleButton.classList.add( 'panel-open' );
1933
+
1934
+ }
1935
+
1936
+ if ( layout.activeTabId ) {
1937
+
1938
+ this.setActiveTab( layout.activeTabId );
1939
+
1940
+ }
1941
+
1942
+ if ( layout.detachedTabs && layout.detachedTabs.length > 0 ) {
1943
+
1944
+ this.pendingDetachedTabs = layout.detachedTabs;
1945
+ this.restoreDetachedTabs();
1946
+
1947
+ }
1948
+
1949
+ // Update panel size after loading layout
1950
+ this.updatePanelSize();
1951
+
1952
+ // Ensure initial open state applies to mini panel as well
1953
+ if ( this.panel.classList.contains( 'visible' ) ) {
1954
+
1955
+ this.miniPanel.classList.add( 'panel-open' );
1956
+
1957
+ }
1958
+
1959
+ } catch ( e ) {
1960
+
1961
+ console.warn( 'Failed to load profiler layout:', e );
1962
+
1963
+ } finally {
1964
+
1965
+ this.isLoadingLayout = false;
1966
+
1967
+ }
1968
+
1969
+ }
1970
+
1971
+ restoreDetachedTabs() {
1972
+
1973
+ if ( ! this.pendingDetachedTabs || this.pendingDetachedTabs.length === 0 ) return;
1974
+
1975
+ this.pendingDetachedTabs.forEach( detachedTabData => {
1976
+
1977
+ const tab = this.tabs[ detachedTabData.tabId ];
1978
+
1979
+ if ( ! tab || tab.isDetached ) return;
1980
+
1981
+ // Restore originalIndex if saved
1982
+ if ( detachedTabData.originalIndex !== undefined ) {
1983
+
1984
+ tab.originalIndex = detachedTabData.originalIndex;
1985
+
1986
+ }
1987
+
1988
+ if ( tab.button.parentNode ) {
1989
+
1990
+ tab.button.parentNode.removeChild( tab.button );
1991
+
1992
+ }
1993
+
1994
+ if ( tab.content.parentNode ) {
1995
+
1996
+ tab.content.parentNode.removeChild( tab.content );
1997
+
1998
+ }
1999
+
2000
+ const detachedWindow = this.createDetachedWindow( tab, 0, 0 );
2001
+
2002
+ detachedWindow.panel.style.left = `${ detachedTabData.left }px`;
2003
+ detachedWindow.panel.style.top = `${ detachedTabData.top }px`;
2004
+ detachedWindow.panel.style.width = `${ detachedTabData.width }px`;
2005
+ detachedWindow.panel.style.height = `${ detachedTabData.height }px`;
2006
+
2007
+ // Constrain window to bounds after restoring position and size
2008
+ this.constrainWindowToBounds( detachedWindow.panel );
2009
+
2010
+ this.detachedWindows.push( detachedWindow );
2011
+
2012
+ tab.isDetached = true;
2013
+ tab.detachedWindow = detachedWindow;
2014
+
2015
+ } );
2016
+
2017
+ this.pendingDetachedTabs = null;
2018
+
2019
+ // Update maxZIndex to be higher than all existing windows
2020
+ this.detachedWindows.forEach( detachedWindow => {
2021
+
2022
+ const currentZIndex = parseInt( getComputedStyle( detachedWindow.panel ).zIndex ) || 0;
2023
+ if ( currentZIndex > this.maxZIndex ) {
2024
+
2025
+ this.maxZIndex = currentZIndex;
2026
+
2027
+ }
2028
+
2029
+ } );
2030
+
2031
+ const needsNewActiveTab = ! this.activeTabId ||
2032
+ ! this.tabs[ this.activeTabId ] ||
2033
+ this.tabs[ this.activeTabId ].isDetached ||
2034
+ ! this.tabs[ this.activeTabId ].isVisible;
2035
+
2036
+ if ( needsNewActiveTab ) {
2037
+
2038
+ const tabIds = Object.keys( this.tabs );
2039
+ const availableTabs = tabIds.filter( id =>
2040
+ ! this.tabs[ id ].isDetached &&
2041
+ this.tabs[ id ].isVisible
2042
+ );
2043
+
2044
+ if ( availableTabs.length > 0 ) {
2045
+
2046
+ const buttons = Array.from( this.tabsContainer.children );
2047
+ const orderedTabIds = buttons.map( btn => {
2048
+
2049
+ return Object.keys( this.tabs ).find( id => this.tabs[ id ].button === btn );
2050
+
2051
+ } ).filter( id =>
2052
+ id !== undefined &&
2053
+ ! this.tabs[ id ].isDetached &&
2054
+ this.tabs[ id ].isVisible
2055
+ );
2056
+
2057
+ this.setActiveTab( orderedTabIds[ 0 ] || availableTabs[ 0 ] );
2058
+
2059
+ } else {
2060
+
2061
+ this.activeTabId = null;
2062
+
2063
+ }
2064
+
2065
+ }
2066
+
2067
+ // Update panel size after restoring detached tabs
2068
+ this.updatePanelSize();
167
2069
 
168
2070
  }
169
2071