11import { platform } from "os" ;
22
3- import { Mesh , Tools , SubMesh } from "babylonjs" ;
4-
53import { Component , PropsWithChildren , ReactNode } from "react" ;
64
75import { AiOutlinePlus , AiOutlineClose } from "react-icons/ai" ;
86
7+ import { Mesh , SubMesh , Node , InstancedMesh } from "babylonjs" ;
8+
99import {
1010 ContextMenu ,
1111 ContextMenuContent ,
@@ -19,21 +19,27 @@ import {
1919 ContextMenuCheckboxItem ,
2020} from "../../../ui/shadcn/ui/context-menu" ;
2121
22+ import { showConfirm } from "../../../ui/dialog" ;
23+ import { Separator } from "../../../ui/shadcn/ui/separator" ;
2224import { SceneAssetBrowserDialogMode , showAssetBrowserDialog } from "../../../ui/scene-asset-browser" ;
2325
2426import { getMeshCommands } from "../../dialogs/command-palette/mesh" ;
2527import { getLightCommands } from "../../dialogs/command-palette/light" ;
2628
2729import { isSound } from "../../../tools/guards/sound" ;
30+ import { cloneNode , ICloneNodeOptions } from "../../../tools/node/clone" ;
2831import { reloadSound } from "../../../tools/sound/tools" ;
2932import { registerUndoRedo } from "../../../tools/undoredo" ;
33+ import { waitNextAnimationFrame } from "../../../tools/tools" ;
34+ import { createMeshInstance } from "../../../tools/mesh/instance" ;
3035import { isScene , isSceneLinkNode } from "../../../tools/guards/scene" ;
31- import { UniqueNumber , waitNextAnimationFrame } from "../../../tools/tools" ;
3236import { isAbstractMesh , isMesh , isNode } from "../../../tools/guards/nodes" ;
3337import { isNodeLocked , isNodeSerializable , setNodeLocked , setNodeSerializable } from "../../../tools/node/metadata" ;
3438
3539import { addGPUParticleSystem , addParticleSystem } from "../../../project/add/particles" ;
3640
41+ import { EditorInspectorSwitchField } from "../inspector/fields/switch" ;
42+
3743import { Editor } from "../../main" ;
3844
3945import { removeNodes } from "./remove" ;
@@ -65,6 +71,10 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
6571
6672 { ! isScene ( this . props . object ) && ! isSound ( this . props . object ) && (
6773 < >
74+ < ContextMenuItem onClick = { ( ) => this . _cloneNode ( this . props . object ) } > Clone</ ContextMenuItem >
75+
76+ < ContextMenuSeparator />
77+
6878 < ContextMenuItem onClick = { ( ) => this . props . editor . layout . graph . copySelectedNodes ( ) } >
6979 Copy < ContextMenuShortcut > { platform ( ) === "darwin" ? "⌘+C" : "CTRL+C" } </ ContextMenuShortcut >
7080 </ ContextMenuItem >
@@ -179,8 +189,10 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
179189 { isMesh ( this . props . object ) && (
180190 < >
181191 < ContextMenuSeparator />
192+
182193 < ContextMenuItem onClick = { ( ) => this . _createMeshInstance ( this . props . object ) } > Create Instance</ ContextMenuItem >
183194
195+ < ContextMenuSeparator />
184196 < ContextMenuItem onClick = { ( ) => this . _updateMeshGeometry ( this . props . object ) } > Update Geometry...</ ContextMenuItem >
185197 </ >
186198 ) }
@@ -189,32 +201,92 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
189201 }
190202
191203 private _createMeshInstance ( mesh : Mesh ) : void {
192- const instance = mesh . createInstance ( `${ mesh . name } (Mesh Instance)` ) ;
193- instance . id = Tools . RandomId ( ) ;
194- instance . uniqueId = UniqueNumber . Get ( ) ;
195- instance . parent = mesh . parent ;
196- instance . position . copyFrom ( mesh . position ) ;
197- instance . rotation . copyFrom ( mesh . rotation ) ;
198- instance . scaling . copyFrom ( mesh . scaling ) ;
199- instance . rotationQuaternion = mesh . rotationQuaternion ?. clone ( ) ?? null ;
200- instance . isVisible = mesh . isVisible ;
201- instance . setEnabled ( mesh . isEnabled ( ) ) ;
202-
203- const lights = this . props . editor . layout . preview . scene . lights ;
204- const shadowMaps = lights . map ( ( light ) => light . getShadowGenerator ( ) ?. getShadowMap ( ) ) . filter ( ( s ) => s ) ;
205-
206- shadowMaps . forEach ( ( shadowMap ) => {
207- if ( shadowMap ?. renderList ?. includes ( mesh ) ) {
208- shadowMap . renderList . push ( instance ) ;
209- }
204+ let instance : InstancedMesh | null = null ;
205+
206+ registerUndoRedo ( {
207+ executeRedo : true ,
208+ action : ( ) => {
209+ this . props . editor . layout . graph . refresh ( ) ;
210+
211+ waitNextAnimationFrame ( ) . then ( ( ) => {
212+ if ( instance ) {
213+ this . props . editor . layout . graph . setSelectedNode ( instance ) ;
214+ this . props . editor . layout . animations . setEditedObject ( instance ) ;
215+ }
216+
217+ this . props . editor . layout . inspector . setEditedObject ( instance ) ;
218+ this . props . editor . layout . preview . gizmo . setAttachedNode ( instance ) ;
219+ } ) ;
220+ } ,
221+ undo : ( ) => {
222+ instance ?. dispose ( false , false ) ;
223+ instance = null ;
224+ } ,
225+ redo : ( ) => {
226+ instance = createMeshInstance ( this . props . editor , mesh ) ;
227+ } ,
210228 } ) ;
229+ }
230+
231+ private async _cloneNode ( node : any ) : Promise < void > {
232+ let clone : Node | null = null ;
233+
234+ const cloneOptions : ICloneNodeOptions = {
235+ shareGeometry : true ,
236+ shareSkeleton : true ,
237+ cloneMaterial : true ,
238+ cloneThinInstances : true ,
239+ } ;
240+
241+ const allNodes = isNode ( node ) ? [ node , ...node . getDescendants ( false ) ] : [ node ] ;
242+ if ( allNodes . find ( ( node ) => isMesh ( node ) ) ) {
243+ const result = await showConfirm (
244+ "Clone options" ,
245+ < div className = "flex flex-col gap-2" >
246+ < Separator />
247+
248+ < div className = "text-muted font-semibold" > Options for meshes</ div >
249+
250+ < div className = "flex flex-col" >
251+ < EditorInspectorSwitchField object = { cloneOptions } property = "shareGeometry" label = "Share Geometry" />
252+ < EditorInspectorSwitchField object = { cloneOptions } property = "shareSkeleton" label = "Share Skeleton" />
253+ < EditorInspectorSwitchField object = { cloneOptions } property = "cloneMaterial" label = "Clone Material" />
254+ < EditorInspectorSwitchField object = { cloneOptions } property = "cloneThinInstances" label = "Clone Thin Instances" />
255+ </ div >
256+ </ div > ,
257+ {
258+ asChild : true ,
259+ confirmText : "Clone" ,
260+ }
261+ ) ;
262+
263+ if ( ! result ) {
264+ return ;
265+ }
266+ }
267+
268+ registerUndoRedo ( {
269+ executeRedo : true ,
270+ action : ( ) => {
271+ this . props . editor . layout . graph . refresh ( ) ;
211272
212- this . props . editor . layout . graph . refresh ( ) ;
273+ waitNextAnimationFrame ( ) . then ( ( ) => {
274+ if ( clone ) {
275+ this . props . editor . layout . graph . setSelectedNode ( clone ) ;
276+ this . props . editor . layout . animations . setEditedObject ( clone ) ;
277+ }
213278
214- waitNextAnimationFrame ( ) . then ( ( ) => {
215- this . props . editor . layout . graph . setSelectedNode ( instance ) ;
216- this . props . editor . layout . inspector . setEditedObject ( instance ) ;
217- this . props . editor . layout . preview . gizmo . setAttachedNode ( instance ) ;
279+ this . props . editor . layout . inspector . setEditedObject ( clone ) ;
280+ this . props . editor . layout . preview . gizmo . setAttachedNode ( clone ) ;
281+ } ) ;
282+ } ,
283+ undo : ( ) => {
284+ clone ?. dispose ( false , false ) ;
285+ clone = null ;
286+ } ,
287+ redo : ( ) => {
288+ clone = cloneNode ( this . props . editor , node , cloneOptions ) ;
289+ } ,
218290 } ) ;
219291 }
220292
0 commit comments