Skip to content

Commit b42a640

Browse files
feat: implement AddObjectMenu component for streamlined object creation in the menu
1 parent 03d026b commit b42a640

File tree

3 files changed

+146
-153
lines changed

3 files changed

+146
-153
lines changed

src/features/menu/components/menu-bar.tsx

Lines changed: 13 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useClipboardStore } from '@/stores/clipboard-store';
2121
import { geometryRedo, geometryUndo } from '@/stores/geometry-store';
2222
import { useRegisterShortcuts } from '@/components/shortcut-provider';
2323
import { Euler, Matrix4, Quaternion, Vector3 } from 'three/webgpu';
24+
import AddObjectMenu from '@/features/shared/add-object-menu';
2425

2526
type Props = { onOpenShaderEditor?: () => void };
2627
const MenuBar: React.FC<Props> = ({ onOpenShaderEditor }) => {
@@ -58,7 +59,7 @@ const MenuBar: React.FC<Props> = ({ onOpenShaderEditor }) => {
5859
};
5960
}, []);
6061

61-
const buildWorkspaceData = useCallback((): WorkspaceData => ({
62+
const buildWorkspaceData = useCallback((): WorkspaceData => ({
6263
meshes: Array.from(geometryStore.meshes.values()),
6364
materials: Array.from(geometryStore.materials.values()),
6465
objects: Object.values(sceneStore.objects),
@@ -72,8 +73,8 @@ const MenuBar: React.FC<Props> = ({ onOpenShaderEditor }) => {
7273
backgroundColor: viewportStore.backgroundColor,
7374
},
7475
selectedObjectId: sceneStore.selectedObjectId,
75-
lights: sceneStore.lights,
76-
cameras: geometryStore.cameras,
76+
lights: sceneStore.lights,
77+
cameras: geometryStore.cameras,
7778
}), [geometryStore, sceneStore, viewportStore]);
7879

7980
// Save (T3D) with existing handle when possible
@@ -377,78 +378,15 @@ const MenuBar: React.FC<Props> = ({ onOpenShaderEditor }) => {
377378
</Menu.Portal>
378379
</Menu.Root>
379380

380-
{/* Add */}
381-
<Menu.Root modal={false} openOnHover>
382-
<Menu.Trigger className="px-2 py-1 text-xs rounded text-gray-300 hover:text-white hover:bg-white/5 data-[open]:bg-white/10 data-[open]:text-white">
383-
Add
384-
</Menu.Trigger>
385-
<Menu.Portal container={portalContainer}>
386-
<Menu.Positioner side="bottom" align="start" sideOffset={4} className="z-90">
387-
<Menu.Popup className="mt-0 min-w-48 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
388-
{/* Quick */}
389-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={handleCreateParticleSystem}>Particle System</Menu.Item>
390-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={handleCreateFluidSystem}>Fluid System</Menu.Item>
391-
<Menu.Separator className="my-1 h-px bg-white/10" />
392-
{/* Mesh submenu */}
393-
<Menu.SubmenuRoot>
394-
<Menu.SubmenuTrigger className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200">Mesh</Menu.SubmenuTrigger>
395-
<Menu.Portal container={portalContainer}>
396-
<Menu.Positioner sideOffset={6} className="z-90">
397-
<Menu.Popup className="min-w-44 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
398-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => beginShape('cube')}>Cube</Menu.Item>
399-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => beginShape('plane')}>Plane</Menu.Item>
400-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => beginShape('cylinder')}>Cylinder</Menu.Item>
401-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => beginShape('cone')}>Cone</Menu.Item>
402-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => beginShape('uvsphere')}>UV Sphere</Menu.Item>
403-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => beginShape('icosphere')}>Ico Sphere</Menu.Item>
404-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => beginShape('torus')}>Torus</Menu.Item>
405-
</Menu.Popup>
406-
</Menu.Positioner>
407-
</Menu.Portal>
408-
</Menu.SubmenuRoot>
409-
{/* Light submenu */}
410-
<Menu.SubmenuRoot>
411-
<Menu.SubmenuTrigger className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200">Light</Menu.SubmenuTrigger>
412-
<Menu.Portal container={portalContainer}>
413-
<Menu.Positioner sideOffset={6} className="z-90">
414-
<Menu.Popup className="min-w-44 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
415-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addLight('directional')}>Directional</Menu.Item>
416-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addLight('spot')}>Spot</Menu.Item>
417-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addLight('point')}>Point</Menu.Item>
418-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addLight('ambient')}>Ambient</Menu.Item>
419-
</Menu.Popup>
420-
</Menu.Positioner>
421-
</Menu.Portal>
422-
</Menu.SubmenuRoot>
423-
{/* Camera submenu */}
424-
<Menu.SubmenuRoot>
425-
<Menu.SubmenuTrigger className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200">Camera</Menu.SubmenuTrigger>
426-
<Menu.Portal container={portalContainer}>
427-
<Menu.Positioner sideOffset={6} className="z-90">
428-
<Menu.Popup className="min-w-44 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
429-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addCamera('perspective')}>Perspective</Menu.Item>
430-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addCamera('orthographic')}>Orthographic</Menu.Item>
431-
</Menu.Popup>
432-
</Menu.Positioner>
433-
</Menu.Portal>
434-
</Menu.SubmenuRoot>
435-
{/* Force Field submenu */}
436-
<Menu.SubmenuRoot>
437-
<Menu.SubmenuTrigger className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200">Force Field</Menu.SubmenuTrigger>
438-
<Menu.Portal container={portalContainer}>
439-
<Menu.Positioner sideOffset={6} className="z-90">
440-
<Menu.Popup className="min-w-44 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
441-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addForce('attractor')}>Attractor</Menu.Item>
442-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addForce('repulsor')}>Repulsor</Menu.Item>
443-
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => addForce('vortex')}>Vortex</Menu.Item>
444-
</Menu.Popup>
445-
</Menu.Positioner>
446-
</Menu.Portal>
447-
</Menu.SubmenuRoot>
448-
</Menu.Popup>
449-
</Menu.Positioner>
450-
</Menu.Portal>
451-
</Menu.Root>
381+
<AddObjectMenu
382+
portalContainer={portalContainer}
383+
onCreateShape={beginShape}
384+
onAddLight={addLight}
385+
onAddCamera={addCamera}
386+
onAddForce={addForce}
387+
onAddParticleSystem={handleCreateParticleSystem}
388+
onAddFluidSystem={handleCreateFluidSystem}
389+
/>
452390

