visualifyjs 2.5.3 → 3.0.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 (252) hide show
  1. package/.claude/mem/TIMELINE.md +36 -0
  2. package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-external-script-solution.md +24 -0
  3. package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-session-summary.md +43 -0
  4. package/.claude/mem/notes/2026-02-11-cli-fix-editor-command-alias.md +26 -0
  5. package/.claude/mem/notes/2026-02-11-phase-3-developer-experience-completed.md +51 -0
  6. package/.claude/mem/notes/2026-02-11-phase-4-web-workers-implementation-complete.md +59 -0
  7. package/.claude/mem/notes/2026-02-11-visualify-phase-2-3d-visualization-complete.md +50 -0
  8. package/.claude/mem/notes/2026-02-11-visualify-phase-2-committed-ready-for-phase-3.md +33 -0
  9. package/.claude/mem/notes/2026-02-11-visualify-phase-3-complete-developer-experience.md +52 -0
  10. package/.claude/mem/notes/2026-02-11-visualify-repository-cleanup-complete.md +28 -0
  11. package/.claude/mem/notes/2026-02-18-codebase-cleanup-docsify-plugin-documentation.md +37 -0
  12. package/.claude/mem/notes/2026-02-19-css-grid-layout-fix-displaycontents-on-vcontroller.md +18 -0
  13. package/.claude/mem/notes/2026-02-19-docsify-plugin-fixes-latex-and-visualify-code-bloc.md +26 -0
  14. package/.claude/mem/notes/2026-02-19-page-mode-docs-update-decisions.md +23 -0
  15. package/.claude/mem/notes/2026-02-19-react-context-infinite-re-render-loop-fix-pattern.md +31 -0
  16. package/.claude/mem/notes/2026-02-19-version-300-bump-and-build-fixes.md +32 -0
  17. package/.claude/mem/notes/2026-02-19-visualify-build-deployment-architecture-bug-fixes.md +25 -0
  18. package/.claude/mem/notes/2026-02-19-visualify-dist-iife-self-contained-build-config.md +30 -0
  19. package/.claude/mem/notes/2026-02-19-visualify-infinite-loop-i18n-fixes.md +31 -0
  20. package/.claude/mem/notes/2026-02-19-visualify-v3-bundle-splitting-docs-restructuring.md +32 -0
  21. package/.claude/mem/notes/2026-02-20-bundle-externalization-final-architecture.md +29 -0
  22. package/.claude/mem/notes/2026-02-20-chromium-page-fix-unstable-keys-and-double-event-b.md +27 -0
  23. package/.claude/mem/notes/2026-02-20-console-cleanup-bundle-optimization-commit.md +20 -0
  24. package/.claude/mem/notes/2026-02-20-dotbio-dot-plot-fix-useeffect-dependency.md +21 -0
  25. package/.claude/mem/notes/2026-02-20-public-folder-cleanup-and-readme-rewrite.md +25 -0
  26. package/.claude/mem/notes/2026-02-20-v300-release-and-beta-channel-strategy.md +29 -0
  27. package/.claude/mem/notes/2026-02-20-visium-background-image-unknown-legend-fix.md +19 -0
  28. package/.claude/mem/notes/2026-02-20-visualify-cdn-loader-bundle-externalization.md +34 -0
  29. package/.claude/mem/sessions/session-2026-02-20-031524.md +54 -0
  30. package/.claude/settings.local.json +21 -0
  31. package/.github/workflows/static.yml.bak +51 -51
  32. package/.sisyphus/boulder.json +65 -0
  33. package/.sisyphus/plans/phase-4-advanced-optimizations.md +217 -0
  34. package/LICENSE +674 -674
  35. package/README.md +94 -59
  36. package/config-overrides.js +31 -31
  37. package/dist/stats.html +4949 -0
  38. package/dist/visualify-3d.esm.js +1 -0
  39. package/dist/visualify-3d.js +1 -0
  40. package/dist/visualify-core.esm.js +1 -0
  41. package/dist/visualify-core.js +1 -0
  42. package/dist/visualify-docs.esm.js +1 -0
  43. package/dist/visualify-docs.js +1 -0
  44. package/dist/visualify-loader.js +1 -0
  45. package/dist/visualify-pages.esm.js +1 -0
  46. package/dist/visualify-pages.js +1 -0
  47. package/dist/visualify-portal.esm.js +1 -0
  48. package/dist/visualify-portal.js +1 -0
  49. package/dist/visualify-shared.js +26571 -0
  50. package/dist/visualify.js +1 -188
  51. package/docs/CHANGELOG.md +148 -0
  52. package/docs/cli/commands.md +513 -0
  53. package/docs/configuration/visualify-json.md +474 -0
  54. package/docs/docs/3d-visualization.md +374 -0
  55. package/docs/docs/CLI.md +303 -34
  56. package/docs/docs/README.md +65 -65
  57. package/docs/docs/Rechart/bar.md +190 -190
  58. package/docs/docs/Rechart/funnel.md +241 -193
  59. package/docs/docs/Rechart/line.md +355 -355
  60. package/docs/docs/Rechart/pie.md +225 -225
  61. package/docs/docs/Rechart/radar.md +253 -253
  62. package/docs/docs/Rechart/scatter.md +298 -0
  63. package/docs/docs/_404.md +51 -51
  64. package/docs/docs/_coverpage.md +11 -11
  65. package/docs/docs/_sidebar.md +54 -43
  66. package/docs/docs/components/dotBio.md +87 -34
  67. package/docs/docs/components/echart.md +171 -82
  68. package/docs/docs/components/html.md +61 -34
  69. package/docs/docs/components/macaron.md +156 -145
  70. package/docs/docs/components/markdown.md +42 -0
  71. package/docs/docs/components/more.md +183 -142
  72. package/docs/docs/components/plotly.md +132 -62
  73. package/docs/docs/components/scatterL.md +171 -70
  74. package/docs/docs/components/visium.md +112 -57
  75. package/docs/docs/configuration.md +121 -123
  76. package/docs/docs/deploy.md +31 -31
  77. package/docs/docs/docsify-plugin.md +655 -0
  78. package/docs/docs/hmr.md +165 -0
  79. package/docs/docs/i18n.md +332 -0
  80. package/docs/docs/log.md +30 -1
  81. package/docs/docs/more-pages.md +23 -23
  82. package/docs/docs/quickstart.md +148 -119
  83. package/docs/docs/rechart-attributes.md +74 -74
  84. package/docs/docs/rechart-basic-usage.md +160 -162
  85. package/docs/docs/theme.md +5 -5
  86. package/docs/docs/typescript.md +306 -0
  87. package/docs/docs/visual-editor.md +359 -0
  88. package/docs/index.html +85 -71
  89. package/docs/manifest.json +23 -23
  90. package/docs/migration/v3-migration.md +392 -0
  91. package/docs/static/css/fluff-stuff.css +169 -169
  92. package/docs/static/css/font-awesome.min.css +4 -4
  93. package/docs/static/css/visualify.css +6 -25
  94. package/docs/static/js/3d-viz-examples.js +181 -0
  95. package/docs/static/js/configuration.js +630 -448
  96. package/docs/static/js/visualify.js +1 -188
  97. package/package.json +106 -84
  98. package/rollup.config.mjs +766 -76
  99. package/src/_css/404.css +115 -115
  100. package/src/_css/App.css +37 -37
  101. package/src/_css/autoSuggestion.css +26 -26
  102. package/src/_css/circular-progress.css +32 -32
  103. package/src/_css/index.css +36 -36
  104. package/src/_css/modern.css +350 -25
  105. package/src/_media/corner.svg +8 -8
  106. package/src/_media/download.svg +3 -3
  107. package/src/_media/logo.svg +14 -14
  108. package/src/_test/App.test.js +15 -15
  109. package/src/_utils/reportWebVitals.js +13 -13
  110. package/src/a11y/README.md +177 -0
  111. package/src/a11y/aria-labels.js +339 -0
  112. package/src/a11y/color-contrast.js +535 -0
  113. package/src/a11y/index.js +197 -0
  114. package/src/a11y/keyboard-nav.js +523 -0
  115. package/src/a11y/styles.css +165 -0
  116. package/src/cli/commands/dev.js +214 -0
  117. package/src/cli/commands/docs.js +521 -0
  118. package/src/cli/commands/edit.js +379 -0
  119. package/src/cli/commands/init.js +213 -0
  120. package/src/cli/commands/portal.js +236 -0
  121. package/src/cli/dev-server.js +530 -0
  122. package/src/cli/hmr.js +456 -0
  123. package/src/cli/index.js +180 -0
  124. package/src/cli/utils/config.js +207 -0
  125. package/src/cli/utils/logger.js +241 -0
  126. package/src/config/defaults.ts +122 -0
  127. package/src/config/index.ts +72 -0
  128. package/src/config/loader.ts +478 -0
  129. package/src/config/schema.ts +227 -0
  130. package/src/config/validator.ts +337 -0
  131. package/src/core/appContext.js +34 -27
  132. package/src/core/components/Bar.js +383 -0
  133. package/src/core/components/Bar3D.js +473 -0
  134. package/src/core/components/LargeDatasetChart.js +296 -0
  135. package/src/core/components/Line3D.js +310 -0
  136. package/src/core/components/Scatter.js +392 -188
  137. package/src/core/components/Scatter3D.js +455 -0
  138. package/src/core/components/ScatterBio.js +601 -572
  139. package/src/core/components/Surface3D.js +326 -0
  140. package/src/core/components/ThreeCustom.js +648 -0
  141. package/src/core/components/ThreeScene.js +459 -0
  142. package/src/core/components/VisiumPlot.js +191 -165
  143. package/src/core/components/browser.js +42 -42
  144. package/src/core/components/dotplot.js +413 -413
  145. package/src/core/components/html.js +29 -29
  146. package/src/core/components/list.js +178 -178
  147. package/src/core/components/macaron.js +206 -201
  148. package/src/core/components/markdown.js +56 -56
  149. package/src/core/components/parser.scatterBio.js +582 -579
  150. package/src/core/components/ratio.js +82 -80
  151. package/src/core/components/scatterL.js +206 -173
  152. package/src/core/components/searchbar.js +156 -131
  153. package/src/core/components/selection.js +310 -193
  154. package/src/core/components/timeline.js +236 -281
  155. package/src/core/components/visium.js +114 -97
  156. package/src/core/data-processor.js +413 -0
  157. package/src/core/fetch/condfetch.js +82 -82
  158. package/src/core/fetch/fetch.js +92 -92
  159. package/src/core/fetch/json.js +29 -29
  160. package/src/core/fetch/vfetch.js +42 -42
  161. package/src/core/hmr-client.js +724 -0
  162. package/src/core/liveEditor.js +44 -44
  163. package/src/core/modules/codeEditorWithPreview.js +104 -104
  164. package/src/core/modules/echarts/common.js +20 -20
  165. package/src/core/modules/echarts/gl.js +228 -0
  166. package/src/core/modules/echarts/presetHandler.js +41 -41
  167. package/src/core/modules/echarts/presets/esodev.chromium.js +172 -172
  168. package/src/core/modules/echarts/presets/esodev.codex.js +130 -130
  169. package/src/core/modules/echarts/presets/esodev.visium.js +123 -123
  170. package/src/core/modules/echarts/presets/mmtrbc.js +186 -186
  171. package/src/core/modules/echarts.js +70 -71
  172. package/src/core/modules/echartsUtils.js +43 -43
  173. package/src/core/modules/echartswitcher.js +227 -152
  174. package/src/core/modules/replotly/presetHandler.js +24 -24
  175. package/src/core/modules/replotly/presets/minimum.js +18 -18
  176. package/src/core/modules/replotly/presets/mmtrbc.dot.js +114 -114
  177. package/src/core/modules/replotly/presets/mmtrbc.violin.js +100 -100
  178. package/src/core/modules/replotly.js +74 -71
  179. package/src/core/modules/threejs/Camera.js +373 -0
  180. package/src/core/modules/threejs/Lighting.js +459 -0
  181. package/src/core/modules/threejs/Renderer.js +364 -0
  182. package/src/core/modules/threejs/Scene.js +266 -0
  183. package/src/core/modules/threejs/index.js +155 -0
  184. package/src/core/pages/404.js +50 -50
  185. package/src/core/pages/error.js +27 -27
  186. package/src/core/pages/jsonPage.js +62 -62
  187. package/src/core/pages/loading.js +44 -44
  188. package/src/core/parser/echart.data.js +204 -183
  189. package/src/core/parser/echart.features.js +125 -125
  190. package/src/core/parser/echart.general.js +147 -143
  191. package/src/core/parser/echart.hilbert.js +57 -57
  192. package/src/core/parser/echart.parser.js +210 -210
  193. package/src/core/parser/echart.series.js +67 -67
  194. package/src/core/parser/echart.types.js +76 -76
  195. package/src/core/parser/plotly.config.js +10 -10
  196. package/src/core/parser/plotly.data.js +132 -132
  197. package/src/core/parser/plotly.layout.js +9 -9
  198. package/src/core/parser/plotly.violin.js +18 -18
  199. package/src/core/recharts.js +361 -62
  200. package/src/core/router/alias.js +49 -49
  201. package/src/core/router/jsonRouter.js +31 -31
  202. package/src/core/themes/modern.js +32 -32
  203. package/src/core/themes/themeSelector.js +33 -33
  204. package/src/core/visualify.js +213 -47
  205. package/src/core/widgets/circularProgress.js +23 -23
  206. package/src/core/widgets/controller.js +116 -83
  207. package/src/core/widgets/errorBoundary.js +36 -36
  208. package/src/core/widgets/footer.js +185 -177
  209. package/src/core/widgets/header.js +238 -234
  210. package/src/core/widgets/layout/Grid.js +31 -31
  211. package/src/core/widgets/layout.js +36 -36
  212. package/src/core/widgets/mapping.js +56 -42
  213. package/src/core/workers/data-worker.js +349 -0
  214. package/src/core/workers/worker-pool.js +396 -0
  215. package/src/docsify/bundle.js +215 -0
  216. package/src/docsify/markdown.js +271 -0
  217. package/src/docsify/plugin.js +268 -0
  218. package/src/editor/README.md +172 -0
  219. package/src/editor/components/ChartBuilder.jsx +341 -0
  220. package/src/editor/components/ChartTypeSidebar.jsx +91 -0
  221. package/src/editor/components/Editor.jsx +367 -0
  222. package/src/editor/components/Preview.jsx +446 -0
  223. package/src/editor/components/PropertyPanel.jsx +468 -0
  224. package/src/editor/components/StatusBar.jsx +85 -0
  225. package/src/editor/context/EditorContext.js +248 -0
  226. package/src/editor/hooks/useDebounce.js +32 -0
  227. package/src/editor/index.js +315 -0
  228. package/src/editor/styles/editor.css +637 -0
  229. package/src/editor/utils/chartValidator.js +263 -0
  230. package/src/entries/charts3d.js +70 -0
  231. package/src/entries/core.js +78 -0
  232. package/src/entries/docs.js +154 -0
  233. package/src/entries/pages.js +93 -0
  234. package/src/entries/portal.js +204 -0
  235. package/src/entries/shared.js +50 -0
  236. package/src/i18n/formatters.js +455 -0
  237. package/src/i18n/index.js +169 -0
  238. package/src/i18n/locales/ar.json +137 -0
  239. package/src/i18n/locales/de.json +137 -0
  240. package/src/i18n/locales/en.json +137 -0
  241. package/src/i18n/locales/es.json +137 -0
  242. package/src/i18n/locales/he.json +137 -0
  243. package/src/i18n/locales/zh.json +137 -0
  244. package/src/i18n/rtl.css +183 -0
  245. package/src/index.js +82 -62
  246. package/src/loader.js +103 -0
  247. package/src/setupTests.js +5 -5
  248. package/tsconfig.json +51 -0
  249. package/types/charts.d.ts +569 -0
  250. package/types/components.d.ts +441 -0
  251. package/types/config.d.ts +199 -0
  252. package/types/index.d.ts +353 -0
