senangwebs-aframe-editor 1.6.5

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 (104) hide show
  1. package/.babelrc +3 -0
  2. package/.editorconfig +12 -0
  3. package/.eslintignore +2 -0
  4. package/.eslintrc +40 -0
  5. package/.github/workflows/ci.yml +39 -0
  6. package/.husky/pre-commit +4 -0
  7. package/.prettierignore +1 -0
  8. package/.prettierrc.json +5 -0
  9. package/.stylelintrc +12 -0
  10. package/LICENSE +21 -0
  11. package/README.md +75 -0
  12. package/assets/gltf.svg +49 -0
  13. package/dist/aframe-inspector.js +106250 -0
  14. package/dist/aframe-inspector.js.map +1 -0
  15. package/dist/aframe-inspector.min.js +29040 -0
  16. package/dist/aframe-inspector.min.js.LICENSE.txt +56 -0
  17. package/dist/aframe-inspector.min.js.map +1 -0
  18. package/examples/360video.html +48 -0
  19. package/examples/colors.html +18 -0
  20. package/examples/controllers.html +60 -0
  21. package/examples/embedded-zoom.html +78 -0
  22. package/examples/embedded.html +79 -0
  23. package/examples/empty.html +13 -0
  24. package/examples/index-aframe.html +66 -0
  25. package/examples/index.html +71 -0
  26. package/examples/supercraft.html +6 -0
  27. package/index.html +8 -0
  28. package/package.json +84 -0
  29. package/senangwebs-webverse-editor.png +0 -0
  30. package/src/components/AwesomeIcon.js +53 -0
  31. package/src/components/Collapsible.js +57 -0
  32. package/src/components/EntityRepresentation.js +83 -0
  33. package/src/components/Main.js +222 -0
  34. package/src/components/__tests__/Collapsible.test.js +30 -0
  35. package/src/components/components/AddComponent.js +104 -0
  36. package/src/components/components/CommonComponents.js +160 -0
  37. package/src/components/components/Component.js +151 -0
  38. package/src/components/components/ComponentsContainer.js +52 -0
  39. package/src/components/components/DefaultComponents.js +1 -0
  40. package/src/components/components/Mixins.js +83 -0
  41. package/src/components/components/PropertyRow.js +145 -0
  42. package/src/components/components/Sidebar.js +51 -0
  43. package/src/components/icons/BackViewIcon.js +27 -0
  44. package/src/components/icons/BottomViewIcon.js +26 -0
  45. package/src/components/icons/FrontViewIcon.js +23 -0
  46. package/src/components/icons/LeftViewIcon.js +24 -0
  47. package/src/components/icons/PerspectiveIcon.js +23 -0
  48. package/src/components/icons/PrimitiveBoxIcon.js +143 -0
  49. package/src/components/icons/PrimitiveConeIcon.js +44 -0
  50. package/src/components/icons/PrimitiveCylinderIcon.js +51 -0
  51. package/src/components/icons/PrimitiveEmptyEntityIcon.js +78 -0
  52. package/src/components/icons/PrimitiveImageIcon.js +86 -0
  53. package/src/components/icons/PrimitiveLightIcon.js +107 -0
  54. package/src/components/icons/PrimitivePlaneIcon.js +87 -0
  55. package/src/components/icons/PrimitiveSphereIcon.js +39 -0
  56. package/src/components/icons/PrimitiveTextIcon.js +89 -0
  57. package/src/components/icons/PrimitiveTorusIcon.js +31 -0
  58. package/src/components/icons/RightViewIcon.js +24 -0
  59. package/src/components/icons/TopViewIcon.js +24 -0
  60. package/src/components/modals/Modal.js +107 -0
  61. package/src/components/modals/ModalHelp.js +97 -0
  62. package/src/components/modals/ModalPrimitive.js +114 -0
  63. package/src/components/modals/ModalTextures.js +430 -0
  64. package/src/components/scenegraph/Entity.js +142 -0
  65. package/src/components/scenegraph/SceneGraph.js +337 -0
  66. package/src/components/scenegraph/Toolbar.js +147 -0
  67. package/src/components/viewport/CameraToolbar.js +122 -0
  68. package/src/components/viewport/TransformToolbar.js +102 -0
  69. package/src/components/viewport/ViewportHUD.js +33 -0
  70. package/src/components/widgets/BooleanWidget.js +49 -0
  71. package/src/components/widgets/ColorWidget.js +89 -0
  72. package/src/components/widgets/InputWidget.js +42 -0
  73. package/src/components/widgets/NumberWidget.js +179 -0
  74. package/src/components/widgets/SelectWidget.js +58 -0
  75. package/src/components/widgets/TextureWidget.js +252 -0
  76. package/src/components/widgets/Vec2Widget.js +55 -0
  77. package/src/components/widgets/Vec3Widget.js +58 -0
  78. package/src/components/widgets/Vec4Widget.js +61 -0
  79. package/src/components/widgets/index.js +9 -0
  80. package/src/index.js +301 -0
  81. package/src/lib/EditorControls.js +336 -0
  82. package/src/lib/Events.js +6 -0
  83. package/src/lib/TransformControls.js +1365 -0
  84. package/src/lib/assetsLoader.js +43 -0
  85. package/src/lib/assetsUtils.js +30 -0
  86. package/src/lib/cameras.js +121 -0
  87. package/src/lib/entity.js +556 -0
  88. package/src/lib/history.js +30 -0
  89. package/src/lib/raycaster.js +129 -0
  90. package/src/lib/shortcuts.js +211 -0
  91. package/src/lib/utils.js +118 -0
  92. package/src/lib/viewport.js +268 -0
  93. package/src/style/components.styl +275 -0
  94. package/src/style/entity.styl +22 -0
  95. package/src/style/help.styl +40 -0
  96. package/src/style/index.styl +358 -0
  97. package/src/style/lib.styl +41 -0
  98. package/src/style/primitiveModal.styl +90 -0
  99. package/src/style/scenegraph.styl +173 -0
  100. package/src/style/select.styl +71 -0
  101. package/src/style/textureModal.styl +220 -0
  102. package/src/style/viewport.styl +168 -0
  103. package/src/style/widgets.styl +71 -0
  104. package/webpack.config.js +65 -0