453391
{/* View (placeholders) */}
454392
<Menu.Root modal={false} openOnHover>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"use client";
2+
3+
import React from 'react';
4+
import { Menu } from '@base-ui-components/react/menu';
5+
6+
type Shape = 'cube' | 'plane' | 'cylinder' | 'cone' | 'uvsphere' | 'icosphere' | 'torus';
7+
8+
type Props = {
9+
portalContainer?: HTMLElement | null;
10+
openOnHover?: boolean;
11+
controlledOpen?: boolean;
12+
onOpenChange?: (open: boolean) => void;
13+
triggerLabel?: React.ReactNode;
14+
triggerClassName?: string;
15+
onCreateShape: (shape: Shape) => void;
16+
onAddLight: (type: 'directional' | 'spot' | 'point' | 'ambient') => void;
17+
onAddCamera: (type: 'perspective' | 'orthographic') => void;
18+
onAddForce: (type: 'attractor' | 'repulsor' | 'vortex') => void;
19+
onAddParticleSystem: () => void;
20+
onAddFluidSystem?: () => void;
21+
};
22+
23+
const AddObjectMenu: React.FC<Props> = ({
24+
portalContainer,
25+
openOnHover = false,
26+
controlledOpen,
27+
onOpenChange,
28+
triggerLabel = 'Add',
29+
triggerClassName = 'px-2 py-1 text-xs rounded text-gray-300 hover:text-white hover:bg-white/5',
30+
onCreateShape,
31+
onAddLight,
32+
onAddCamera,
33+
onAddForce,
34+
onAddParticleSystem,
35+
onAddFluidSystem,
36+
}) => {
37+
const closeIfControlled = () => { if (typeof onOpenChange === 'function') onOpenChange(false); };
38+
39+
return (
40+
<Menu.Root modal={false} openOnHover={openOnHover} {...(controlledOpen !== undefined ? { open: controlledOpen, onOpenChange } : {})}>
41+
<Menu.Trigger className={triggerClassName}>{triggerLabel}</Menu.Trigger>
42+
<Menu.Portal container={portalContainer}>
43+
<Menu.Positioner sideOffset={6} className="z-90">
44+
<Menu.Popup className="mt-0 min-w-48 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
45+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddParticleSystem(); closeIfControlled(); }}>Particle System</Menu.Item>
46+
{onAddFluidSystem && (
47+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddFluidSystem(); closeIfControlled(); }}>Fluid System</Menu.Item>
48+
)}
49+
<Menu.Separator className="my-1 h-px bg-white/10" />
50+
51+
{/* Force Field submenu */}
52+
<Menu.SubmenuRoot>
53+
<Menu.SubmenuTrigger className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200">Force Field</Menu.SubmenuTrigger>
54+
<Menu.Portal container={portalContainer}>
55+
<Menu.Positioner sideOffset={6} className="z-90">
56+
<Menu.Popup className="min-w-44 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
57+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddForce('attractor'); closeIfControlled(); }}>Attractor</Menu.Item>
58+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddForce('repulsor'); closeIfControlled(); }}>Repulsor</Menu.Item>
59+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddForce('vortex'); closeIfControlled(); }}>Vortex</Menu.Item>
60+
</Menu.Popup>
61+
</Menu.Positioner>
62+
</Menu.Portal>
63+
</Menu.SubmenuRoot>
64+
65+
<Menu.Separator className="my-1 h-px bg-white/10" />
66+
67+
{/* Mesh submenu */}
68+
<Menu.SubmenuRoot>
69+
<Menu.SubmenuTrigger className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200">Mesh</Menu.SubmenuTrigger>
70+
<Menu.Portal container={portalContainer}>
71+
<Menu.Positioner sideOffset={6} className="z-90">
72+
<Menu.Popup className="min-w-44 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
73+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onCreateShape('cube'); closeIfControlled(); }}>Cube</Menu.Item>
74+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onCreateShape('plane'); closeIfControlled(); }}>Plane</Menu.Item>
75+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onCreateShape('cylinder'); closeIfControlled(); }}>Cylinder</Menu.Item>
76+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onCreateShape('cone'); closeIfControlled(); }}>Cone</Menu.Item>
77+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onCreateShape('uvsphere'); closeIfControlled(); }}>UV Sphere</Menu.Item>
78+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onCreateShape('icosphere'); closeIfControlled(); }}>Ico Sphere</Menu.Item>
79+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onCreateShape('torus'); closeIfControlled(); }}>Torus</Menu.Item>
80+
</Menu.Popup>
81+
</Menu.Positioner>
82+
</Menu.Portal>
83+
</Menu.SubmenuRoot>
84+
85+
{/* Light submenu */}
86+
<Menu.SubmenuRoot>
87+
<Menu.SubmenuTrigger className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200">Light</Menu.SubmenuTrigger>
88+
<Menu.Portal container={portalContainer}>
89+
<Menu.Positioner sideOffset={6} className="z-90">
90+
<Menu.Popup className="min-w-44 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
91+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddLight('directional'); closeIfControlled(); }}>Directional</Menu.Item>
92+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddLight('spot'); closeIfControlled(); }}>Spot</Menu.Item>
93+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddLight('point'); closeIfControlled(); }}>Point</Menu.Item>
94+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddLight('ambient'); closeIfControlled(); }}>Ambient</Menu.Item>
95+
</Menu.Popup>
96+
</Menu.Positioner>
97+
</Menu.Portal>
98+
</Menu.SubmenuRoot>
99+
100+
{/* Camera submenu */}
101+
<Menu.SubmenuRoot>
102+
<Menu.SubmenuTrigger className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200">Camera</Menu.SubmenuTrigger>
103+
<Menu.Portal container={portalContainer}>
104+
<Menu.Positioner sideOffset={6} className="z-90">
105+
<Menu.Popup className="min-w-44 rounded border border-white/10 bg-[#0b0e13]/95 shadow-lg py-1 text-xs z-90" style={{ zIndex: 10050 }}>
106+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddCamera('perspective'); closeIfControlled(); }}>Perspective</Menu.Item>
107+
<Menu.Item className="w-full text-left px-3 py-1.5 hover:bg-white/10 text-gray-200" onClick={() => { onAddCamera('orthographic'); closeIfControlled(); }}>Orthographic</Menu.Item>
108+
</Menu.Popup>
109+
</Menu.Positioner>
110+
</Menu.Portal>
111+
</Menu.SubmenuRoot>
112+
</Menu.Popup>
113+
</Menu.Positioner>
114+
</Menu.Portal>
115+
</Menu.Root>
116+
);
117+
};
118+
119+
export default AddObjectMenu;

0 commit comments

Comments
 (0)