Skip to content

Commit

Permalink
Merge pull request #4 from gharielsl/object-tree
Browse files Browse the repository at this point in the history
Object tree
  • Loading branch information
gharielsl authored Dec 14, 2024
2 parents dd0ff66 + c2085e7 commit b5a6c93
Show file tree
Hide file tree
Showing 25 changed files with 4,951 additions and 101 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

Expand Down
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
## Overview
Visit live demo - [Github Pages](https://gharielsl.github.io/voxel-mesh-editor).

The **Voxel Mesh Editor** is a tool for creating and editing voxel-based 3D models.

It allows to import and voxelify 3D models, sculpt with voxels, and to smooth out voxel meshes.

### Current and future features:
- Object mode
- [x] Copy paste
- [x] Transform
- [ ] Undo
- [ ] Fly controls
- Voxel mode
- [x] Add/Remove
- [x] Spray
- [ ] Select
- Scene tree and property editor
- [x] Hide/Show/Select
- [x] March cubes
- [x] Transform
- [ ] Voxelify mesh
- Materials
- [ ] Selection
- [ ] Multiple groups
- [ ] Voxel mesh shader
- Import/Export
- [ ] Save/Open
- [ ] Import
- Misc
- [ ] Measuring
- [ ] First person walk
- [ ] 3d annotations

## Controls

### General
- **Wheel Mouse Button + Drag**: Orbit.
- **Left Mouse Button + Drag**: Fly.
- **Left Mouse Button + WASD**: Fly - move.
- **Right Mouse Button + Drag**: Span.
- **Tab**: Switch between object mode and voxel mode.

### Object Mode
- **T**: Translate.
- **R**: Rotate.
- **G**: Scale.
- **F**: Select.
- **Delete**: Delete selected objects.
- **Ctrl + C**: Copy selected objects.
- **Ctrl + V**: Paste the copied object.

### Voxel Mode
- **Left Click**: Add a voxel.
- **Right Click**: Remove a voxel.
- **Ctrl + Drag**: Spray voxels.

## Voxelifying Imported Mesh
- On the menu bar select `File`.
- Import GLB/GLTF.
- Select your file.
- On the properties panel in the bottom right click `convert to voxel mesh`.

## Smoothing Voxel Meshes
- Select the mesh.
- On the properties panel tick `march cubes`.
- Tick `smooth`.
Binary file added dist/assets/bootstrap-icons-BOrJxbIo.woff
Binary file not shown.
Binary file added dist/assets/bootstrap-icons-BtvjY1KL.woff2
Binary file not shown.
5 changes: 5 additions & 0 deletions dist/assets/index-D1Nu9pvv.css

Large diffs are not rendered by default.

4,504 changes: 4,504 additions & 0 deletions dist/assets/index-DQAIsfz8.js

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions dist/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" crossorigin src="/assets/index-DQAIsfz8.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D1Nu9pvv.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
Expand Down
Binary file added dist/mesh/rotate_mesh.glb
Binary file not shown.
Binary file added dist/mesh/scale_mesh.glb
Binary file not shown.
Binary file added dist/mesh/translate_mesh.glb
Binary file not shown.
25 changes: 0 additions & 25 deletions package.json.disable

This file was deleted.

9 changes: 6 additions & 3 deletions src/components/input/Brush.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export default defineComponent({
ev.clientY <= rect.bottom;
}
},
setBrushShape(shape: string) {
state.brushShape = shape;
},
inputChange(event: any) {
state.brushSize = +event.target.value;
}
Expand Down Expand Up @@ -51,16 +54,16 @@ export default defineComponent({
</div>
<p style="margin: 4px;margin-bottom:0;text-align: left;">Size</p>
<div style="display: flex;margin-left: 4px;margin-right:4px;align-items: center;">
<input @mousemove="inputChange" :value="state.brushSize" min="1" max="64" class="custom-range" type="range" style="flex: 1">
<input @mousemove="inputChange" :value="state.brushSize" :min="1" max="16" class="custom-range" type="range" style="flex: 1">
<span style="height:100%;width:18px;text-align:center;color:var(--color-text-disabled)">{{ state.brushSize }}</span>
<span style="height:100%;margin-bottom: 0.1em;color:var(--color-text-disabled)">px</span>
</div>
<p style="margin: 4px;margin-bottom:0;text-align: left;">Shape</p>
<div style="display: flex;width: 100%;justify-content: space-around;flex:1;align-items: center;">
<div @click="state.brushShape = 'square'" class="brush-type" :style="`background-color: var(${state.brushShape === 'square' ? '--color-secondary' : '--color-foreground-2'});`">
<div @click="setBrushShape('square')" class="brush-type" :style="`background-color: var(${state.brushShape === 'square' ? '--color-secondary' : '--color-foreground-2'});`">
<i class="bi bi-square-fill"></i>
</div>
<div @click="state.brushShape = 'round'" class="brush-type" :style="`background-color: var(${state.brushShape === 'round' ? '--color-secondary' : '--color-foreground-2'});`">
<div @click="setBrushShape('round')" class="brush-type" :style="`background-color: var(${state.brushShape === 'round' ? '--color-secondary' : '--color-foreground-2'});`">
<i class="bi bi-circle-fill"></i>
</div>
</div>
Expand Down
15 changes: 8 additions & 7 deletions src/components/panel/MenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ import TransformationContext from '../../core/TransformationContext';
return {
mouseInFile: false,
mouseInEdit: false,
mouseInAdd: false
mouseInAdd: false,
state
}
}
});
Expand All @@ -62,19 +63,19 @@ import TransformationContext from '../../core/TransformationContext';
File
</div>
<div v-if="mouseInFile" class="menu-list">
<div class="menu-list-item">AAA</div>
<div class="menu-list-item">AAA</div>
<div class="menu-list-item">AAA</div>
<div class="menu-list-item">Open</div>
<div class="menu-list-item">Save</div>
<div class="menu-list-item">Import</div>
<div class="menu-list-item">Export</div>
</div>
</div>
<div @mouseenter="mouseIn('mouseInEdit')" @mouseleave="mouseOut('mouseInEdit')" class="menu-item">
<div :class="'menu-item-button ' + (mouseInEdit ? 'menu-item-button-open' : '')">
Edit
</div>
<div v-if="mouseInEdit" class="menu-list">
<div class="menu-list-item">AAA</div>
<div class="menu-list-item">AAA</div>
<div class="menu-list-item">AAA</div>
<div @click="state.renderingContext?.copy();mouseInEdit = false" class="menu-list-item">Copy (Ctrl + C)</div>
<div @click="state.renderingContext?.paste();mouseInEdit = false" class="menu-list-item">Paste (Ctrl + v)</div>
</div>
</div>
<div @mouseenter="mouseIn('mouseInAdd')" @mouseleave="mouseOut('mouseInAdd')" class="menu-item">
Expand Down
2 changes: 1 addition & 1 deletion src/components/panel/ModeBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<div class="mode-bar-item-select">
<div @click="state.setCurrentMode('sculpt')" class="mode-button" :style="state.currentMode === 'sculpt' ? 'background-color: var(--color-secondary);' : ''">
<i class="bi bi-hammer"></i>
Sculpt Mode
Voxel Mode
</div>
</div>
</div>
Expand Down
88 changes: 57 additions & 31 deletions src/components/panel/ObjectProperties.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import * as THREE from "three";
import { defineComponent } from 'vue';
import { defineComponent, nextTick } from 'vue';
import { state } from '../../state';
import VoxelMesh from '../../core/VoxelMesh';
import NumberInput from '../input/NumberInput.vue';
Expand Down Expand Up @@ -47,6 +47,15 @@ export default defineComponent({
state.selectedObject.rotation.y = v.y;
state.selectedObject.rotation.z = v.z;
}
},
collapse(what: string) {
const el = this.$refs[what] as HTMLElement;
if ((this as any)[what + 'Open']) {
el.style.maxHeight = 0 + 'px';
} else {
el.style.maxHeight = el.scrollHeight + 'px';
}
(this as any)[what + 'Open'] = !(this as any)[what + 'Open'];
}
},
mounted() {
Expand All @@ -57,10 +66,22 @@ export default defineComponent({
unmounted() {
clearInterval(this.interval);
},
updated() {
['position', 'scale', 'rotation', 'voxel'].forEach((i) => {
const el = this.$refs[i] as HTMLElement;
if (el && el.style && (this as any)[i + 'Open']) {
el.style.maxHeight = el.scrollHeight + 'px';
}
});
},
data() {
return {
state,
interval: 0
interval: 0,
positionOpen: true,
scaleOpen: true,
rotationOpen: true,
voxelOpen: true
}
}
});
Expand All @@ -72,63 +93,53 @@ export default defineComponent({
</div>
<div class="object-options-list" v-else-if="state.selectedObject">
{{ state.selectedObject.constructor.name }}
<div class="object-option-group-title" style="text-align: left;">
<h5>Position</h5>
<div @click="collapse('position')" class="object-option-group-title" style="text-align: left;">
<i :class="{'bi bi-caret-down-fill': positionOpen, 'bi bi-caret-right-fill': !positionOpen}"></i>
<h5 style="margin-left: 8px;">Position</h5>
</div>
<div class="object-option-group">
<div class="object-option-group" ref="position" :class="{'collapse': !positionOpen}">
<Vector3Input
:xp="(state.selectedObject as MeshObject).position.x || 0"
:yp="(state.selectedObject as MeshObject).position.y || 0"
:zp="(state.selectedObject as MeshObject).position.z || 0"
@changeValue="positionChange"
/>
</div>
<div class="object-option-group-title" style="text-align: left;">
<h5>Scale</h5>
<div @click="collapse('scale')" class="object-option-group-title" style="text-align: left;">
<i :class="{'bi bi-caret-down-fill': scaleOpen, 'bi bi-caret-right-fill': !scaleOpen}"></i>
<h5 style="margin-left: 8px;">Scale</h5>
</div>
<div class="object-option-group">
<div class="object-option-group" ref="scale" :class="{'collapse': !scaleOpen}">
<Vector3Input
:xp="(state.selectedObject as MeshObject).scale.x || 0"
:yp="(state.selectedObject as MeshObject).scale.y || 0"
:zp="(state.selectedObject as MeshObject).scale.z || 0"
@changeValue="scaleChange"
/>
</div>
<div class="object-option-group-title" style="text-align: left;">
<h5>Rotation</h5>
<div @click="collapse('rotation')" class="object-option-group-title" style="text-align: left;">
<i :class="{'bi bi-caret-down-fill': rotationOpen, 'bi bi-caret-right-fill': !rotationOpen}"></i>
<h5 style="margin-left: 8px;">Rotation</h5>
</div>
<div class="object-option-group">
<div class="object-option-group" ref="rotation" :class="{'collapse': !rotationOpen}">
<Vector3Input
:xp="(state.selectedObject as MeshObject).rotation.x || 0"
:yp="(state.selectedObject as MeshObject).rotation.y || 0"
:zp="(state.selectedObject as MeshObject).rotation.z || 0"
@changeValue="rotationChange"
/>
</div>
<!-- <div class="object-option-group">
<div class="object-option">
<h5>X:</h5>
<NumberInput @changeValue="positionXChange" unit="m" :value="state.selectedObject.position.x" />
</div>
<div class="object-option">
<h5>Y:</h5>
<NumberInput @changeValue="positionYChange" unit="m" :value="state.selectedObject.position.y" />
</div>
<div class="object-option">
<h5>Z:</h5>
<NumberInput @changeValue="positionZChange" unit="m" :value="state.selectedObject.position.z" />
</div>
</div> -->

<div class="object-option-group-title" style="text-align: left;">
<h5>Voxel mesh</h5>
<div @click="collapse('voxel')" class="object-option-group-title" style="text-align: left;">
<i :class="{'bi bi-caret-down-fill': voxelOpen, 'bi bi-caret-right-fill': !voxelOpen}"></i>
<h5 style="margin-left: 8px;">Voxel mesh</h5>
</div>
<div class="object-option-group">
<div class="object-option-group" ref="voxel" :class="{'collapse': !voxelOpen}">
<div v-if="state.selectedObject.constructor.name === 'VoxelMesh'" class="object-option">
<h5>March cubes</h5>
<input @change="marchCubes" :checked="(state.selectedObject as VoxelMesh).marchCubes" type="checkbox">
</div>
<div v-if="(state.selectedObject as VoxelMesh).marchCubes" class="object-option">
<div v-if="(state.selectedObject as VoxelMesh).marchCubes && !(state.selectedObject as VoxelMesh).smoothGeometry" class="object-option">
<h5>Smooth normals</h5>
<input @change="smoothNormals" :checked="(state.selectedObject as VoxelMesh).smoothNormals" type="checkbox">
</div>
Expand All @@ -141,6 +152,9 @@ export default defineComponent({
<div class="object-options-list" v-else>
No object selected
</div>
<div class="object-option-group-title" style="text-align: left;">
<h5></h5>
</div>
</template>

<style scoped>
Expand All @@ -150,14 +164,26 @@ export default defineComponent({
align-items: center;
}
.object-option-group-title {
width: 70%;
display: flex;
align-items: center;
width: 90%;
}
.object-option-group-title:hover {
color: var(--color-primary);
cursor: pointer;
}
.object-option-group {
width: 70%;
width: 80%;
padding-left: 8px;
padding-right: 8px;
border-radius: 8px;
background-color: var(--color-foreground-2);
transition: max-height 0.3s ease-in-out;
overflow: hidden;
}
.object-option-group.collapse {
transition: max-height 0.3s ease-out;
max-height: 0;
}
.object-options-list {
margin-top: 8px;
Expand Down
3 changes: 2 additions & 1 deletion src/components/panel/PropertiesEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@

<style scoped>
.properties-panel {
height: calc(100% - 2px);
height: calc(100vh - 35px);
width: 256px;
background-color: var(--color-background);
display: flex;
flex-direction: column;
border: 1px var(--color-foreground-2) solid;
overflow: hidden;
}
</style>
Loading

0 comments on commit b5a6c93

Please sign in to comment.