@@ -0,0 +1,648 @@
1
+ /**
2
+ * ThreeCustom.js - Custom geometry support for Three.js in Visualify.js
3
+ * Supports loading custom geometries (OBJ, GLTF, etc.) with material configuration and animation
4
+ */
5
+
6
+ import React, {
7
+ useRef,
8
+ useMemo,
9
+ useEffect,
10
+ useState,
11
+ useCallback,
12
+ forwardRef,
13
+ useImperativeHandle,
14
+ } from 'react';
15
+ import { Canvas, useFrame, useThree } from '@react-three/fiber';
16
+ import {
17
+ OrbitControls,
18
+ TrackballControls,
19
+ FlyControls,
20
+ useGLTF,
21
+ Center,
22
+ Bounds,
23
+ } from '@react-three/drei';
24
+ import * as THREE from 'three';
25
+
26
+ // Import OBJLoader - using .js extension for proper module resolution
27
+ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
28
+
29
+ /**
30
+ * Error boundary for WebGL failures
31
+ */
32
+ class ErrorBoundary extends React.Component {
33
+ constructor(props) {
34
+ super(props)
35
+ this.state = { hasError: false, error: null }
36
+ }
37
+
38
+ static getDerivedStateFromError(error) {
39
+ return { hasError: true, error }
40
+ }
41
+
42
+ componentDidCatch(error, errorInfo) {
43
+ console.error('ThreeCustom error:', error, errorInfo)
44
+ }
45
+
46
+ render() {
47
+ if (this.state.hasError) {
48
+ return (
49
+ <div style={{ padding: '20px', color: 'red', textAlign: 'center' }}>
50
+ <h3>WebGL Error</h3>
51
+ <p>Failed to initialize 3D scene. Please check your browser supports WebGL.</p>
52
+ <p style={{ fontSize: '12px', color: '#666' }}>{this.state.error?.message}</p>
53
+ </div>
54
+ )
55
+ }
56
+ return this.props.children
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Loading fallback component
62
+ */
63
+ function LoadingFallback() {
64
+ return (
65
+ <mesh>
66
+ <boxGeometry args={[1, 1, 1]} />
67
+ <meshBasicMaterial color="#cccccc" wireframe />
68
+ </mesh>
69
+ )
70
+ }
71
+
72
+ /**
73
+ * Light component
74
+ */
75
+ function Light({ config }) {
76
+ const lightRef = useRef()
77
+
78
+ const light = useMemo(() => {
79
+ switch (config.type) {
80
+ case 'ambient':
81
+ return new THREE.AmbientLight(config.color, config.intensity)
82
+ case 'directional':
83
+ const dirLight = new THREE.DirectionalLight(config.color, config.intensity)
84
+ dirLight.position.set(...(config.position || [10, 10, 10]))
85
+ dirLight.castShadow = config.castShadow || false
86
+ return dirLight
87
+ case 'point':
88
+ const pointLight = new THREE.PointLight(
89
+ config.color,
90
+ config.intensity,
91
+ config.distance || 0,
92
+ config.decay || 2
93
+ )
94
+ pointLight.position.set(...(config.position || [0, 0, 0]))
95
+ pointLight.castShadow = config.castShadow || false
96
+ return pointLight
97
+ case 'spot':
98
+ const spotLight = new THREE.SpotLight(
99
+ config.color,
100
+ config.intensity,
101
+ config.distance || 0,
102
+ config.angle || Math.PI / 6,
103
+ config.penumbra || 0,
104
+ config.decay || 2
105
+ )
106
+ spotLight.position.set(...(config.position || [0, 10, 0]))
107
+ spotLight.castShadow = config.castShadow || false
108
+ return spotLight
109
+ case 'hemisphere':
110
+ const hemiLight = new THREE.HemisphereLight(
111
+ config.skyColor || '#ffffff',
112
+ config.groundColor || '#444444',
113
+ config.intensity || 1
114
+ )
115
+ hemiLight.position.set(...(config.position || [0, 10, 0]))
116
+ return hemiLight
117
+ default:
118
+ return new THREE.AmbientLight('#ffffff', 0.5)
119
+ }
120
+ }, [config])
121
+
122
+ return <primitive ref={lightRef} object={light} />
123
+ }
124
+
125
+ /**
126
+ * Controls component
127
+ */
128
+ function Controls({ type, config = {} }) {
129
+ const { camera, gl } = useThree()
130
+
131
+ const controlsProps = useMemo(() => ({
132
+ enableDamping: config.enableDamping !== false,
133
+ dampingFactor: config.dampingFactor || 0.05,
134
+ enableZoom: config.enableZoom !== false,
135
+ enablePan: config.enablePan !== false,
136
+ enableRotate: config.enableRotate !== false,
137
+ autoRotate: config.autoRotate || false,
138
+ autoRotateSpeed: config.autoRotateSpeed || 1,
139
+ minDistance: config.minDistance || 0,
140
+ maxDistance: config.maxDistance || Infinity,
141
+ ...config,
142
+ }), [config])
143
+
144
+ switch (type) {
145
+ case 'orbit':
146
+ return <OrbitControls {...controlsProps} args={[camera, gl.domElement]} />
147
+ case 'trackball':
148
+ return <TrackballControls {...controlsProps} args={[camera, gl.domElement]} />
149
+ case 'fly':
150
+ return <FlyControls {...controlsProps} args={[camera, gl.domElement]} />
151
+ default:
152
+ return <OrbitControls {...controlsProps} args={[camera, gl.domElement]} />
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Material component that creates materials from config
158
+ */
159
+ function createMaterial(materialConfig) {
160
+ if (!materialConfig) return new THREE.MeshStandardMaterial({ color: '#888888' })
161
+
162
+ const mat = materialConfig
163
+ switch (mat.type) {
164
+ case 'basic':
165
+ return new THREE.MeshBasicMaterial({
166
+ color: mat.color || '#888888',
167
+ transparent: mat.transparent || false,
168
+ opacity: mat.opacity !== undefined ? mat.opacity : 1,
169
+ wireframe: mat.wireframe || false,
170
+ })
171
+ case 'standard':
172
+ const standardMat = new THREE.MeshStandardMaterial({
173
+ color: mat.color || '#888888',
174
+ roughness: mat.roughness !== undefined ? mat.roughness : 0.5,
175
+ metalness: mat.metalness !== undefined ? mat.metalness : 0.5,
176
+ transparent: mat.transparent || false,
177
+ opacity: mat.opacity !== undefined ? mat.opacity : 1,
178
+ wireframe: mat.wireframe || false,
179
+ })
180
+ if (mat.map) {
181
+ // Texture loading would be handled here
182
+ }
183
+ return standardMat
184
+ case 'phong':
185
+ return new THREE.MeshPhongMaterial({
186
+ color: mat.color || '#888888',
187
+ shininess: mat.shininess || 30,
188
+ transparent: mat.transparent || false,
189
+ opacity: mat.opacity !== undefined ? mat.opacity : 1,
190
+ })
191
+ case 'lambert':
192
+ return new THREE.MeshLambertMaterial({
193
+ color: mat.color || '#888888',
194
+ transparent: mat.transparent || false,
195
+ opacity: mat.opacity !== undefined ? mat.opacity : 1,
196
+ })
197
+ case 'physical':
198
+ return new THREE.MeshPhysicalMaterial({
199
+ color: mat.color || '#888888',
200
+ roughness: mat.roughness !== undefined ? mat.roughness : 0.5,
201
+ metalness: mat.metalness !== undefined ? mat.metalness : 0.5,
202
+ clearcoat: mat.clearcoat || 0,
203
+ clearcoatRoughness: mat.clearcoatRoughness || 0,
204
+ transparent: mat.transparent || false,
205
+ opacity: mat.opacity !== undefined ? mat.opacity : 1,
206
+ })
207
+ default:
208
+ return new THREE.MeshStandardMaterial({ color: mat.color || '#888888' })
209
+ }
210
+ }
211
+
212
+ /**
213
+ * GLTF Model component
214
+ */
215
+ function GLTFModel({ url, config, onLoad, onError }) {
216
+ const groupRef = useRef()
217
+ const [modelError, setModelError] = useState(null)
218
+
219
+ const gltf = useGLTF(url, true)
220
+
221
+ useEffect(() => {
222
+ if (!gltf) {
223
+ const err = new Error('Failed to load GLTF model')
224
+ setModelError(err)
225
+ if (onError) onError(err)
226
+ }
227
+ }, [gltf, onError])
228
+
229
+ useEffect(() => {
230
+ if (gltf && onLoad) {
231
+ onLoad(gltf)
232
+ }
233
+ }, [gltf, onLoad])
234
+
235
+ // Apply animations
236
+ useFrame((state) => {
237
+ if (!groupRef.current || !config?.animation) return
238
+
239
+ const anim = config.animation
240
+ if (anim.autoRotate) {
241
+ const speed = anim.speed || 0.01
242
+ groupRef.current.rotation.y += speed
243
+ }
244
+ if (anim.rotation) {
245
+ groupRef.current.rotation.x += anim.rotation[0] || 0
246
+ groupRef.current.rotation.y += anim.rotation[1] || 0
247
+ groupRef.current.rotation.z += anim.rotation[2] || 0
248
+ }
249
+ })
250
+
251
+ // Apply material override
252
+ useEffect(() => {
253
+ if (!groupRef.current || !config?.material) return
254
+
255
+ const material = createMaterial(config.material)
256
+ groupRef.current.traverse((child) => {
257
+ if (child.isMesh) {
258
+ child.material = material
259
+ }
260
+ })
261
+
262
+ return () => {
263
+ material.dispose()
264
+ }
265
+ }, [config?.material])
266
+
267
+ if (modelError) {
268
+ return (
269
+ <mesh>
270
+ <boxGeometry args={[1, 1, 1]} />
271
+ <meshBasicMaterial color="#ff0000" wireframe />
272
+ </mesh>
273
+ )
274
+ }
275
+
276
+ if (!gltf) {
277
+ return <LoadingFallback />
278
+ }
279
+
280
+ return (
281
+ <group
282
+ ref={groupRef}
283
+ position={config?.position || [0, 0, 0]}
284
+ rotation={config?.rotation || [0, 0, 0]}
285
+ scale={config?.scale || [1, 1, 1]}
286
+ >
287
+ <primitive object={gltf.scene} />
288
+ </group>
289
+ )
290
+ }
291
+
292
+ /**
293
+ * OBJ Model component
294
+ */
295
+ function OBJModel({ url, config, onLoad, onError }) {
296
+ const groupRef = useRef()
297
+ const [model, setModel] = useState(null)
298
+ const [modelError, setModelError] = useState(null)
299
+
300
+ useEffect(() => {
301
+ const loader = new OBJLoader()
302
+ loader.load(
303
+ url,
304
+ (loadedModel) => {
305
+ setModel(loadedModel)
306
+ if (onLoad) onLoad(loadedModel)
307
+ },
308
+ undefined,
309
+ (err) => {
310
+ console.error('Error loading OBJ:', err)
311
+ setModelError(err)
312
+ if (onError) onError(err)
313
+ }
314
+ )
315
+ }, [url, onLoad, onError])
316
+
317
+ // Apply animations
318
+ useFrame(() => {
319
+ if (!groupRef.current || !config?.animation) return
320
+
321
+ const anim = config.animation
322
+ if (anim.autoRotate) {
323
+ const speed = anim.speed || 0.01
324
+ groupRef.current.rotation.y += speed
325
+ }
326
+ if (anim.rotation) {
327
+ groupRef.current.rotation.x += anim.rotation[0] || 0
328
+ groupRef.current.rotation.y += anim.rotation[1] || 0
329
+ groupRef.current.rotation.z += anim.rotation[2] || 0
330
+ }
331
+ })
332
+
333
+ // Apply material override
334
+ useEffect(() => {
335
+ if (!groupRef.current || !config?.material) return
336
+
337
+ const material = createMaterial(config.material)
338
+ groupRef.current.traverse((child) => {
339
+ if (child.isMesh) {
340
+ child.material = material
341
+ }
342
+ })
343
+
344
+ return () => {
345
+ material.dispose()
346
+ }
347
+ }, [config?.material])
348
+
349
+ if (modelError) {
350
+ return (
351
+ <mesh>
352
+ <boxGeometry args={[1, 1, 1]} />
353
+ <meshBasicMaterial color="#ff0000" wireframe />
354
+ </mesh>
355
+ )
356
+ }
357
+
358
+ if (!model) {
359
+ return <LoadingFallback />
360
+ }
361
+
362
+ return (
363
+ <group
364
+ ref={groupRef}
365
+ position={config?.position || [0, 0, 0]}
366
+ rotation={config?.rotation || [0, 0, 0]}
367
+ scale={config?.scale || [1, 1, 1]}
368
+ >
369
+ <primitive object={model} />
370
+ </group>
371
+ )
372
+ }
373
+
374
+ /**
375
+ * Custom geometry from config
376
+ */
377
+ function CustomGeometry({ config }) {
378
+ const meshRef = useRef()
379
+
380
+ const geometry = useMemo(() => {
381
+ if (!config?.geometry) return new THREE.BoxGeometry(1, 1, 1)
382
+
383
+ const geo = config.geometry
384
+ switch (geo.type) {
385
+ case 'box':
386
+ return new THREE.BoxGeometry(geo.width || 1, geo.height || 1, geo.depth || 1)
387
+ case 'sphere':
388
+ return new THREE.SphereGeometry(geo.radius || 1, geo.widthSegments || 32, geo.heightSegments || 16)
389
+ case 'cylinder':
390
+ return new THREE.CylinderGeometry(geo.radiusTop || 1, geo.radiusBottom || 1, geo.height || 1, geo.radialSegments || 32)
391
+ case 'plane':
392
+ return new THREE.PlaneGeometry(geo.width || 1, geo.height || 1)
393
+ case 'torus':
394
+ return new THREE.TorusGeometry(geo.radius || 1, geo.tube || 0.4, geo.radialSegments || 16, geo.tubularSegments || 100)
395
+ case 'cone':
396
+ return new THREE.ConeGeometry(geo.radius || 1, geo.height || 1, geo.radialSegments || 32)
397
+ case 'buffer':
398
+ // Custom buffer geometry from vertices
399
+ if (geo.vertices) {
400
+ const bufferGeo = new THREE.BufferGeometry()
401
+ bufferGeo.setAttribute('position', new THREE.Float32BufferAttribute(geo.vertices, 3))
402
+ if (geo.normals) {
403
+ bufferGeo.setAttribute('normal', new THREE.Float32BufferAttribute(geo.normals, 3))
404
+ }
405
+ if (geo.uvs) {
406
+ bufferGeo.setAttribute('uv', new THREE.Float32BufferAttribute(geo.uvs, 2))
407
+ }
408
+ if (geo.indices) {
409
+ bufferGeo.setIndex(geo.indices)
410
+ }
411
+ return bufferGeo
412
+ }
413
+ return new THREE.BoxGeometry(1, 1, 1)
414
+ default:
415
+ return new THREE.BoxGeometry(1, 1, 1)
416
+ }
417
+ }, [config?.geometry])
418
+
419
+ const material = useMemo(() => createMaterial(config?.material), [config?.material])
420
+
421
+ // Animation
422
+ useFrame(() => {
423
+ if (!meshRef.current || !config?.animation) return
424
+
425
+ const anim = config.animation
426
+ if (anim.autoRotate) {
427
+ const speed = anim.speed || 0.01
428
+ meshRef.current.rotation.y += speed
429
+ }
430
+ if (anim.rotation) {
431
+ meshRef.current.rotation.x += anim.rotation[0] || 0
432
+ meshRef.current.rotation.y += anim.rotation[1] || 0
433
+ meshRef.current.rotation.z += anim.rotation[2] || 0
434
+ }
435
+ })
436
+
437
+ // Cleanup
438
+ useEffect(() => {
439
+ return () => {
440
+ geometry.dispose()
441
+ material.dispose()
442
+ }
443
+ }, [geometry, material])
444
+
445
+ return (
446
+ <mesh
447
+ ref={meshRef}
448
+ geometry={geometry}
449
+ material={material}
450
+ position={config?.position || [0, 0, 0]}
451
+ rotation={config?.rotation || [0, 0, 0]}
452
+ scale={config?.scale || [1, 1, 1]}
453
+ castShadow={config?.castShadow !== false}
454
+ receiveShadow={config?.receiveShadow !== false}
455
+ />
456
+ )
457
+ }
458
+
459
+ /**
460
+ * Model loader that handles different formats
461
+ */
462
+ function ModelLoader({ config, onLoad, onError }) {
463
+ const format = config?.format || 'geometry'
464
+
465
+ switch (format) {
466
+ case 'gltf':
467
+ case 'glb':
468
+ return <GLTFModel url={config.url} config={config} onLoad={onLoad} onError={onError} />
469
+ case 'obj':
470
+ return <OBJModel url={config.url} config={config} onLoad={onLoad} onError={onError} />
471
+ case 'geometry':
472
+ default:
473
+ return <CustomGeometry config={config} />
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Scene content component
479
+ */
480
+ function SceneContent({ sceneConfig, controls, animation, onModelLoad, onModelError }) {
481
+ const { scene } = useThree()
482
+
483
+ // Set scene background
484
+ useEffect(() => {
485
+ if (sceneConfig?.backgroundColor) {
486
+ scene.background = new THREE.Color(sceneConfig.backgroundColor)
487
+ }
488
+ if (sceneConfig?.fog) {
489
+ scene.fog = new THREE.Fog(
490
+ sceneConfig.fog.color || 0x000000,
491
+ sceneConfig.fog.near || 1,
492
+ sceneConfig.fog.far || 1000
493
+ )
494
+ }
495
+ }, [scene, sceneConfig])
496
+
497
+ // Memoize lights
498
+ const lights = useMemo(() => {
499
+ return (sceneConfig?.lights || [
500
+ { type: 'ambient', color: '#ffffff', intensity: 0.5 },
501
+ { type: 'directional', position: [10, 10, 10], intensity: 1 },
502
+ ]).map((light, index) => <Light key={`light-${index}`} config={light} />)
503
+ }, [sceneConfig?.lights])
504
+
505
+ return (
506
+ <>
507
+ {lights}
508
+ <Bounds fit clip observe margin={1.2}>
509
+ <Center>
510
+ <ModelLoader
511
+ config={sceneConfig?.model}
512
+ onLoad={onModelLoad}
513
+ onError={onModelError}
514
+ />
515
+ </Center>
516
+ </Bounds>
517
+ <Controls type={controls} config={animation} />
518
+ </>
519
+ )
520
+ }
521
+
522
+ /**
523
+ * Main ThreeCustom component
524
+ */
525
+ const ThreeCustom = forwardRef(({ props, style }, ref) => {
526
+ const { config } = props || {}
527
+ const [isLoading, setIsLoading] = useState(true)
528
+ const [error, setError] = useState(null)
529
+ const canvasRef = useRef()
530
+
531
+ // Extract configuration
532
+ const sceneConfig = config?.scene || {}
533
+ const cameraConfig = sceneConfig?.camera || {
534
+ position: [0, 0, 100],
535
+ fov: 75,
536
+ near: 0.1,
537
+ far: 1000,
538
+ }
539
+ const controls = config?.controls || 'orbit'
540
+ const animation = config?.animation || {}
541
+
542
+ // Memoize camera settings
543
+ const cameraSettings = useMemo(
544
+ () => ({
545
+ position: cameraConfig.position || [0, 0, 100],
546
+ fov: cameraConfig.fov || 75,
547
+ near: cameraConfig.near || 0.1,
548
+ far: cameraConfig.far || 1000,
549
+ }),
550
+ [cameraConfig]
551
+ )
552
+
553
+ // Handle model load
554
+ const handleModelLoad = useCallback(() => {
555
+ setIsLoading(false)
556
+ }, [])
557
+
558
+ // Handle model error
559
+ const handleModelError = useCallback((err) => {
560
+ console.error('Model loading error:', err)
561
+ setError(err)
562
+ setIsLoading(false)
563
+ }, [])
564
+
565
+ // Expose imperative handle
566
+ useImperativeHandle(
567
+ ref,
568
+ () => ({
569
+ getCanvas: () => canvasRef.current,
570
+ getScene: () => canvasRef.current?.scene,
571
+ getCamera: () => canvasRef.current?.camera,
572
+ getRenderer: () => canvasRef.current?.gl,
573
+ screenshot: (mimeType = 'image/png') => {
574
+ const renderer = canvasRef.current?.gl
575
+ if (renderer) {
576
+ return renderer.domElement.toDataURL(mimeType)
577
+ }
578
+ return null
579
+ },
580
+ }),
581
+ []
582
+ )
583
+
584
+ if (error) {
585
+ return (
586
+ <div
587
+ style={{
588
+ ...style,
589
+ display: 'flex',
590
+ alignItems: 'center',
591
+ justifyContent: 'center',
592
+ }}
593
+ >
594
+ <div style={{ color: 'red', textAlign: 'center' }}>
595
+ <p>Failed to load 3D model</p>
596
+ <p style={{ fontSize: '12px' }}>{error.message}</p>
597
+ </div>
598
+ </div>
599
+ )
600
+ }
601
+
602
+ return (
603
+ <div id={props?.id} style={{ ...style, position: 'relative' }}>
604
+ {isLoading && (
605
+ <div
606
+ style={{
607
+ position: 'absolute',
608
+ top: 0,
609
+ left: 0,
610
+ right: 0,
611
+ bottom: 0,
612
+ display: 'flex',
613
+ alignItems: 'center',
614
+ justifyContent: 'center',
615
+ background: '#f5f5f5',
616
+ zIndex: 1,
617
+ }}
618
+ >
619
+ Loading 3D Model...
620
+ </div>
621
+ )}
622
+ <ErrorBoundary>
623
+ <Canvas
624
+ ref={canvasRef}
625
+ gl={{
626
+ antialias: true,
627
+ alpha: true,
628
+ powerPreference: 'high-performance',
629
+ }}
630
+ camera={cameraSettings}
631
+ style={{ width: '100%', height: '100%' }}
632
+ >
633
+ <SceneContent
634
+ sceneConfig={sceneConfig}
635
+ controls={controls}
636
+ animation={animation}
637
+ onModelLoad={handleModelLoad}
638
+ onModelError={handleModelError}
639
+ />
640
+ </Canvas>
641
+ </ErrorBoundary>
642
+ </div>
643
+ )
644
+ })
645
+
646
+ ThreeCustom.displayName = 'ThreeCustom'
647
+
648
+ export default ThreeCustom