diff --git a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.css b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.css
index 586605cc7..e820761ad 100644
--- a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.css
+++ b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.css
@@ -23,10 +23,17 @@
}
.Gizmos .gizmo.scale {
+ margin-left: 1px;
+ margin-right: 0;
+ border-radius: 0;
+ background-image: url(./icons/scale.svg);
+}
+
+.Gizmos .gizmo.free {
margin-left: 1px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
- background-image: url(./icons/scale.svg);
+ background-image: url(./icons/free.svg);
}
.Gizmos .open-panel {
@@ -86,4 +93,4 @@
.Gizmos .panel .alignment.disabled .icon {
cursor: not-allowed;
opacity: 0.5;
-}
\ No newline at end of file
+}
diff --git a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx
index 4d10b369c..5ade57a44 100644
--- a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx
+++ b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx
@@ -41,13 +41,12 @@ export const Gizmos = withSdk(({ sdk }) => {
[selection, setSelection]
)
- useHotkey(['M'], handlePositionGizmo)
- useHotkey(['R'], handleRotationGizmo)
- useHotkey(['X'], handleScaleGizmo)
+ const handleFreeGizmo = useCallback(() => setSelection({ gizmo: GizmoType.FREE }), [selection, setSelection])
useHotkey(['M'], handlePositionGizmo)
useHotkey(['R'], handleRotationGizmo)
useHotkey(['X'], handleScaleGizmo)
+ useHotkey(['F'], handleFreeGizmo)
const {
isPositionGizmoWorldAligned,
@@ -89,6 +88,12 @@ export const Gizmos = withSdk(({ sdk }) => {
onClick={handleScaleGizmo}
title="Scaling tool"
/>
+
diff --git a/packages/@dcl/inspector/src/components/Toolbar/Gizmos/icons/free.svg b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/icons/free.svg
new file mode 100644
index 000000000..dcbe9d1ca
--- /dev/null
+++ b/packages/@dcl/inspector/src/components/Toolbar/Gizmos/icons/free.svg
@@ -0,0 +1,6 @@
+
diff --git a/packages/@dcl/inspector/src/lib/sdk/nodes.ts b/packages/@dcl/inspector/src/lib/sdk/nodes.ts
index f25a17a53..45e538b6c 100644
--- a/packages/@dcl/inspector/src/lib/sdk/nodes.ts
+++ b/packages/@dcl/inspector/src/lib/sdk/nodes.ts
@@ -25,7 +25,7 @@ export function getRoot(entity: Entity, nodes: DeepReadonlyObject
) {
return root
}
-function getNodes(engine: IEngine): readonly DeepReadonlyObject[] {
+export function getNodes(engine: IEngine): readonly DeepReadonlyObject[] {
const Nodes = engine.getComponent(EditorComponentNames.Nodes) as EditorComponents['Nodes']
return Nodes.getOrNull(engine.RootEntity)?.value || []
}
diff --git a/packages/@dcl/inspector/src/lib/sdk/operations/add-child.spec.ts b/packages/@dcl/inspector/src/lib/sdk/operations/add-child.spec.ts
new file mode 100644
index 000000000..91722a6bb
--- /dev/null
+++ b/packages/@dcl/inspector/src/lib/sdk/operations/add-child.spec.ts
@@ -0,0 +1,145 @@
+import { Engine, Entity, IEngine } from '@dcl/ecs'
+
+import addChild, { generateUniqueName, getSuffixDigits } from './add-child'
+import { createComponents, createEditorComponents, SdkComponents } from '../components'
+
+describe('generateUniqueName', () => {
+ let engine: IEngine
+ let Name: SdkComponents['Name']
+ let _addChild: (parent: Entity, name: string) => Entity
+
+ beforeEach(() => {
+ engine = Engine()
+ const coreComponents = createComponents(engine)
+ createEditorComponents(engine)
+ Name = coreComponents.Name
+ _addChild = addChild(engine)
+ })
+
+ it('should return the base name when there are no existing nodes', () => {
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName')
+ })
+
+ it('should return the base name with _1 when the base name already exists', () => {
+ _addChild(engine.RootEntity, 'SomeName')
+
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName_1')
+ })
+
+ it('should return the base name with the next incremented suffix', () => {
+ _addChild(engine.RootEntity, 'SomeName')
+ _addChild(engine.RootEntity, 'SomeName_1')
+ _addChild(engine.RootEntity, 'SomeName_2')
+
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName_3')
+ })
+
+ it('should not match names with multiple underscore-separated numbers', () => {
+ _addChild(engine.RootEntity, 'SomeName_123_1')
+
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName')
+ })
+
+ it('should handle mixed case names correctly', () => {
+ _addChild(engine.RootEntity, 'someName')
+ _addChild(engine.RootEntity, 'SomeName_1')
+
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName_2')
+ })
+
+ it('should return the base name with _1 when the base name exists in mixed case', () => {
+ _addChild(engine.RootEntity, 'SomeName')
+ _addChild(engine.RootEntity, 'SOMENAME_1')
+
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName_2')
+ })
+
+ it('should handle names with an underscore and valid digits', () => {
+ _addChild(engine.RootEntity, 'SomeName_1')
+ _addChild(engine.RootEntity, 'SomeName_2')
+ _addChild(engine.RootEntity, 'SomeName_3')
+
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName_4')
+ })
+
+ it('should handle names with only digits after underscore', () => {
+ _addChild(engine.RootEntity, 'SomeName')
+ _addChild(engine.RootEntity, 'SomeName_10')
+
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName_11')
+ })
+
+ it('should handle names with no digits after underscore', () => {
+ _addChild(engine.RootEntity, 'SomeName_')
+
+ const result = generateUniqueName(engine, Name, 'SomeName')
+
+ expect(result).toBe('SomeName')
+ })
+})
+
+describe('getSuffixDigits', () => {
+ it('should return -1 if there is no underscore', () => {
+ expect(getSuffixDigits('SomeName')).toBe(-1)
+ })
+
+ it('should return -1 if the string ends with an underscore', () => {
+ expect(getSuffixDigits('SomeName_')).toBe(-1)
+ })
+
+ it('should return the digits after the last underscore if they are valid numbers', () => {
+ expect(getSuffixDigits('SomeName_123')).toBe(123)
+ })
+
+ it('should return -1 if the part after the last underscore is not a valid number', () => {
+ expect(getSuffixDigits('SomeName_abc')).toBe(-1)
+ })
+
+ it('should return -1 if there is only one underscore without digits', () => {
+ expect(getSuffixDigits('SomeName_')).toBe(-1)
+ })
+
+ it('should return the correct number for multiple underscores with valid digits at the end', () => {
+ expect(getSuffixDigits('SomeName_1_2_3')).toBe(3)
+ })
+
+ it('should return -1 if there are multiple underscores and the last part is not a valid number', () => {
+ expect(getSuffixDigits('SomeName_1_2_abc')).toBe(-1)
+ })
+
+ it('should return -1 for an empty string', () => {
+ expect(getSuffixDigits('')).toBe(-1)
+ })
+
+ it('should return -1 if the string is just an underscore', () => {
+ expect(getSuffixDigits('_')).toBe(-1)
+ })
+
+ it('should return -1 if there are no digits after the underscore', () => {
+ expect(getSuffixDigits('SomeName_')).toBe(-1)
+ })
+
+ it('should handle strings with digits but no underscore correctly', () => {
+ expect(getSuffixDigits('123')).toBe(-1)
+ })
+
+ it('should return -1 if there are multiple underscores with no digits after the last underscore', () => {
+ expect(getSuffixDigits('SomeName_1_')).toBe(-1)
+ })
+})
diff --git a/packages/@dcl/inspector/src/lib/sdk/operations/add-child.ts b/packages/@dcl/inspector/src/lib/sdk/operations/add-child.ts
index c80ad71df..d59c7db9e 100644
--- a/packages/@dcl/inspector/src/lib/sdk/operations/add-child.ts
+++ b/packages/@dcl/inspector/src/lib/sdk/operations/add-child.ts
@@ -1,6 +1,7 @@
-import { Entity, IEngine, Transform as TransformEngine, Name as NameEngine } from '@dcl/ecs'
+import { Entity, IEngine, Transform as TransformEngine, Name as NameEngine, NameComponent } from '@dcl/ecs'
+
import { EditorComponentNames, EditorComponents } from '../components'
-import { pushChild } from '../nodes'
+import { getNodes, pushChild } from '../nodes'
export function addChild(engine: IEngine) {
return function addChild(parent: Entity, name: string): Entity {
@@ -10,7 +11,7 @@ export function addChild(engine: IEngine) {
const Name = engine.getComponent(NameEngine.componentName) as typeof NameEngine
// create new child components
- Name.create(child, { value: name })
+ Name.create(child, { value: generateUniqueName(engine, Name, name) })
Transform.create(child, { parent })
// update Nodes component
Nodes.createOrReplace(engine.RootEntity, { value: pushChild(engine, parent, child) })
@@ -19,4 +20,34 @@ export function addChild(engine: IEngine) {
}
}
+export function generateUniqueName(engine: IEngine, Name: NameComponent, value: string): string {
+ const pattern = new RegExp(`^${value.toLowerCase()}(_\\d+)?$`, 'i')
+ const nodes = getNodes(engine)
+
+ let isFirst = true
+ let max = 0
+ for (const $ of nodes) {
+ const name = (Name.getOrNull($.entity)?.value || '').toLowerCase()
+ if (pattern.test(name)) {
+ isFirst = false
+ const suffix = getSuffixDigits(name)
+ if (suffix !== -1) {
+ max = Math.max(max, suffix)
+ }
+ }
+ }
+
+ const suffix = isFirst ? '' : `_${max + 1}`
+
+ return `${value}${suffix}`
+}
+
+export function getSuffixDigits(name: string): number {
+ const underscoreIndex = name.lastIndexOf('_')
+ if (underscoreIndex === -1 || underscoreIndex === name.length - 1) return -1
+
+ const digits = name.slice(underscoreIndex + 1)
+ return /^\d+$/.test(digits) ? parseInt(digits, 10) : -1
+}
+
export default addChild
diff --git a/packages/@dcl/inspector/src/lib/sdk/operations/duplicate-entity.spec.ts b/packages/@dcl/inspector/src/lib/sdk/operations/duplicate-entity.spec.ts
index e4b8f5ef0..f949e45d3 100644
--- a/packages/@dcl/inspector/src/lib/sdk/operations/duplicate-entity.spec.ts
+++ b/packages/@dcl/inspector/src/lib/sdk/operations/duplicate-entity.spec.ts
@@ -59,7 +59,7 @@ describe('duplicateEntity', () => {
expect(duplicateChild).not.toBe(original)
expect(duplicateChild).not.toBe(originalChild)
expect(duplicateChild).not.toBe(duplicate)
- expect(NameComponent.get(duplicateChild!).value).toBe(NameComponent.get(originalChild).value)
+ expect(NameComponent.get(duplicateChild!).value).toBe(`${NameComponent.get(originalChild).value}_1`)
expect(Nodes.get(ROOT).value).toStrictEqual(nodesAfter)
})
})
diff --git a/packages/@dcl/inspector/src/lib/sdk/operations/duplicate-entity.ts b/packages/@dcl/inspector/src/lib/sdk/operations/duplicate-entity.ts
index 4860b9baa..685b17d7a 100644
--- a/packages/@dcl/inspector/src/lib/sdk/operations/duplicate-entity.ts
+++ b/packages/@dcl/inspector/src/lib/sdk/operations/duplicate-entity.ts
@@ -1,14 +1,22 @@
-import { Entity, IEngine, Transform as TransformEngine, NetworkEntity as NetworkEntityEngine } from '@dcl/ecs'
+import {
+ Entity,
+ IEngine,
+ Transform as TransformEngine,
+ NetworkEntity as NetworkEntityEngine,
+ Name as NameEngine
+} from '@dcl/ecs'
import { clone } from '@dcl/asset-packs'
import { EditorComponentNames, EditorComponents } from '../components'
import { pushChild } from '../nodes'
import updateSelectedEntity from './update-selected-entity'
+import { generateUniqueName } from './add-child'
export function duplicateEntity(engine: IEngine) {
return function duplicateEntity(entity: Entity) {
const Transform = engine.getComponent(TransformEngine.componentId) as typeof TransformEngine
const Nodes = engine.getComponent(EditorComponentNames.Nodes) as EditorComponents['Nodes']
const Triggers = engine.getComponent(EditorComponentNames.Triggers) as EditorComponents['Triggers']
+ const Name = engine.getComponent(NameEngine.componentName) as typeof NameEngine
const NetworkEntity = engine.getComponent(NetworkEntityEngine.componentId) as typeof NetworkEntityEngine
const { entities } = clone(entity, engine as any, Transform as any, Triggers as any) as {
@@ -20,6 +28,9 @@ export function duplicateEntity(engine: IEngine) {
NetworkEntity.createOrReplace(duplicate, { entityId: duplicate, networkId: 0 })
}
+ const originalName = Name.getOrNull(original)?.value
+ Name.createOrReplace(duplicate, { value: generateUniqueName(engine, Name, originalName || '') })
+
const transform = Transform.getMutableOrNull(duplicate)
if (transform === null || !transform.parent) {
Nodes.createOrReplace(engine.RootEntity, { value: pushChild(engine, engine.RootEntity, duplicate) })