Skip to content

Commit

Permalink
fix deleting entities from scenes
Browse files Browse the repository at this point in the history
  • Loading branch information
preaction committed Jul 13, 2024
1 parent dfbca0e commit 3cdfea4
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 239 deletions.
38 changes: 26 additions & 12 deletions editor/src/components/EntityPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,20 +156,24 @@ export default defineComponent({
this.update();
},
deleteEntity(entityData: EntityData) {
deleteEntity(path: string) {
const entityData = this.getEntityDataByPath(path);
if (confirm(`Are you sure you want to delete "${entityData.name}"?`)) {
if (this.selectedEntityData === entityData) {
this.select(this.entities[0], this.entities[0].name);
}
for (let i = 0; i < this.entities.length; i++) {
if (this.entities[i].path === entityData.path) {
this.entities.splice(i, 1);
break;
this.visitEntityDataPath(path, (entityData, parentData) => {
if (parentData) {
parentData.children.splice(parentData.children.indexOf(entityData), 1);
}
else {
this.entities.splice(this.entities.indexOf(entityData), 1);
}
});
const entity = this.scene.getEntityByPath(path);
if (entity) {
this.scene.removeEntity(entity.id);
}
const entity = this.scene.getEntityByPath(entityData.path);
this.scene.removeEntity(entity.id);
(this.$refs.tree as typeof Tree).removeNode(entityData);
this.update();
}
},
Expand Down Expand Up @@ -309,18 +313,28 @@ export default defineComponent({
},
getEntityDataByPath(path: string) {
return this.visitEntityDataPath(path, (e) => e);
},
visitEntityDataPath<T>(path: string, cb: (entityData: EntityData, parentEntityData: EntityData | null) => T): T | null {
const pathParts = path.split(/\//);
let parentNode: EntityData | null = null;
let children = this.entities;
for (let i = 0; i < pathParts.length; i++) {
let leafNode = children.find(node => node.name === pathParts[i]);
if (!leafNode) {
return null;
break;
}
if (i === pathParts.length - 1) {
return leafNode;
return cb(leafNode, parentNode);
}
if (!leafNode.children || !leafNode.children.length) {
break;
}
children = leafNode.children;
parentNode = leafNode;
}
return null;
},
updateEntityName() {
Expand Down Expand Up @@ -390,15 +404,15 @@ export default defineComponent({
<div class="scene-tree">
<Tree ref="tree" v-for="entityData in entities" :key="entityData.name" :node="entityData" :onclick="select"
:ondragstart="dragStart" :ondragover="dragOverEntity" :ondrop="dropEntity" default-type="Entity">
<template #menu="{ node: entityData }">
<template #menu="{ node: entityData, path }">
<MenuButton>
<template #button>
<i data-test="entity-menu" class="fa-solid fa-ellipsis-vertical scene-tree-item-menu-button"></i>
</template>
<ul>
<li @click.stop.prevent="createPrefab(entityData)">Create Prefab</li>
<li data-test="duplicate" @click.stop.prevent="duplicateEntity(entityData)">Duplicate</li>
<li @click.stop.prevent="deleteEntity(entityData)">Delete</li>
<li data-test="delete" @click.stop.prevent="deleteEntity(path)">Delete</li>
</ul>
</MenuButton>
</template>
Expand Down
117 changes: 59 additions & 58 deletions editor/src/components/MenuButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,33 @@ import { computePosition, autoUpdate, flip, shift } from '@floating-ui/dom';
// The currently open menu, if any, so we can close it before showing
// another one
let menuOpen:any;
let menuOpen: any;
export default defineComponent({
props: [ 'title', 'show', 'placement' ],
props: ['title', 'show', 'placement'],
mounted() {
if ( this.$props.show ) {
if (this.$props.show) {
this.open();
}
},
methods: {
update() {
computePosition(this.$refs['button'], this.$refs['menu'], {
placement: this.placement || 'right-start',
middleware: [ flip(), shift() ],
}).then(({x, y}) => {
middleware: [flip(), shift()],
}).then(({ x, y }) => {
Object.assign(this.$refs['menu'].style, {
left: `${x}px`,
top: `${y}px`,
});
});
},
open() {
if ( this.closeHandler ) {
if (this.closeHandler) {
this.close();
return;
}
if ( menuOpen && menuOpen !== this ) {
if (menuOpen && menuOpen !== this) {
menuOpen.close();
}
this.$refs['menu'].style.display = 'block';
Expand All @@ -39,19 +39,19 @@ export default defineComponent({
this.update();
});
this.closeHandler = this.close.bind(this);
this.$nextTick( () => document.body.addEventListener( 'click', this.closeHandler ) );
this.$nextTick(() => document.body.addEventListener('click', this.closeHandler));
menuOpen = this;
},
close() {
if ( this.$refs['menu'] ) {
if (this.$refs['menu']) {
// The menu may already be gone because the element has been
// removed
this.$refs['menu'].style.display = '';
}
this.cleanup();
document.body.removeEventListener( 'click', this.closeHandler );
document.body.removeEventListener('click', this.closeHandler);
this.closeHandler = null;
this.$emit( 'close' );
this.$emit('close');
menuOpen = null;
},
},
Expand All @@ -63,66 +63,67 @@ export default defineComponent({
<div class="menu-button-slot" ref="button" @click.stop="open">
<slot name="button">
<button class="menu-button" :title="title">
<slot name="title">{{title}}</slot>
<slot name="title">{{ title }}</slot>
</button>
</slot>
</div>
<div ref="menu" role="menu" class="menu">
<slot/>
<slot />
</div>
</div>
</template>

<style>
.menu {
display: none;
position: absolute;
width: max-content;
top: 0;
left: 0;
background: var(--bw-background-color);
border-radius: 5px;
border: 3px solid var(--bw-border-color);
color: var(--bw-color);
z-index: 10000;
box-shadow: var(--bw-box-shadow);
}
.menu {
display: none;
position: absolute;
width: max-content;
top: 0;
left: 0;
background: var(--bw-background-color);
border-radius: 5px;
border: 3px solid var(--bw-border-color);
color: var(--bw-color);
z-index: 10000;
box-shadow: var(--bw-box-shadow);
}
.menu ul {
list-style: none;
margin: 0;
padding: 0;
}
.menu ul {
list-style: none;
margin: 0;
padding: 0;
}
.menu li {
padding: 0.2em;
display: block;
}
.menu li {
padding: 0.2em;
display: block;
}
.menu hr {
margin: 0;
padding: 0;
height: 0.3em;
border: none;
background: var(--bw-border-color);
}
.menu hr {
margin: 0;
padding: 0;
height: 0.3em;
border: none;
background: var(--bw-border-color);
}
.menu li:hover {
cursor: pointer;
color: var(--bw-color-hover);
background: var(--bw-background-color-hover);
}
.menu li:hover {
cursor: pointer;
color: var(--bw-color-hover);
background: var(--bw-background-color-hover);
}
.menu .disabled {
pointer-events: none;
color: var(--bw-color-disabled);
}
.menu .disabled {
pointer-events: none;
color: var(--bw-color-disabled);
}
.menu li.hr {
padding: 0;
}
.menu li.hr:hover {
cursor: default;
background: transparent;
}
.menu li.hr {
padding: 0;
}
.menu li.hr:hover {
cursor: default;
background: transparent;
}
</style>
6 changes: 3 additions & 3 deletions editor/src/components/Tree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,15 @@ defineExpose({
<span data-test="name">{{ props.node.name }}</span>
</span>
<span class="asset-tree-item__menu">
<slot name="menu" :node="node" />
<slot name="menu" :node="node" :path="nodePath" />
</span>
</a>
<div v-if="hasChildren && showChildren" class="children">
<div v-for="child in props.node.children" :key="nodePath + '/' + child.name">
<Tree ref="childTrees" :onclick="props.onclick" :ondblclick="props.ondblclick" :ondragstart="props.ondragstart"
:ondragover="props.ondragover" :ondrop="props.ondrop" :node="child" :dirname="nodePath">
<template #menu="{ child: node }">
<slot name="menu" :node="node" />
<template #menu="{ node, path }">
<slot name="menu" :node="node" :path="path" />
</template>
</Tree>
</div>
Expand Down
55 changes: 54 additions & 1 deletion editor/test/unit/src/components/SceneEdit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ beforeAll(() => {
dispatchEvent: jest.fn(),
})),
});

global.ResizeObserver = class ResizeObserver {
observe() {
// do nothing
}
unobserve() {
// do nothing
}
disconnect() {
// do nothing
}
};
});

const stubs = {
Expand Down Expand Up @@ -162,7 +174,7 @@ describe('SceneEdit', () => {
const asset = new Asset(new Load(), "OldScene.json");
asset.data = sceneData;
modelValue = new Tab(project, asset);
mockReadItemData.mockReturnValue(Promise.resolve(JSON.stringify(sceneData)));
mockReadItemData.mockImplementation(() => Promise.resolve(JSON.stringify(sceneData)));
mockWriteItemData.mockReturnValue(Promise.resolve());
});

Expand Down Expand Up @@ -347,6 +359,47 @@ describe('SceneEdit', () => {
expect.objectContaining({ edited: true }),
);
});

test('delete an entity', async () => {
sceneData.entities.push({
name: 'Entity 2',
components: {
Transform: {},
Sprite: {},
},
});
sceneData.entities.push({
name: 'Entity 3',
components: {
Transform: {},
Sprite: {},
},
});

const onUpdate = jest.fn();
const wrapper = mount(SceneEdit, {
attachTo: document.body,
props: {
modelValue,
onUpdate,
},
global: { provide },
});
wrapper.setData({ gameClass: Game });
await wrapper.vm.$nextTick();
await flushPromises();

const confirmSpy = jest.spyOn(global, 'confirm').mockReturnValue(true);
await wrapper.get(`[data-path="${sceneData.entities[1].name}"] [data-test=delete]`).trigger('click');
expect(confirmSpy).toHaveBeenCalled();
expect(onUpdate).toHaveBeenCalled();

const newSceneData = wrapper.vm.sceneData;
expect(newSceneData?.entities).toEqual([sceneData.entities[0], sceneData.entities[2]]);

const editScene = wrapper.vm.editScene;
expect(editScene.getEntityByPath(sceneData.entities[1].name)).toBeFalsy();
});
});

});
Expand Down
Loading

0 comments on commit 3cdfea4

Please sign in to comment.