@@ -0,0 +1,337 @@
1
+ /* eslint-disable no-unused-vars, react/no-danger */
2
+ import React from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import { faSearch, faTimes } from '@fortawesome/free-solid-svg-icons';
5
+ import { AwesomeIcon } from '../AwesomeIcon';
6
+ import debounce from 'lodash.debounce';
7
+
8
+ import Entity from './Entity';
9
+ import Toolbar from './Toolbar';
10
+ import Events from '../../lib/Events';
11
+ import CameraToolbar from '../viewport/CameraToolbar';
12
+
13
+ export default class SceneGraph extends React.Component {
14
+ static propTypes = {
15
+ id: PropTypes.string,
16
+ onChange: PropTypes.func,
17
+ scene: PropTypes.object,
18
+ selectedEntity: PropTypes.object,
19
+ visible: PropTypes.bool
20
+ };
21
+
22
+ static defaultProps = {
23
+ selectedEntity: '',
24
+ index: -1,
25
+ id: 'left-sidebar'
26
+ };
27
+
28
+ constructor(props) {
29
+ super(props);
30
+ this.state = {
31
+ entities: [],
32
+ expandedElements: new WeakMap([[props.scene, true]]),
33
+ filter: '',
34
+ filteredEntities: [],
35
+ selectedIndex: -1
36
+ };
37
+
38
+ this.rebuildEntityOptions = debounce(
39
+ this.rebuildEntityOptions.bind(this),
40
+ 1000
41
+ );
42
+ this.updateFilteredEntities = debounce(
43
+ this.updateFilteredEntities.bind(this),
44
+ 500
45
+ );
46
+ }
47
+
48
+ componentDidMount() {
49
+ this.rebuildEntityOptions();
50
+ Events.on('entityidchange', this.rebuildEntityOptions);
51
+ Events.on('entitycreated', this.rebuildEntityOptions);
52
+ Events.on('entityclone', this.rebuildEntityOptions);
53
+ Events.on('entityupdate', (detail) => {
54
+ if (detail.component === 'mixin') {
55
+ this.rebuildEntityOptions();
56
+ }
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Selected entity updated from somewhere else in the app.
62
+ */
63
+ componentDidUpdate(prevProps) {
64
+ if (prevProps.selectedEntity !== this.props.selectedEntity) {
65
+ this.selectEntity(this.props.selectedEntity);
66
+ }
67
+ }
68
+
69
+ selectEntity = (entity) => {
70
+ let found = false;
71
+ for (let i = 0; i < this.state.filteredEntities.length; i++) {
72
+ const entityOption = this.state.filteredEntities[i];
73
+ if (entityOption.entity === entity) {
74
+ this.setState({ selectedEntity: entity, selectedIndex: i });
75
+ // Make sure selected value is visible in scenegraph
76
+ this.expandToRoot(entity);
77
+ if (this.props.onChange) {
78
+ this.props.onChange(entity);
79
+ }
80
+ Events.emit('entityselect', entity, true);
81
+ found = true;
82
+ }
83
+ }
84
+
85
+ if (!found) {
86
+ this.setState({ selectedEntity: null, selectedIndex: -1 });
87
+ }
88
+ };
89
+
90
+ rebuildEntityOptions = () => {
91
+ const entities = [{ depth: 0, entity: this.props.scene }];
92
+
93
+ function treeIterate(element, depth) {
94
+ if (!element) {
95
+ return;
96
+ }
97
+ depth += 1;
98
+
99
+ for (let i = 0; i < element.children.length; i++) {
100
+ let entity = element.children[i];
101
+
102
+ if (
103
+ entity.dataset.isInspector ||
104
+ !entity.isEntity ||
105
+ entity.isInspector ||
106
+ 'aframeInspector' in entity.dataset
107
+ ) {
108
+ continue;
109
+ }
110
+
111
+ entities.push({ entity: entity, depth: depth });
112
+
113
+ treeIterate(entity, depth);
114
+ }
115
+ }
116
+ treeIterate(this.props.scene, 0);
117
+
118
+ this.setState({
119
+ entities: entities,
120
+ filteredEntities: this.getFilteredEntities(this.state.filter, entities)
121
+ });
122
+ };
123
+
124
+ selectIndex = (index) => {
125
+ if (index >= 0 && index < this.state.entities.length) {
126
+ this.selectEntity(this.state.entities[index].entity);
127
+ }
128
+ };
129
+
130
+ onFilterKeyUp = (event) => {
131
+ if (event.key === 'Escape') { // Use event.key for consistency
132
+ this.clearFilter();
133
+ }
134
+ };
135
+
136
+ onKeyDown = (event) => {
137
+ switch (event.key) {
138
+ case 'ArrowLeft':
139
+ case 'ArrowUp':
140
+ case 'ArrowRight':
141
+ case 'ArrowDown':
142
+ event.preventDefault();
143
+ event.stopPropagation();
144
+ break;
145
+ }
146
+ };
147
+
148
+ onKeyUp = (event) => {
149
+ if (this.props.selectedEntity === null) {
150
+ return;
151
+ }
152
+
153
+ switch (event.key) {
154
+ case 'ArrowLeft':
155
+ if (this.isExpanded(this.props.selectedEntity)) {
156
+ this.toggleExpandedCollapsed(this.props.selectedEntity);
157
+ }
158
+ break;
159
+ case 'ArrowUp':
160
+ this.selectIndex(
161
+ this.previousExpandedIndexTo(this.state.selectedIndex)
162
+ );
163
+ break;
164
+ case 'ArrowRight':
165
+ if (!this.isExpanded(this.props.selectedEntity)) {
166
+ this.toggleExpandedCollapsed(this.props.selectedEntity);
167
+ }
168
+ break;
169
+ case 'ArrowDown':
170
+ this.selectIndex(this.nextExpandedIndexTo(this.state.selectedIndex));
171
+ break;
172
+ }
173
+ };
174
+
175
+ getFilteredEntities(filter, entities) {
176
+ entities = entities || this.state.entities;
177
+ if (!filter) {
178
+ return entities;
179
+ }
180
+ return entities.filter((entityOption) => {
181
+ return filterEntity(entityOption.entity, filter || this.state.filter);
182
+ });
183
+ }
184
+
185
+ isVisibleInSceneGraph = (x) => {
186
+ let curr = x.parentNode;
187
+ if (!curr) {
188
+ return false;
189
+ }
190
+ while (curr !== undefined && curr.isEntity) {
191
+ if (!this.isExpanded(curr)) {
192
+ return false;
193
+ }
194
+ curr = curr.parentNode;
195
+ }
196
+ return true;
197
+ };
198
+
199
+ isExpanded = (x) => this.state.expandedElements.get(x) === true;
200
+
201
+ toggleExpandedCollapsed = (x) => {
202
+ this.setState({
203
+ expandedElements: this.state.expandedElements.set(x, !this.isExpanded(x))
204
+ });
205
+ };
206
+
207
+ expandToRoot = (x) => {
208
+ // Expand element all the way to the scene element
209
+ let curr = x.parentNode;
210
+ while (curr !== undefined && curr.isEntity) {
211
+ this.state.expandedElements.set(curr, true);
212
+ curr = curr.parentNode;
213
+ }
214
+ this.setState({ expandedElements: this.state.expandedElements });
215
+ };
216
+
217
+ previousExpandedIndexTo = (i) => {
218
+ for (let prevIter = i - 1; prevIter >= 0; prevIter--) {
219
+ const prevEl = this.state.entities[prevIter].entity;
220
+ if (this.isVisibleInSceneGraph(prevEl)) {
221
+ return prevIter;
222
+ }
223
+ }
224
+ return -1;
225
+ };
226
+
227
+ nextExpandedIndexTo = (i) => {
228
+ for (
229
+ let nextIter = i + 1;
230
+ nextIter < this.state.entities.length;
231
+ nextIter++
232
+ ) {
233
+ const nextEl = this.state.entities[nextIter].entity;
234
+ if (this.isVisibleInSceneGraph(nextEl)) {
235
+ return nextIter;
236
+ }
237
+ }
238
+ return -1;
239
+ };
240
+
241
+ onChangeFilter = (evt) => {
242
+ const filter = evt.target.value;
243
+ this.setState({ filter: filter });
244
+ this.updateFilteredEntities(filter);
245
+ };
246
+
247
+ updateFilteredEntities(filter) {
248
+ this.setState({
249
+ filteredEntities: this.getFilteredEntities(filter)
250
+ });
251
+ }
252
+
253
+ clearFilter = () => {
254
+ this.setState({ filter: '' });
255
+ this.updateFilteredEntities('');
256
+ };
257
+
258
+ renderEntities = () => {
259
+ return this.state.filteredEntities.map((entityOption, idx) => {
260
+ if (
261
+ !this.isVisibleInSceneGraph(entityOption.entity) &&
262
+ !this.state.filter
263
+ ) {
264
+ return null;
265
+ }
266
+ return (
267
+ <Entity
268
+ {...entityOption}
269
+ key={idx}
270
+ isFiltering={!!this.state.filter}
271
+ isExpanded={this.isExpanded(entityOption.entity)}
272
+ isSelected={this.props.selectedEntity === entityOption.entity}
273
+ selectEntity={this.selectEntity}
274
+ toggleExpandedCollapsed={this.toggleExpandedCollapsed}
275
+ />
276
+ );
277
+ });
278
+ };
279
+
280
+ render() {
281
+ // To hide the SceneGraph we have to hide its parent too (#left-sidebar).
282
+ if (!this.props.visible) {
283
+ return null;
284
+ }
285
+
286
+ const clearFilter = this.state.filter ? (
287
+ <a onClick={this.clearFilter} className="button">
288
+ <AwesomeIcon icon={faTimes} />
289
+ </a>
290
+ ) : null;
291
+
292
+ return (
293
+ <div id="scenegraph" className="scenegraph">
294
+ <div className="scenegraph-toolbar">
295
+ <Toolbar />
296
+ <div className="search">
297
+ <input
298
+ id="filter"
299
+ placeholder="Search..."
300
+ onChange={this.onChangeFilter}
301
+ onKeyUp={this.onFilterKeyUp}
302
+ value={this.state.filter}
303
+ />
304
+ {clearFilter}
305
+ {!this.state.filter && <AwesomeIcon icon={faSearch} />}
306
+ </div>
307
+ </div>
308
+ <div
309
+ className="outliner"
310
+ tabIndex="0"
311
+ onKeyDown={this.onKeyDown}
312
+ onKeyUp={this.onKeyUp}
313
+ >
314
+ {this.renderEntities()}
315
+ </div>
316
+ </div>
317
+ );
318
+ }
319
+ }
320
+
321
+ function filterEntity(entity, filter) {
322
+ if (!filter) {
323
+ return true;
324
+ }
325
+
326
+ // Check if the ID, tagName, class, selector includes the filter.
327
+ if (
328
+ entity.id.toUpperCase().indexOf(filter.toUpperCase()) !== -1 ||
329
+ entity.tagName.toUpperCase().indexOf(filter.toUpperCase()) !== -1 ||
330
+ entity.classList.contains(filter) ||
331
+ entity.matches(filter)
332
+ ) {
333
+ return true;
334
+ }
335
+
336
+ return false;
337
+ }
@@ -0,0 +1,147 @@
1
+ import React from 'react';
2
+ import {
3
+ faPlus,
4
+ faPause,
5
+ faPlay,
6
+ // faCube, // Removed as it's not used directly here anymore
7
+ // faCode, // Removed as it's not used directly here anymore
8
+ // faFloppyDisk // Removed as it's not used directly here anymore
9
+ } from '@fortawesome/free-solid-svg-icons';
10
+ import { AwesomeIcon } from '../AwesomeIcon';
11
+ import Events from '../../lib/Events';
12
+ import ModalPrimitive from '../modals/ModalPrimitive'; // Import the new modal component
13
+
14
+ function filterHelpers(scene, visible) {
15
+ scene.traverse((o) => {
16
+ if (o.userData.source === 'INSPECTOR') {
17
+ o.visible = visible;
18
+ }
19
+ });
20
+ }
21
+
22
+ function getSceneName(scene) {
23
+ return scene.id || slugify(window.location.host + window.location.pathname);
24
+ }
25
+
26
+ /**
27
+ * Slugify the string removing non-word chars and spaces
28
+ * @param {string} text String to slugify
29
+ * @return {string} Slugified string
30
+ */
31
+ function slugify(text) {
32
+ return text
33
+ .toString()
34
+ .toLowerCase()
35
+ .replace(/\s+/g, '-') // Replace spaces with -
36
+ .replace(/[^\w-]+/g, '-') // Replace all non-word chars with -
37
+ .replace(/--+/g, '-') // Replace multiple - with single -
38
+ .replace(/^-+/, '') // Trim - from start of text
39
+ .replace(/-+$/, ''); // Trim - from end of text
40
+ }
41
+
42
+ /**
43
+ * Tools and actions.
44
+ */
45
+ export default class Toolbar extends React.Component {
46
+ constructor(props) {
47
+ super(props);
48
+
49
+ this.state = {
50
+ isPlaying: false,
51
+ isAddEntityModalOpen: false // Add state for modal visibility
52
+ };
53
+ }
54
+
55
+ exportSceneToGLTF() {
56
+ const sceneName = getSceneName(AFRAME.scenes[0]);
57
+ const scene = AFRAME.scenes[0].object3D;
58
+ filterHelpers(scene, false);
59
+ AFRAME.INSPECTOR.exporters.gltf.parse(
60
+ scene,
61
+ function (buffer) {
62
+ filterHelpers(scene, true);
63
+ const blob = new Blob([buffer], { type: 'application/octet-stream' });
64
+ saveBlob(blob, sceneName + '.glb');
65
+ },
66
+ function (error) {
67
+ console.error(error);
68
+ },
69
+ { binary: true }
70
+ );
71
+ }
72
+
73
+ // Renamed original addEntity to toggleAddEntityModal
74
+ toggleAddEntityModal = () => {
75
+ this.setState({ isAddEntityModalOpen: !this.state.isAddEntityModalOpen });
76
+ };
77
+
78
+ // New function to handle entity creation based on modal selection
79
+ createEntity = (primitiveType) => {
80
+ if (primitiveType) {
81
+ Events.emit('entitycreate', { element: primitiveType, components: {} });
82
+ }
83
+ this.setState({ isAddEntityModalOpen: false }); // Close modal after selection or cancellation
84
+ };
85
+
86
+
87
+ toggleScenePlaying = () => {
88
+ if (this.state.isPlaying) {
89
+ AFRAME.scenes[0].pause();
90
+ this.setState({ isPlaying: false });
91
+ AFRAME.scenes[0].isPlaying = true;
92
+ document.getElementById('aframeInspectorMouseCursor').play();
93
+ return;
94
+ }
95
+ AFRAME.scenes[0].isPlaying = false;
96
+ AFRAME.scenes[0].play();
97
+ this.setState({ isPlaying: true });
98
+ };
99
+
100
+ render() {
101
+ // const watcherTitle = 'Write changes with aframe-watcher.'; // Keep or remove as needed
102
+
103
+ return (
104
+ <div id="toolbar">
105
+ <div className="toolbarActions">
106
+ <a
107
+ className="button"
108
+ title="Add a new entity"
109
+ onClick={this.toggleAddEntityModal} // Changed onClick handler
110
+ >
111
+ <AwesomeIcon icon={faPlus} />
112
+ <span>Add Entity</span>
113
+ </a>
114
+ {/* Play/Pause Button */}
115
+ <a
116
+ id="playPauseScene"
117
+ className="button"
118
+ title={this.state.isPlaying ? 'Pause scene' : 'Resume scene'}
119
+ onClick={this.toggleScenePlaying}
120
+ >
121
+ {this.state.isPlaying ? (
122
+ <>
123
+ <AwesomeIcon icon={faPause} />
124
+ <span>Pause Scene</span>
125
+ </>
126
+ ) : (
127
+ <>
128
+ <AwesomeIcon icon={faPlay} />
129
+ <span>Play Scene</span>
130
+ </>
131
+ )}
132
+ </a>
133
+ {/* Add other buttons like export, save if needed */}
134
+ </div>
135
+
136
+ {/* Conditionally render the modal */}
137
+ {this.state.isAddEntityModalOpen && (
138
+ <ModalPrimitive
139
+ isOpen={this.state.isAddEntityModalOpen}
140
+ onClose={this.toggleAddEntityModal} // Pass function to close modal
141
+ onSelectPrimitive={this.createEntity} // Pass function to create entity
142
+ />
143
+ )}
144
+ </div>
145
+ );
146
+ }
147
+ }
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import Select from 'react-select';
3
+ import Events from '../../lib/Events';
4
+
5
+ // Import custom SVG icons
6
+ import PerspectiveIcon from '../icons/PerspectiveIcon';
7
+ import LeftViewIcon from '../icons/LeftViewIcon';
8
+ import RightViewIcon from '../icons/RightViewIcon';
9
+ import TopViewIcon from '../icons/TopViewIcon';
10
+ import BottomViewIcon from '../icons/BottomViewIcon';
11
+ import BackViewIcon from '../icons/BackViewIcon';
12
+ import FrontViewIcon from '../icons/FrontViewIcon';
13
+
14
+ const options = [
15
+ {
16
+ value: 'perspective',
17
+ event: 'cameraperspectivetoggle',
18
+ payload: null,
19
+ label: 'Perspective',
20
+ icon: <PerspectiveIcon width="24" height="24" /> // Use custom icon
21
+ },
22
+ {
23
+ value: 'ortholeft',
24
+ event: 'cameraorthographictoggle',
25
+ payload: 'left',
26
+ label: 'Left View',
27
+ icon: <LeftViewIcon width="24" height="24" /> // Use custom icon
28
+ },
29
+ {
30
+ value: 'orthoright',
31
+ event: 'cameraorthographictoggle',
32
+ payload: 'right',
33
+ label: 'Right View',
34
+ icon: <RightViewIcon width="24" height="24" /> // Use custom icon
35
+ },
36
+ {
37
+ value: 'orthotop',
38
+ event: 'cameraorthographictoggle',
39
+ payload: 'top',
40
+ label: 'Top View',
41
+ icon: <TopViewIcon width="24" height="24" /> // Use custom icon
42
+ },
43
+ {
44
+ value: 'orthobottom',
45
+ event: 'cameraorthographictoggle',
46
+ payload: 'bottom',
47
+ label: 'Bottom View',
48
+ icon: <BottomViewIcon width="24" height="24" /> // Use custom icon
49
+ },
50
+ {
51
+ value: 'orthoback',
52
+ event: 'cameraorthographictoggle',
53
+ payload: 'back',
54
+ label: 'Back View',
55
+ icon: <BackViewIcon width="24" height="24" /> // Use custom icon
56
+ },
57
+ {
58
+ value: 'orthofront',
59
+ event: 'cameraorthographictoggle',
60
+ payload: 'front',
61
+ label: 'Front View',
62
+ icon: <FrontViewIcon width="24" height="24" /> // Use custom icon
63
+ }
64
+ ];
65
+
66
+ export default class CameraToolbar extends React.Component {
67
+ constructor(props) {
68
+ super(props);
69
+ this.state = {
70
+ selectedCamera: 'perspective'
71
+ };
72
+ this.justChangedCamera = false;
73
+ }
74
+
75
+ componentDidMount() {
76
+ Events.on('cameratoggle', (data) => {
77
+ if (this.justChangedCamera) {
78
+ // Prevent recursion.
79
+ this.justChangedCamera = false;
80
+ return;
81
+ }
82
+ // Check if the incoming data value exists in our options
83
+ const isValidOption = options.some((opt) => opt.value === data.value);
84
+ if (isValidOption) {
85
+ this.setState({ selectedCamera: data.value });
86
+ }
87
+ });
88
+ }
89
+
90
+ onChange(option) {
91
+ console.log('Selected Camera Option:', option);
92
+ this.justChangedCamera = true;
93
+ this.setState({ selectedCamera: option.value });
94
+ Events.emit(option.event, option.payload);
95
+ }
96
+
97
+ render() {
98
+ // Custom formatOption to render label and icon
99
+ const formatOptionLabel = ({ label, icon }) => (
100
+ <div style={{ display: 'flex', alignItems: 'center' }}>
101
+ {icon}
102
+ <span style={{ marginLeft: '8px' }}>{label}</span>
103
+ </div>
104
+ );
105
+
106
+ return (
107
+ <div id="cameraToolbar">
108
+ <Select
109
+ id="cameraSelect"
110
+ classNamePrefix="select"
111
+ options={options}
112
+ value={options.find(
113
+ (option) => option.value === this.state.selectedCamera
114
+ )}
115
+ isSearchable={false}
116
+ onChange={this.onChange.bind(this)}
117
+ formatOptionLabel={formatOptionLabel}
118
+ />
119
+ </div>
120
+ );
121
+ }
122
+ }
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+ import {
3
+ faArrowsAlt,
4
+ faRotateRight,
5
+ faUpRightAndDownLeftFromCenter
6
+ } from '@fortawesome/free-solid-svg-icons';
7
+ import { AwesomeIcon } from '../AwesomeIcon';
8
+ import clsx from 'clsx';
9
+ import Events from '../../lib/Events';
10
+
11
+ var TransformButtons = [
12
+ { value: 'translate', icon: <AwesomeIcon icon={faArrowsAlt} /> },
13
+ { value: 'rotate', icon: <AwesomeIcon icon={faRotateRight} /> },
14
+ {
15
+ value: 'scale',
16
+ icon: <AwesomeIcon icon={faUpRightAndDownLeftFromCenter} />
17
+ }
18
+ ];
19
+
20
+ export default class TransformToolbar extends React.Component {
21
+ constructor(props) {
22
+ super(props);
23
+ this.state = {
24
+ selectedTransform: 'translate',
25
+ localSpace: false
26
+ };
27
+ }
28
+
29
+ componentDidMount() {
30
+ Events.on('transformmodechange', (mode) => {
31
+ this.setState({ selectedTransform: mode });
32
+ });
33
+
34
+ Events.on('transformspacechange', () => {
35
+ Events.emit(
36
+ 'transformspacechanged',
37
+ this.state.localSpace ? 'world' : 'local'
38
+ );
39
+ this.setState({ localSpace: !this.state.localSpace });
40
+ });
41
+ }
42
+
43
+ changeTransformMode = (mode) => {
44
+ this.setState({ selectedTransform: mode });
45
+ Events.emit('transformmodechange', mode);
46
+ };
47
+
48
+ onLocalChange = (e) => {
49
+ const local = e.target.checked;
50
+ this.setState({ localSpace: local });
51
+ Events.emit('transformspacechanged', local ? 'local' : 'world');
52
+ };
53
+
54
+ renderTransformButtons = () => {
55
+ return TransformButtons.map(
56
+ function (option, i) {
57
+ var selected = option.value === this.state.selectedTransform;
58
+ var classes = clsx({
59
+ button: true,
60
+ active: selected
61
+ });
62
+
63
+ return (
64
+ <a
65
+ title={option.value}
66
+ key={i}
67
+ onClick={this.changeTransformMode.bind(this, option.value)}
68
+ className={classes}
69
+ >
70
+ {option.icon}
71
+ </a>
72
+ );
73
+ }.bind(this)
74
+ );
75
+ };
76
+
77
+ render() {
78
+ return (
79
+ <div id="transformToolbar" className="toolbarButtons">
80
+ {this.renderTransformButtons()}
81
+ {/* <span className="local-transform">
82
+ <input
83
+ id="local"
84
+ type="checkbox"
85
+ title="Toggle between local and world space transforms"
86
+ checked={
87
+ this.state.localSpace || this.state.selectedTransform === 'scale'
88
+ }
89
+ disabled={this.state.selectedTransform === 'scale'}
90
+ onChange={this.onLocalChange}
91
+ />
92
+ <label
93
+ htmlFor="local"
94
+ title="Toggle between local and world space transforms"
95
+ >
96
+ Local Space Transform
97
+ </label>
98
+ </span> */}
99
+ </div>
100
+ );
101
+ }
102
+ }