diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..fdd8651 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["@babel/plugin-proposal-class-properties"] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4972456..e8c7d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.3] - 2020-02-08 +### Added +- Added tools panel +- Added save/open functionality +### Changed +- Changed plugin system + ## [0.0.2] - 2020-02-02 ### Added - Added orbit camera control @@ -7,5 +14,6 @@ ## [0.0.1] - 2020-02-01 ### Initial release +[0.0.3]: https://github.com/Perkovec/Vuxel/compare/v0.0.2...v0.0.3 [0.0.2]: https://github.com/Perkovec/Vuxel/compare/v0.0.1...v0.0.2 [0.0.1]: https://github.com/Perkovec/Vuxel/releases/tag/v0.0.1 \ No newline at end of file diff --git a/README.md b/README.md index 03dab61..2cbead3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/Perkovec/Vuxel.svg?branch=master)](https://travis-ci.org/Perkovec/Vuxel) [![Greenkeeper badge](https://badges.greenkeeper.io/Perkovec/Vuxel.svg)](https://greenkeeper.io/) -Version 0.0.2 +Version 0.0.3 By Ilya Karpuk perkovec24@gmail.com @@ -37,7 +37,7 @@ npm run build - [ ] Orientation indicator and controller - [ ] Painting tools - [ ] Advanced color picker -- [ ] Export/import models +- [x] Export/import models - [ ] Layers ## Licence diff --git a/demo.png b/demo.png index 6789476..f36d4a3 100644 Binary files a/demo.png and b/demo.png differ diff --git a/package-lock.json b/package-lock.json index d6d4cc7..57cf171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vuxel", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -137,6 +137,20 @@ "semver": "^5.5.0" } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.3.tgz", + "integrity": "sha512-qmp4pD7zeTxsv0JNecSBsEmG1ei2MqwJq4YQcK3ZWm/0t07QstWfvuV/vm3Qt5xNMFETn2SZqpMx2MQzbtq+KA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3" + } + }, "@babel/helper-create-regexp-features-plugin": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz", @@ -348,6 +362,16 @@ "@babel/plugin-syntax-async-generators": "^7.8.0" } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", + "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, "@babel/plugin-proposal-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz", @@ -2822,6 +2846,11 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "event-lite": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz", + "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==" + }, "events": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", @@ -4684,6 +4713,11 @@ "to-regex": "^3.0.2" } }, + "micromodal": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/micromodal/-/micromodal-0.4.2.tgz", + "integrity": "sha512-tL8Z6Vi72haNUNlkhLjJJv1Q16eqUWMPAGT4RF7mwDM5DNyJXC67Yj8jw/yVgGYdhnCEBBf3X5JM3d/MpXSEKg==" + }, "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", diff --git a/package.json b/package.json index 3531581..4a1dc19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vuxel", - "version": "0.0.2", + "version": "0.0.3", "description": "Open Source Voxel editor", "scripts": { "start": "parcel src/index.html", @@ -10,10 +10,14 @@ "author": "perkovec", "license": "MIT", "dependencies": { + "event-lite": "^0.1.2", + "micromodal": "^0.4.2", "purecss": "^1.0.1", "three": "^0.113.1" }, "devDependencies": { + "@babel/core": "^7.8.4", + "@babel/plugin-proposal-class-properties": "^7.8.3", "parcel": "^1.12.4", "posthtml-include": "^1.3.3" } diff --git a/src/app.js b/src/app.js index f7f8f02..4e0dec2 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,7 @@ export class App { constructor(THREE, plugins) { this.THREE = THREE; this.objects = []; + this.sceneObjects = []; this.isShiftDown = false; this.editorContainer = document.getElementById('editor'); @@ -24,30 +25,30 @@ export class App { this.createLight(); this.createGround(); - this.createCubes(); window.addEventListener('resize', () => this.onWindowResize(), false); - this.editorContainer.addEventListener('mousemove', event => this.onDocumentMouseMove(event), false); - this.editorContainer.addEventListener('mousedown', event => this.onDocumentMouseDown(event), false); - document.addEventListener('keydown', event => this.onDocumentKeyDown(event), false); - document.addEventListener('keyup', event => this.onDocumentKeyUp(event), false); - this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(this.editorContainer.clientWidth, this.editorContainer.clientHeight); this.editorContainer.appendChild(this.renderer.domElement); this.rect = this.renderer.domElement.getBoundingClientRect(); + + this.pluginsInstances = {}; const pluginsConfig = { THREE, - brushMaterial: this.cubeMaterial, renderer: this.renderer, camera: this.camera, render: this.render.bind(this), + scene: this.scene, + sceneObjects: this.sceneObjects, + objects: this.objects, + plugins: this.pluginsInstances, + rect: this.rect, } plugins.forEach(Plugin => { - new Plugin(pluginsConfig); + this.pluginsInstances[Plugin.meta.name] = new Plugin(pluginsConfig); }); } @@ -62,18 +63,6 @@ export class App { this.scene.add(directionalLight); } - createCubes() { - const THREE = this.THREE; - - const rollOverGeo = new THREE.BoxBufferGeometry(50, 50, 50); - const rollOverMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, opacity: 0.5, transparent: true }); - this.rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial); - this.scene.add(this.rollOverMesh); - - this.cubeGeo = new THREE.BoxBufferGeometry(50, 50, 50); - this.cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); - } - createGround() { const THREE = this.THREE; const geometry = new THREE.PlaneBufferGeometry(1000, 1000); @@ -88,73 +77,10 @@ export class App { this.camera.aspect = this.editorContainer.clientWidth / this.editorContainer.clientHeight; this.camera.updateProjectionMatrix(); - this.renderer.setSize(this.editorContainer.clientWidth, this.editorContainer.clientHeight); - } - - onDocumentMouseMove(event) { - event.preventDefault(); - - this.mouse.set( - ((event.clientX - this.rect.left) / this.editorContainer.clientWidth) * 2 - 1, - -((event.clientY - this.rect.top) / this.editorContainer.clientHeight) * 2 + 1 - ); - this.raycaster.setFromCamera(this.mouse, this.camera); - - const intersects = this.raycaster.intersectObjects(this.objects); - if (intersects.length > 0) { - const intersect = intersects[0]; - this.rollOverMesh.position.copy(intersect.point).add(intersect.face.normal); - this.rollOverMesh.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25); - } - + this.renderer.setSize(this.editorContainer.clientWidth, this.editorContainer.clientHeight); this.render(); } - onDocumentMouseDown(event) { - event.preventDefault(); - const THREE = this.THREE; - - this.mouse.set( - ((event.clientX - this.rect.left) / this.editorContainer.clientWidth) * 2 - 1, - -((event.clientY - this.rect.top) / this.editorContainer.clientHeight) * 2 + 1 - ); - this.raycaster.setFromCamera(this.mouse, this.camera); - - const intersects = this.raycaster.intersectObjects(this.objects); - if (intersects.length > 0) { - const intersect = intersects[0]; - - // delete cube - if (this.isShiftDown) { - if (intersect.object !== this.plane) { - this.scene.remove(intersect.object); - this.objects.splice(this.objects.indexOf(intersect.object), 1); - } - } else { - // create cube - const voxel = new THREE.Mesh(this.cubeGeo, this.cubeMaterial.clone()); - voxel.position.copy(intersect.point).add(intersect.face.normal); - voxel.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25); - this.scene.add(voxel); - - this.objects.push(voxel); - } - this.render(); - } - } - - onDocumentKeyDown(event) { - switch (event.keyCode) { - case 16: this.isShiftDown = true; break; - } - } - - onDocumentKeyUp(event) { - switch (event.keyCode) { - case 16: this.isShiftDown = false; break; - } - } - render() { this.renderer.render(this.scene, this.camera); } diff --git a/src/fonts/Flaticon.eot b/src/fonts/Flaticon.eot new file mode 100644 index 0000000..67ccf30 Binary files /dev/null and b/src/fonts/Flaticon.eot differ diff --git a/src/fonts/Flaticon.svg b/src/fonts/Flaticon.svg new file mode 100644 index 0000000..a09317e --- /dev/null +++ b/src/fonts/Flaticon.svg @@ -0,0 +1,48 @@ + + + + + +Created by FontForge 20170731 at Sun Feb 2 23:18:40 2020 + By root + + + + + + + + + + + + + diff --git a/src/fonts/Flaticon.ttf b/src/fonts/Flaticon.ttf new file mode 100644 index 0000000..1c4c1be Binary files /dev/null and b/src/fonts/Flaticon.ttf differ diff --git a/src/fonts/Flaticon.woff b/src/fonts/Flaticon.woff new file mode 100644 index 0000000..41bc485 Binary files /dev/null and b/src/fonts/Flaticon.woff differ diff --git a/src/fonts/Flaticon.woff2 b/src/fonts/Flaticon.woff2 new file mode 100644 index 0000000..3b1d80d Binary files /dev/null and b/src/fonts/Flaticon.woff2 differ diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 30ccd43..0000000 --- a/src/index.css +++ /dev/null @@ -1,117 +0,0 @@ -/* GLOBAL */ - -html, body { - width: 100%; - height: 100%; -} - -.editor-layout { - width: 100%; - height: 100%; - display: flex; -} - -.editor-layout #editor { - width: 100%; -} - -.editor-layout #editor canvas { - outline: none; -} - -.brush-color-container { - padding: 0px 15px; -} - -.brush-color { - width: 20px; - height: 20px; - padding: 0; - border: 1px solid black; - margin: 5px; -} - -.brush-color.brown { - background-color: #800000; -} - -.brush-color.red { - background-color: #FF0000; -} - -.brush-color.black { - background-color: #000000; -} - -.brush-color.white { - background-color: #FFFFFF; -} - -.brush-color.yellow { - background-color: #FFFF00; -} - -.brush-color.lime { - background-color: #00FF00; -} - -.brush-color.green { - background-color: #008000; -} - -.brush-color.aqua { - background-color: #00FFFF; -} - -.brush-color.blue { - background-color: #0000FF; -} - -.brush-color.fuchsia { - background-color: #FF00FF; -} - -.brush-color.purple { - background-color: #800080; -} - -.brush-color.orange { - background-color: #FF8000; -} - -.brush-color-input { - margin: 20px 0px; -} - -/* (UI) LOADER */ - -.ui-loader { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - background-color: #191818; - color: #ffffff; - display: flex; - justify-content: center; - align-items: center; -} - -/* (UI) SIDEBAR */ - -.ui-sidebar { - width: 275px; - display: flex; - flex-direction: column; -} - -.ui-sidebar .pure-menu-list { - flex: 1; -} - -.ui-sidebar .ui-sidebar-guide-block { - padding: 10px; - font-size: 14px; - color: grey; -} diff --git a/src/index.html b/src/index.html index cadc533..d4b4497 100644 --- a/src/index.html +++ b/src/index.html @@ -5,10 +5,18 @@ -
- -
+ +
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/src/index.js b/src/index.js index 59ac9f8..d873dcc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,39 +1,72 @@ import 'purecss/build/pure-min.css'; -import './index.css'; +import './styles/flaticon.css'; +import './styles/index.css'; -Promise.all([ +const loader = document.getElementById('loader'); +const loaderContainer = document.getElementById('loader-container'); +const loaderProgress = document.getElementById('loader-progress'); + +const progressPromise = (promises, tickCallback) => { + const len = promises.length; + let progress = 0; + + const tick = (promise) => { + promise.then(() => { + progress++; + tickCallback(progress, len); + }); + return promise; + } + + return Promise.all(promises.map(tick)); +} + +const assets = [ + // CORE import('./three.js'), import('./app.js'), + // LIBS + import('micromodal/dist/micromodal.min.js'), + // PLUGINS + import('./plugins/file-manager/file-manager.js'), + import('./plugins/tools/index.js'), import('./plugins/color-picker.js'), import('./plugins/camera-control.js'), -]).then(([ - { THREE }, - { App }, +]; - // PLUGINS - { ColorPicker }, - { CameraControl }, -]) => { - const plugins = [ - ColorPicker, - CameraControl, - ]; - - const app = new App(THREE, plugins); - // app.init(); - app.render(); - // app.animate(); -}); - -// window.addEventListener( 'resize', onWindowResize, false ); -// -// function onWindowResize(){ -// -// camera.aspect = window.innerWidth / window.innerHeight; -// camera.updateProjectionMatrix(); -// -// renderer.setSize( window.innerWidth, window.innerHeight ); -// -// } \ No newline at end of file +const update = (completed, total) => { + const progressWidth = loaderContainer.clientWidth / total * completed; + loaderProgress.style.width = `${progressWidth}px`; +} + +progressPromise(assets, update) + .then(([ + // CORE + { THREE }, + { App }, + + // LIBS + MicroModal, + + // PLUGINS + { FileManager }, + { Tools }, + { ColorPicker }, + { CameraControl }, + ]) => { + MicroModal.init(); + + const plugins = [ + FileManager, + Tools, + ColorPicker, + CameraControl, + ]; + + const app = new App(THREE, plugins); + app.render(); + + loader.style.display = 'none'; + }); diff --git a/src/plugins/camera-control.js b/src/plugins/camera-control.js index f7cea34..7236d94 100644 --- a/src/plugins/camera-control.js +++ b/src/plugins/camera-control.js @@ -1,6 +1,10 @@ /* BASED ON https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js */ export class CameraControl { + static meta = { + name: 'camera-control', + }; + constructor(configs) { const THREE = this.THREE = configs.THREE; @@ -308,7 +312,6 @@ export class CameraControl { } onKeyDown(event) { - console.log(event); if (this.enableKeys === false || this.enablePan === false) return; this.handleKeyDown(event); @@ -512,4 +515,4 @@ export class CameraControl { this._panOffset.add( v ); }; } -} \ No newline at end of file +} diff --git a/src/plugins/color-picker.js b/src/plugins/color-picker.js index 7a55c62..514eb8a 100644 --- a/src/plugins/color-picker.js +++ b/src/plugins/color-picker.js @@ -1,12 +1,27 @@ export class ColorPicker { + static meta = { + name: 'color-picker', + }; + constructor(configs) { this.configs = configs; + this.plugins = configs.plugins; - this.colorInput = document.getElementById('brush-color-input'); + this.avaliableTools = ['single_voxel']; + this.colorInput = document.getElementById('brush-color-input'); this.presets = document.querySelectorAll('.brush-color'); + this.containers = document.querySelectorAll('[data-plugin-color-picker]'); - this.colorInput.value = '#' + this.configs.brushMaterial.color.getHexString(); + if (this.plugins['tools']) { + if (this.avaliableTools.includes(this.plugins['tools'].currentToolName)) { + this.colorInput.value = '#' + this.plugins['tools'].currentTool.mainMaterial.color.getHexString(); + } + this.setupListeners(); + } else { + console.warn('Color picker plugin could not find Tools plugin.'); + this.hide(); + } this.presets.forEach(preset => { preset.addEventListener('click', () => { @@ -18,7 +33,32 @@ export class ColorPicker { this.colorInput.addEventListener('input', ev => this.setBrushColor(ev.target.value)); } + hide() { + this.containers.forEach(container => { + container.style.display = 'none'; + }) + } + + show() { + this.containers.forEach(container => { + container.style.display = 'block'; + }) + } + + setupListeners() { + const toolsPlugin = this.plugins['tools']; + + toolsPlugin.on('tool_enabled', (toolName, tool) => { + if (this.avaliableTools.includes(toolName)) { + this.colorInput.value = '#' + tool.mainMaterial.color.getHexString(); + this.show(); + } else { + this.hide(); + } + }); + } + setBrushColor(color) { - this.configs.brushMaterial.color.setStyle(color); + this.plugins['tools'].currentTool.mainMaterial.color.setStyle(color); } -} \ No newline at end of file +} diff --git a/src/plugins/file-manager/exporter.js b/src/plugins/file-manager/exporter.js new file mode 100644 index 0000000..cee4650 --- /dev/null +++ b/src/plugins/file-manager/exporter.js @@ -0,0 +1,19 @@ +export const exporter = (voxels) => { + const data = { + version: '1', + voxels: [], + } + + let minorVoxel; + voxels.forEach(voxel => { + minorVoxel = { + position: voxel.position.toArray(), + material: { + color: voxel.material.color.toArray(), + } + }; + data.voxels.push(minorVoxel); + }); + + return data; +} \ No newline at end of file diff --git a/src/plugins/file-manager/file-manager.js b/src/plugins/file-manager/file-manager.js new file mode 100644 index 0000000..f0d3d18 --- /dev/null +++ b/src/plugins/file-manager/file-manager.js @@ -0,0 +1,97 @@ +import { exporter } from './exporter'; +import { loader } from './loader'; + +export class FileManager { + static meta = { + name: 'file-manager', + }; + + constructor(configs) { + this.configs = configs; + + const menuItems = document.querySelectorAll('.plugin-file-manager'); + menuItems.forEach(item => { + item.addEventListener('click', (event) => this.dispatchEvent(event, item.dataset.event)); + }) + + this.fakeLink = document.createElement('a'); + this.fakeLink.style.display = 'none'; + document.body.appendChild(this.fakeLink); + + this.fakeInput = document.createElement('input'); + this.fakeInput.type = 'file'; + this.fakeInput.accept = '.vxl'; + this.fakeInput.style.display = 'none'; + document.body.appendChild(this.fakeInput); + + this.fakeInput.addEventListener('change', (event) => this.fileSelected(event)); + } + + dispatchEvent(event, eventName) { + switch(eventName) { + case 'new': + this.handleNew(); + break; + case 'save': + this.handleSave(); + break; + case 'open': + this.handleOpen(); + break; + } + } + + clearScene() { + const scene = this.configs.scene; + const sceneObjects = this.configs.sceneObjects; + + scene.remove(...sceneObjects); + sceneObjects.splice(0, sceneObjects.length); + } + + handleNew() { + if (confirm('Are you sure you want to create a new file?')) { + this.clearScene(); + this.configs.render(); + } + } + + handleSave() { + const data = exporter(this.configs.sceneObjects); + + const output = JSON.stringify(data, null, 2); + this.fakeLink.href = URL.createObjectURL(new Blob([output], { type: 'text/plain' })); + this.fakeLink.download = 'scene.vxl'; + this.fakeLink.click(); + } + + handleOpen() { + this.fakeInput.click(); + } + + fileSelected(event) { + const files = event.target.files; + const THREE = this.configs.THREE; + const scene = this.configs.scene; + const sceneObjects = this.configs.sceneObjects; + + if (files && files.length) { + const reader = new FileReader(); + reader.readAsText(files[0]); + + reader.onload = () => { + this.clearScene(); + + const data = loader(THREE, reader.result); + data.forEach(voxel => { + scene.add(voxel); + sceneObjects.push(voxel); + }) + + this.configs.render(); + }; + } + + event.target.value = null; + } +} diff --git a/src/plugins/file-manager/loader.js b/src/plugins/file-manager/loader.js new file mode 100644 index 0000000..8193525 --- /dev/null +++ b/src/plugins/file-manager/loader.js @@ -0,0 +1,16 @@ +export const loader = (THREE, plainText) => { + const voxels = []; + + const parsedData = JSON.parse(plainText); + parsedData.voxels.forEach(voxel => { + const mesh = new THREE.Mesh( + new THREE.BoxBufferGeometry(50, 50, 50), + new THREE.MeshLambertMaterial({ color: new THREE.Color(...voxel.material.color) }), + ); + mesh.position.fromArray(voxel.position); + + voxels.push(mesh); + }); + + return voxels; +} \ No newline at end of file diff --git a/src/plugins/tools/index.js b/src/plugins/tools/index.js new file mode 100644 index 0000000..9b0cc3f --- /dev/null +++ b/src/plugins/tools/index.js @@ -0,0 +1,46 @@ +import EventEmitter from 'event-lite'; +import { SingleVoxel } from './single-voxel'; +import { RemoveVoxel } from './remove-voxel'; + +export class Tools extends EventEmitter { + static meta = { + name: 'tools', + }; + + constructor(configs) { + super(); + this.configs = configs; + + this.toolInstances = { + 'single_voxel': new SingleVoxel(configs), + 'remove_voxel': new RemoveVoxel(configs), + }; + + this.tools = document.querySelectorAll('.tool-item'); + this.currentTool = null; + this.currentToolName = null; + + this.tools.forEach(tool => { + tool.addEventListener('click', () => { + this.setTool(tool.dataset.tool); + }); + }); + + this.setTool(this.tools[0].dataset.tool); + } + + setTool(toolName) { + if (this.currentTool && this.currentTool.destroy) { + this.currentTool.destroy(); + this.currentToolName = null; + this.emit('tool_disabled'); + } + + this.currentTool = this.toolInstances[toolName]; + this.currentToolName = toolName; + if (this.currentTool && this.currentTool.init) { + this.currentTool.init(); + this.emit('tool_enabled', toolName, this.currentTool); + } + } +} diff --git a/src/plugins/tools/remove-voxel.js b/src/plugins/tools/remove-voxel.js new file mode 100644 index 0000000..bca89ad --- /dev/null +++ b/src/plugins/tools/remove-voxel.js @@ -0,0 +1,82 @@ +export class RemoveVoxel { + constructor(configs) { + const THREE = this.THREE = configs.THREE; + + this.scene = configs.scene; + this.renderer = configs.renderer; + this.camera = configs.camera; + this.sceneObjects = configs.sceneObjects; + this.render = configs.render; + this.rect = configs.rect; + + this.raycaster = new THREE.Raycaster(); + this.mouse = new THREE.Vector2(); + + const rollOverGeo = new THREE.BoxBufferGeometry(50, 50, 50); + const rollOverMaterial = new THREE.MeshBasicMaterial({ + color: 0xff0000, + opacity: 0.5, + transparent: true, + visible: false, + }); + this.rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial); + + this.onDocumentMouseMove = this.onDocumentMouseMove.bind(this); + this.onDocumentMouseDown = this.onDocumentMouseDown.bind(this); + } + + init() { + this.scene.add(this.rollOverMesh); + + this.renderer.domElement.addEventListener('mousemove', this.onDocumentMouseMove, false); + this.renderer.domElement.addEventListener('mousedown', this.onDocumentMouseDown, false); + } + + destroy() { + this.scene.remove(this.rollOverMesh); + + this.renderer.domElement.removeEventListener('mousemove', this.onDocumentMouseMove, false); + this.renderer.domElement.removeEventListener('mousedown', this.onDocumentMouseDown, false); + } + + onDocumentMouseMove(event) { + event.preventDefault(); + + this.mouse.set( + ((event.clientX - this.rect.left) / this.renderer.domElement.clientWidth) * 2 - 1, + -((event.clientY - this.rect.top) / this.renderer.domElement.clientHeight) * 2 + 1 + ); + this.raycaster.setFromCamera(this.mouse, this.camera); + + const intersects = this.raycaster.intersectObjects(this.sceneObjects); + if (intersects.length > 0) { + const intersect = intersects[0]; + this.rollOverMesh.material.visible = true; + this.rollOverMesh.position.copy(intersect.object.position); + } else { + this.rollOverMesh.material.visible = false; + } + + this.render(); + } + + onDocumentMouseDown(event) { + event.preventDefault(); + + this.mouse.set( + ((event.clientX - this.rect.left) / this.renderer.domElement.clientWidth) * 2 - 1, + -((event.clientY - this.rect.top) / this.renderer.domElement.clientHeight) * 2 + 1 + ); + this.raycaster.setFromCamera(this.mouse, this.camera); + + const intersects = this.raycaster.intersectObjects(this.sceneObjects); + if (intersects.length > 0) { + const intersect = intersects[0]; + + this.scene.remove(intersect.object); + this.sceneObjects.splice(this.sceneObjects.indexOf(intersect.object), 1); + + this.render(); + } + } +} \ No newline at end of file diff --git a/src/plugins/tools/single-voxel.js b/src/plugins/tools/single-voxel.js new file mode 100644 index 0000000..aff472d --- /dev/null +++ b/src/plugins/tools/single-voxel.js @@ -0,0 +1,92 @@ +export class SingleVoxel { + constructor(configs) { + const THREE = this.THREE = configs.THREE; + + this.scene = configs.scene; + this.renderer = configs.renderer; + this.camera = configs.camera; + this.objects = configs.objects; + this.sceneObjects = configs.sceneObjects; + this.render = configs.render; + this.rect = configs.rect; + + this.raycaster = new THREE.Raycaster(); + this.mouse = new THREE.Vector2(); + + const rollOverGeo = new THREE.BoxBufferGeometry(50, 50, 50); + const rollOverMaterial = new THREE.MeshBasicMaterial({ + color: 0x00ff00, + opacity: 0.5, + transparent: true, + visible: false, + }); + this.rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial); + + this.cubeGeo = new THREE.BoxBufferGeometry(50, 50, 50); + this.mainMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); + + this.onDocumentMouseMove = this.onDocumentMouseMove.bind(this); + this.onDocumentMouseDown = this.onDocumentMouseDown.bind(this); + } + + init() { + this.scene.add(this.rollOverMesh); + + this.renderer.domElement.addEventListener('mousemove', this.onDocumentMouseMove, false); + this.renderer.domElement.addEventListener('mousedown', this.onDocumentMouseDown, false); + } + + destroy() { + this.scene.remove(this.rollOverMesh); + + this.renderer.domElement.removeEventListener('mousemove', this.onDocumentMouseMove, false); + this.renderer.domElement.removeEventListener('mousedown', this.onDocumentMouseDown, false); + } + + onDocumentMouseMove(event) { + event.preventDefault(); + + this.mouse.set( + ((event.clientX - this.rect.left) / this.renderer.domElement.clientWidth) * 2 - 1, + -((event.clientY - this.rect.top) / this.renderer.domElement.clientHeight) * 2 + 1 + ); + this.raycaster.setFromCamera(this.mouse, this.camera); + + const intersects = this.raycaster.intersectObjects([...this.objects, ...this.sceneObjects]); + if (intersects.length > 0) { + const intersect = intersects[0]; + this.rollOverMesh.material.visible = true; + this.rollOverMesh.position.copy(intersect.point).add(intersect.face.normal); + this.rollOverMesh.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25); + } else { + this.rollOverMesh.material.visible = false; + } + + this.render(); + } + + onDocumentMouseDown(event) { + event.preventDefault(); + const THREE = this.THREE; + + this.mouse.set( + ((event.clientX - this.rect.left) / this.renderer.domElement.clientWidth) * 2 - 1, + -((event.clientY - this.rect.top) / this.renderer.domElement.clientHeight) * 2 + 1 + ); + this.raycaster.setFromCamera(this.mouse, this.camera); + + const intersects = this.raycaster.intersectObjects([...this.objects, ...this.sceneObjects]); + if (intersects.length > 0) { + const intersect = intersects[0]; + + const voxel = new THREE.Mesh(this.cubeGeo, this.mainMaterial.clone()); + voxel.position.copy(intersect.point).add(intersect.face.normal); + voxel.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25); + this.scene.add(voxel); + + this.sceneObjects.push(voxel); + + this.render(); + } + } +} \ No newline at end of file diff --git a/src/styles/flaticon.css b/src/styles/flaticon.css new file mode 100644 index 0000000..8bb0790 --- /dev/null +++ b/src/styles/flaticon.css @@ -0,0 +1,34 @@ + /* + Flaticon icon font: Flaticon + Creation date: 02/02/2020 23:18 + */ + +@font-face { + font-family: "Flaticon"; + src: url("../fonts/Flaticon.eot"); + src: url("../fonts/Flaticon.eot?#iefix") format("embedded-opentype"), + url("../fonts/Flaticon.woff2") format("woff2"), + url("../fonts/Flaticon.woff") format("woff"), + url("../fonts/Flaticon.ttf") format("truetype"), + url("../fonts/Flaticon.svg#Flaticon") format("svg"); + font-weight: normal; + font-style: normal; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: "Flaticon"; + src: url("../fonts/Flaticon.svg#Flaticon") format("svg"); + } +} + +[class^="flaticon-"]:before, [class*=" flaticon-"]:before, +[class^="flaticon-"]:after, [class*=" flaticon-"]:after { + font-family: Flaticon; + font-style: normal; +} + +.flaticon-rubik:before { content: "\f100"; } +.flaticon-3d:before { content: "\f101"; } +.flaticon-eraser:before { content: "\f102"; } +.flaticon-cancel:before { content: "\f103"; } \ No newline at end of file diff --git a/src/styles/index.css b/src/styles/index.css new file mode 100644 index 0000000..40741c2 --- /dev/null +++ b/src/styles/index.css @@ -0,0 +1,232 @@ +/* GLOBAL */ + +html, body { + width: 100%; + height: 100%; +} + +.main-layout { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} + +.editor-layout { + width: 100%; + max-width: 100%; + height: 100%; + display: flex; +} + +.editor-layout #editor { + flex: 1; +} + +.editor-layout #editor canvas { + outline: none; + display: block; + max-width: 100%; +} + +.brush-color-container { + padding: 0px 15px; +} + +.brush-color { + width: 20px; + height: 20px; + padding: 0; + border: 1px solid black; + margin: 5px; +} + +.brush-color.brown { + background-color: #800000; +} + +.brush-color.red { + background-color: #FF0000; +} + +.brush-color.black { + background-color: #000000; +} + +.brush-color.white { + background-color: #FFFFFF; +} + +.brush-color.yellow { + background-color: #FFFF00; +} + +.brush-color.lime { + background-color: #00FF00; +} + +.brush-color.green { + background-color: #008000; +} + +.brush-color.aqua { + background-color: #00FFFF; +} + +.brush-color.blue { + background-color: #0000FF; +} + +.brush-color.fuchsia { + background-color: #FF00FF; +} + +.brush-color.purple { + background-color: #800080; +} + +.brush-color.orange { + background-color: #FF8000; +} + +.brush-color-input { + margin: 20px 0px; +} + +.tools-container { + padding: 0px 15px; +} + +.tool-item { + width: 26px; + height: 26px; + padding: 0; + border: 1px solid black; + margin: 5px; + background-color: #ffffff; + font-size: 20px; +} + +.tool-item.flaticon-3d:before { + margin-left: 2px; +} + +/* (UI) LOADER */ + +.ui-loader { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: #f0f0f0; + color: grey; + display: flex; + justify-content: center; + align-items: center; + z-index: 100; + flex-direction: column; +} + +.ui-loader .loader-container { + width: 300px; + height: 10px; + border: 1px solid grey; + border-radius: 10px; +} + +.ui-loader .loader-progress { + height: 100%; + background-color: grey; + width: 0; + transition: width 0.2s ease-in-out; +} + +/* (UI) SIDEBAR */ + +.ui-sidebar { + width: 275px; + display: flex; + flex-direction: column; + border-right: 1px solid grey; +} + +.ui-sidebar .pure-menu-list { + flex: 1; +} + +.ui-sidebar .pure-menu-item { + height: auto; +} + +.ui-sidebar .ui-sidebar-guide-block { + padding: 10px; + font-size: 14px; + color: grey; +} + +/* (UI) TOPBAR */ + +.ui-topbar { + border-bottom: 1px solid grey; + user-select: none; +} + +.ui-topbar .pure-menu-children { + border: 1px solid grey; +} + +.ui-topbar .pure-menu-list { + margin-left: 20px; + cursor: pointer; +} + +/* (UI) MODAL */ + +.ui-modal { + display: none; +} + +.ui-modal.is-open { + display: block; +} + +.ui-modal .modal-overlay { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + justify-content: center; +} + +.ui-modal .modal-container { + background-color: #ffffff; + min-width: 300px; + border-radius: 5px; +} + +.ui-modal .modal-container header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0px 10px; + border-bottom: 1px solid grey; +} + +.ui-modal .modal-container .modal-content { + padding: 0px 10px 10px; +} + +.ui-modal .modal-container .modal-close { + border: none; + padding: 3px; + width: 24px; + height: 24px; + font-size: 14px; + background: none; + outline: none; +} diff --git a/src/ui-parts/loader.html b/src/ui-parts/loader.html index 855d3b9..1717a6a 100644 --- a/src/ui-parts/loader.html +++ b/src/ui-parts/loader.html @@ -1,3 +1,6 @@ -
+

Loading...

+
+
+
\ No newline at end of file diff --git a/src/ui-parts/modals/about-modal.html b/src/ui-parts/modals/about-modal.html new file mode 100644 index 0000000..7024302 --- /dev/null +++ b/src/ui-parts/modals/about-modal.html @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/src/ui-parts/sidebar.html b/src/ui-parts/sidebar.html index 5d63b41..61eded6 100644 --- a/src/ui-parts/sidebar.html +++ b/src/ui-parts/sidebar.html @@ -1,26 +1,29 @@
    -
  • Color
  • -
  • - - - - - - - - - - - - - -
  • +
  • Tools
  • +
  • + + +
  • +
  • Color
  • +
  • + + + + + + + + + + + + + +
-

Click - add voxel

-

Shift + Click - remove voxel

Click + Move - rotate camera

Right Click + Move - move camera

Mouse Wheel - camera zoom

diff --git a/src/ui-parts/topbar.html b/src/ui-parts/topbar.html new file mode 100644 index 0000000..7f71fc3 --- /dev/null +++ b/src/ui-parts/topbar.html @@ -0,0 +1,16 @@ +
+
    +
  • VUXEL
  • +
  • + File +
      + + + +
    +
  • +
  • + About +
  • +
+
\ No newline at end of file