├── src ├── plugins │ ├── __init__.py │ ├── tool_erase.py │ ├── tool_colourpick.py │ ├── tool_paint.py │ ├── tool_fill.py │ ├── io_sproxel.py │ ├── tool_drag.py │ ├── io_obj.py │ ├── tool_draw.py │ ├── io_zoxel.py │ └── io_qubicle.py ├── gfx │ ├── icons │ │ ├── Add.png │ │ ├── Go.png │ │ ├── eye.png │ │ ├── Back.png │ │ ├── First.png │ │ ├── Last.png │ │ ├── Pause.png │ │ ├── Play.png │ │ ├── Stop.png │ │ ├── block.png │ │ ├── color.png │ │ ├── disk.png │ │ ├── grid.png │ │ ├── guide.png │ │ ├── mouse.png │ │ ├── redo.png │ │ ├── ruler.png │ │ ├── undo.png │ │ ├── wand.png │ │ ├── water.png │ │ ├── Cancel.png │ │ ├── Create.png │ │ ├── Delete.png │ │ ├── Forward.png │ │ ├── Goback.png │ │ ├── Playback.png │ │ ├── Rewind.png │ │ ├── arrow-in.png │ │ ├── border.png │ │ ├── document.png │ │ ├── grid-dot.png │ │ ├── palette.png │ │ ├── pencil.png │ │ ├── pipette.png │ │ ├── shovel.png │ │ ├── spectrum.png │ │ ├── wrench.png │ │ ├── zoom_in.png │ │ ├── zoom_out.png │ │ ├── Goforward.png │ │ ├── Lastrecord.png │ │ ├── Nexttrack.png │ │ ├── border-all.png │ │ ├── categories.png │ │ ├── layer-flip.png │ │ ├── paint-can.png │ │ ├── paint-tube.png │ │ ├── Fast-forward.png │ │ ├── First-record.png │ │ ├── arrow-in-out.png │ │ ├── block--arrow.png │ │ ├── block--minus.png │ │ ├── block--pencil.png │ │ ├── block--plus.png │ │ ├── block-share.png │ │ ├── border-down.png │ │ ├── border-inside.png │ │ ├── color-swatch.png │ │ ├── disks-black.png │ │ ├── highlighter.png │ │ ├── layer-resize.png │ │ ├── layer-rotate.png │ │ ├── layer-select.png │ │ ├── mouse-select.png │ │ ├── paint-brush.png │ │ ├── pencil-ruler.png │ │ ├── Previousrecord.png │ │ ├── border-outside.png │ │ ├── document-block.png │ │ ├── document-export.png │ │ ├── layers-ungroup.png │ │ ├── ruler-triangle.png │ │ ├── weather-clear-2.png │ │ ├── zoom-original-4.png │ │ ├── application-block.png │ │ ├── application-resize.png │ │ ├── block--exclamation.png │ │ ├── disk-return-black.png │ │ ├── layer-rotate-left.png │ │ ├── layer-select-point.png │ │ ├── layer-shape-line.png │ │ ├── mouse-select-right.png │ │ ├── mouse-select-wheel.png │ │ ├── transform-scale-2.png │ │ ├── wrench-screwdriver.png │ │ ├── border-bottom-thick.png │ │ ├── border-outside-thick.png │ │ ├── layer-flip-vertical.png │ │ ├── layer-resize-actual.png │ │ ├── object-rotate-left-3.png │ │ ├── application-resize-full.png │ │ ├── folder-horizontal-open.png │ │ ├── layers-alignment-center.png │ │ ├── object-rotate-right-3.png │ │ ├── application-resize-actual.png │ │ └── README.txt │ ├── texture.png │ └── about_dialog_image.png ├── plugin_loader.py ├── constants.py ├── dialog_about.py ├── zoxel.py ├── dialog_resize.py ├── plugin_api.py ├── undo.py ├── dialog_about.ui ├── dialog_resize.ui ├── resources.qrc ├── palette_widget.py ├── tool.py ├── voxel_grid.py ├── mainwindow.ui ├── mainwindow.py ├── voxel_widget.py └── voxel.py ├── run ├── freeze.bat ├── run.bat ├── .gitignore ├── README └── win32-installer.iss /src/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gfx/icons/Add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Add.png -------------------------------------------------------------------------------- /src/gfx/icons/Go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Go.png -------------------------------------------------------------------------------- /src/gfx/icons/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/eye.png -------------------------------------------------------------------------------- /src/gfx/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/texture.png -------------------------------------------------------------------------------- /src/gfx/icons/Back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Back.png -------------------------------------------------------------------------------- /src/gfx/icons/First.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/First.png -------------------------------------------------------------------------------- /src/gfx/icons/Last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Last.png -------------------------------------------------------------------------------- /src/gfx/icons/Pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Pause.png -------------------------------------------------------------------------------- /src/gfx/icons/Play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Play.png -------------------------------------------------------------------------------- /src/gfx/icons/Stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Stop.png -------------------------------------------------------------------------------- /src/gfx/icons/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/block.png -------------------------------------------------------------------------------- /src/gfx/icons/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/color.png -------------------------------------------------------------------------------- /src/gfx/icons/disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/disk.png -------------------------------------------------------------------------------- /src/gfx/icons/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/grid.png -------------------------------------------------------------------------------- /src/gfx/icons/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/guide.png -------------------------------------------------------------------------------- /src/gfx/icons/mouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/mouse.png -------------------------------------------------------------------------------- /src/gfx/icons/redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/redo.png -------------------------------------------------------------------------------- /src/gfx/icons/ruler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/ruler.png -------------------------------------------------------------------------------- /src/gfx/icons/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/undo.png -------------------------------------------------------------------------------- /src/gfx/icons/wand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/wand.png -------------------------------------------------------------------------------- /src/gfx/icons/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/water.png -------------------------------------------------------------------------------- /src/gfx/icons/Cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Cancel.png -------------------------------------------------------------------------------- /src/gfx/icons/Create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Create.png -------------------------------------------------------------------------------- /src/gfx/icons/Delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Delete.png -------------------------------------------------------------------------------- /src/gfx/icons/Forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Forward.png -------------------------------------------------------------------------------- /src/gfx/icons/Goback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Goback.png -------------------------------------------------------------------------------- /src/gfx/icons/Playback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Playback.png -------------------------------------------------------------------------------- /src/gfx/icons/Rewind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Rewind.png -------------------------------------------------------------------------------- /src/gfx/icons/arrow-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/arrow-in.png -------------------------------------------------------------------------------- /src/gfx/icons/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/border.png -------------------------------------------------------------------------------- /src/gfx/icons/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/document.png -------------------------------------------------------------------------------- /src/gfx/icons/grid-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/grid-dot.png -------------------------------------------------------------------------------- /src/gfx/icons/palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/palette.png -------------------------------------------------------------------------------- /src/gfx/icons/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/pencil.png -------------------------------------------------------------------------------- /src/gfx/icons/pipette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/pipette.png -------------------------------------------------------------------------------- /src/gfx/icons/shovel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/shovel.png -------------------------------------------------------------------------------- /src/gfx/icons/spectrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/spectrum.png -------------------------------------------------------------------------------- /src/gfx/icons/wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/wrench.png -------------------------------------------------------------------------------- /src/gfx/icons/zoom_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/zoom_in.png -------------------------------------------------------------------------------- /src/gfx/icons/zoom_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/zoom_out.png -------------------------------------------------------------------------------- /src/gfx/icons/Goforward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Goforward.png -------------------------------------------------------------------------------- /src/gfx/icons/Lastrecord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Lastrecord.png -------------------------------------------------------------------------------- /src/gfx/icons/Nexttrack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Nexttrack.png -------------------------------------------------------------------------------- /src/gfx/icons/border-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/border-all.png -------------------------------------------------------------------------------- /src/gfx/icons/categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/categories.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-flip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-flip.png -------------------------------------------------------------------------------- /src/gfx/icons/paint-can.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/paint-can.png -------------------------------------------------------------------------------- /src/gfx/icons/paint-tube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/paint-tube.png -------------------------------------------------------------------------------- /src/gfx/about_dialog_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/about_dialog_image.png -------------------------------------------------------------------------------- /src/gfx/icons/Fast-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Fast-forward.png -------------------------------------------------------------------------------- /src/gfx/icons/First-record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/First-record.png -------------------------------------------------------------------------------- /src/gfx/icons/arrow-in-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/arrow-in-out.png -------------------------------------------------------------------------------- /src/gfx/icons/block--arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/block--arrow.png -------------------------------------------------------------------------------- /src/gfx/icons/block--minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/block--minus.png -------------------------------------------------------------------------------- /src/gfx/icons/block--pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/block--pencil.png -------------------------------------------------------------------------------- /src/gfx/icons/block--plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/block--plus.png -------------------------------------------------------------------------------- /src/gfx/icons/block-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/block-share.png -------------------------------------------------------------------------------- /src/gfx/icons/border-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/border-down.png -------------------------------------------------------------------------------- /src/gfx/icons/border-inside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/border-inside.png -------------------------------------------------------------------------------- /src/gfx/icons/color-swatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/color-swatch.png -------------------------------------------------------------------------------- /src/gfx/icons/disks-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/disks-black.png -------------------------------------------------------------------------------- /src/gfx/icons/highlighter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/highlighter.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-resize.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-rotate.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-select.png -------------------------------------------------------------------------------- /src/gfx/icons/mouse-select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/mouse-select.png -------------------------------------------------------------------------------- /src/gfx/icons/paint-brush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/paint-brush.png -------------------------------------------------------------------------------- /src/gfx/icons/pencil-ruler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/pencil-ruler.png -------------------------------------------------------------------------------- /src/gfx/icons/Previousrecord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/Previousrecord.png -------------------------------------------------------------------------------- /src/gfx/icons/border-outside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/border-outside.png -------------------------------------------------------------------------------- /src/gfx/icons/document-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/document-block.png -------------------------------------------------------------------------------- /src/gfx/icons/document-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/document-export.png -------------------------------------------------------------------------------- /src/gfx/icons/layers-ungroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layers-ungroup.png -------------------------------------------------------------------------------- /src/gfx/icons/ruler-triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/ruler-triangle.png -------------------------------------------------------------------------------- /src/gfx/icons/weather-clear-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/weather-clear-2.png -------------------------------------------------------------------------------- /src/gfx/icons/zoom-original-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/zoom-original-4.png -------------------------------------------------------------------------------- /src/gfx/icons/application-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/application-block.png -------------------------------------------------------------------------------- /src/gfx/icons/application-resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/application-resize.png -------------------------------------------------------------------------------- /src/gfx/icons/block--exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/block--exclamation.png -------------------------------------------------------------------------------- /src/gfx/icons/disk-return-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/disk-return-black.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-rotate-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-rotate-left.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-select-point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-select-point.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-shape-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-shape-line.png -------------------------------------------------------------------------------- /src/gfx/icons/mouse-select-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/mouse-select-right.png -------------------------------------------------------------------------------- /src/gfx/icons/mouse-select-wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/mouse-select-wheel.png -------------------------------------------------------------------------------- /src/gfx/icons/transform-scale-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/transform-scale-2.png -------------------------------------------------------------------------------- /src/gfx/icons/wrench-screwdriver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/wrench-screwdriver.png -------------------------------------------------------------------------------- /src/gfx/icons/border-bottom-thick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/border-bottom-thick.png -------------------------------------------------------------------------------- /src/gfx/icons/border-outside-thick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/border-outside-thick.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-flip-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-flip-vertical.png -------------------------------------------------------------------------------- /src/gfx/icons/layer-resize-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layer-resize-actual.png -------------------------------------------------------------------------------- /src/gfx/icons/object-rotate-left-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/object-rotate-left-3.png -------------------------------------------------------------------------------- /src/gfx/icons/application-resize-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/application-resize-full.png -------------------------------------------------------------------------------- /src/gfx/icons/folder-horizontal-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/folder-horizontal-open.png -------------------------------------------------------------------------------- /src/gfx/icons/layers-alignment-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/layers-alignment-center.png -------------------------------------------------------------------------------- /src/gfx/icons/object-rotate-right-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/object-rotate-right-3.png -------------------------------------------------------------------------------- /src/gfx/icons/application-resize-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/zoxel/develop/src/gfx/icons/application-resize-actual.png -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pyside-rcc -o src/resources_rc.py src/resources.qrc 3 | src=. 4 | for file in $( find $src -name "*.ui") 5 | do 6 | out=`basename $file .ui` 7 | outdir=`dirname $file` 8 | pyside-uic $file -o ${outdir}/ui_${out}.py 9 | done 10 | 11 | cd src 12 | python zoxel.py 13 | -------------------------------------------------------------------------------- /freeze.bat: -------------------------------------------------------------------------------- 1 | cd src 2 | 3 | cxfreeze zoxel.py --target-dir ..\dist --base-name Win32GUI --include-modules atexit,PySide.QtNetwork,PySide.QtWebKit,OpenGL,OpenGL.platform.win32,OpenGL.arrays.nones,OpenGL.arrays.lists,OpenGL.arrays.strings,OpenGL.arrays.numbers,OpenGL.arrays.ctypesarrays,OpenGL.arrays.ctypesparameters,OpenGL.arrays.ctypespointers --include-path . 4 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | c:\Python27\lib\site-packages\PySide\pyside-rcc.exe src\resources.qrc -o src\resources_rc.py 3 | call :iter 4 | cd src 5 | python zoxel.py 6 | cd .. 7 | goto :eof 8 | 9 | :iter 10 | for %%f in (*.ui) do pyside-uic %%~dpnxf -o %%~dpf\ui_%%~nf.py 11 | for /D %%d in (*) do ( 12 | cd %%d 13 | call :iter 14 | cd .. 15 | ) 16 | exit /b 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.py~ 3 | *.zox 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | dist/ 12 | 13 | # Unit test / coverage reports 14 | .coverage 15 | .tox 16 | nosetests.xml 17 | 18 | # IDE 19 | *.e4p 20 | .ropeproject 21 | .eric4project 22 | .settings 23 | .project 24 | .pydevproject 25 | .externalToolBuilders 26 | 27 | # Log files 28 | *.log 29 | 30 | # UI stuff 31 | resources_rc.py 32 | ui_*.py 33 | 34 | -------------------------------------------------------------------------------- /src/plugin_loader.py: -------------------------------------------------------------------------------- 1 | # Load these plugins 2 | # This could (should) be replaced by dynamic module loading, but we do it 3 | # this way to support cx-freeze on Windows. 4 | import plugins.tool_draw 5 | import plugins.tool_paint 6 | import plugins.tool_erase 7 | import plugins.tool_drag 8 | import plugins.io_zoxel 9 | import plugins.io_sproxel 10 | import plugins.tool_fill 11 | import plugins.tool_colourpick 12 | import plugins.io_obj 13 | import plugins.io_qubicle 14 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Zoxel 2 | A cross-platform editor for small voxel models. 3 | Copyright (c) 2013-2014, Graham R King. 4 | http://zoxel.blogspot.com 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | -------------------------------------------------------------------------------- /src/constants.py: -------------------------------------------------------------------------------- 1 | # constants.py 2 | # Zoxel constants 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | ZOXEL_VERSION = "0.5.0 (20th January 2014)" 19 | -------------------------------------------------------------------------------- /src/dialog_about.py: -------------------------------------------------------------------------------- 1 | # about_dialog.py 2 | # A basic About dialog. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | from ui_dialog_about import Ui_AboutDialog 19 | from constants import ZOXEL_VERSION 20 | 21 | class AboutDialog(QtGui.QDialog): 22 | def __init__(self, parent=None): 23 | # Initialise the UI 24 | super(AboutDialog, self).__init__(parent) 25 | self.ui = Ui_AboutDialog() 26 | self.ui.setupUi(self) 27 | self.ui.ver_label.setText("Version %s" % ZOXEL_VERSION) 28 | -------------------------------------------------------------------------------- /src/zoxel.py: -------------------------------------------------------------------------------- 1 | # mainwindow.py 2 | # Zoxel - A Voxel Editor 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import sys 19 | from PySide import QtGui 20 | from mainwindow import MainWindow 21 | 22 | def main(): 23 | # create application 24 | app = QtGui.QApplication(sys.argv) 25 | 26 | # create mainWindow 27 | mainwindow = MainWindow() 28 | mainwindow.show() 29 | 30 | # Remember our main window 31 | app.mainwindow = mainwindow 32 | 33 | # Load system plugins 34 | mainwindow.load_plugins() 35 | 36 | # run main loop 37 | sys.exit(app.exec_()) 38 | 39 | # call main function 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /src/dialog_resize.py: -------------------------------------------------------------------------------- 1 | # about_resize.py 2 | # Prompt for resize model dimensions. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | from ui_dialog_resize import Ui_ResizeDialog 19 | 20 | class ResizeDialog(QtGui.QDialog): 21 | def __init__(self, parent=None): 22 | # Initialise the UI 23 | super(ResizeDialog, self).__init__(parent) 24 | self.ui = Ui_ResizeDialog() 25 | self.ui.setupUi(self) 26 | self.ui.button_auto.clicked.connect(self.on_button_auto_clicked) 27 | 28 | def on_button_auto_clicked(self): 29 | _,_,_,x,y,z = self.parent().display.voxels.get_bounding_box() 30 | self.ui.width.setValue(x) 31 | self.ui.height.setValue(y) 32 | self.ui.depth.setValue(z) 33 | -------------------------------------------------------------------------------- /src/plugins/tool_erase.py: -------------------------------------------------------------------------------- 1 | # tool_erase.py 2 | # Simple voxel removal tool 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | from tool import Tool 19 | from plugin_api import register_plugin 20 | 21 | class EraseTool(Tool): 22 | 23 | def __init__(self, api): 24 | super(EraseTool, self).__init__(api) 25 | # Create our action / icon 26 | self.action = QtGui.QAction( 27 | QtGui.QPixmap(":/images/gfx/icons/shovel.png"), 28 | "Erase", None) 29 | self.action.setStatusTip("Erase voxels") 30 | self.action.setCheckable(True) 31 | # Register the tool 32 | self.api.register_tool(self) 33 | 34 | # Clear the targeted voxel 35 | def on_mouse_click(self, target): 36 | target.voxels.set(target.world_x, target.world_y, target.world_z, 0) 37 | 38 | register_plugin(EraseTool, "Erasing Tool", "1.0") -------------------------------------------------------------------------------- /win32-installer.iss: -------------------------------------------------------------------------------- 1 | ; Inno install packager 2 | 3 | [Setup] 4 | ; NOTE: The value of AppId uniquely identifies this application. 5 | ; Do not use the same AppId value in installers for other applications. 6 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 7 | AppId={{02032A04-CEBD-4E65-9433-A700D205AC32} 8 | AppName=Zoxel 9 | #define VERSION "0.4.5" 10 | AppVersion={#VERSION} 11 | AppPublisher=Graham R King 12 | AppPublisherURL=http://zoxel.blogspot.co.uk 13 | AppSupportURL=http://zoxel.blogspot.co.uk 14 | AppUpdatesURL=http://zoxel.blogspot.co.uk 15 | DefaultDirName={pf}\Zoxel 16 | DefaultGroupName=Zoxel 17 | OutputDir=C:\Source\zoxel\install 18 | OutputBaseFilename=zoxel-v{#VERSION} 19 | Compression=lzma 20 | SolidCompression=yes 21 | 22 | [Languages] 23 | Name: "english"; MessagesFile: "compiler:Default.isl" 24 | 25 | [Tasks] 26 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 27 | 28 | [Files] 29 | Source: "C:\Source\zoxel\dist\zoxel.exe"; DestDir: "{app}"; Flags: ignoreversion 30 | Source: "C:\Source\zoxel\dist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 31 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 32 | 33 | [Icons] 34 | Name: "{group}\Zoxel"; Filename: "{app}\zoxel.exe" 35 | Name: "{group}\{cm:UninstallProgram,Zoxel}"; Filename: "{uninstallexe}" 36 | Name: "{commondesktop}\Zoxel"; Filename: "{app}\zoxel.exe"; Tasks: desktopicon 37 | 38 | [Run] 39 | Filename: "{app}\zoxel.exe"; Description: "{cm:LaunchProgram,Zoxel}"; Flags: nowait postinstall skipifsilent 40 | 41 | -------------------------------------------------------------------------------- /src/plugins/tool_colourpick.py: -------------------------------------------------------------------------------- 1 | # tool_colourpick.py 2 | # Simple colour picking tool. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | from tool import Tool 19 | from plugin_api import register_plugin 20 | 21 | class ColourPickTool(Tool): 22 | 23 | def __init__(self, api): 24 | super(ColourPickTool, self).__init__(api) 25 | # Create our action / icon 26 | self.action = QtGui.QAction( 27 | QtGui.QPixmap(":/images/gfx/icons/pipette.png"), 28 | "Colour Pick", None) 29 | self.action.setStatusTip("Choose a colour from an existing voxel.") 30 | self.action.setCheckable(True) 31 | # Register the tool 32 | self.api.register_tool(self) 33 | 34 | # Grab the colour of the selected voxel 35 | def on_mouse_click(self, target): 36 | # If we have a voxel at the target, colour it 37 | voxel = target.voxels.get(target.world_x, target.world_y, target.world_z) 38 | if voxel: 39 | self.api.set_palette_colour(voxel) 40 | 41 | register_plugin(ColourPickTool, "Colour Picking Tool", "1.0") -------------------------------------------------------------------------------- /src/plugins/tool_paint.py: -------------------------------------------------------------------------------- 1 | # tool_paint.py 2 | # Simple painting tool. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | from tool import Tool, EventData, MouseButtons, KeyModifiers, Face 19 | from plugin_api import register_plugin 20 | 21 | class PaintingTool(Tool): 22 | 23 | def __init__(self, api): 24 | super(PaintingTool, self).__init__(api) 25 | # Create our action / icon 26 | self.action = QtGui.QAction( 27 | QtGui.QPixmap(":/images/gfx/icons/paint-brush.png"), 28 | "Paint", None) 29 | self.action.setStatusTip("Colour Voxels") 30 | self.action.setCheckable(True) 31 | # Register the tool 32 | self.api.register_tool(self) 33 | 34 | # Colour the targeted voxel 35 | def on_mouse_click(self, data): 36 | # If we have a voxel at the target, colour it 37 | voxel = data.voxels.get(data.world_x, data.world_y, data.world_z) 38 | if voxel: 39 | data.voxels.set(data.world_x, data.world_y, data.world_z, self.colour) 40 | 41 | # Colour when dragging also 42 | def on_drag(self, data): 43 | self.on_mouse_click(data) 44 | 45 | register_plugin(PaintingTool, "Painting Tool", "1.0") -------------------------------------------------------------------------------- /src/gfx/icons/README.txt: -------------------------------------------------------------------------------- 1 | Animation icons by Aha-Soft ( http://www.small-icons.com/ ) 2 | Zoom in and Zoom out icons by http://ashung.deviantart.com/ 3 | Undo and Redo icons by http://turbomilk.com/ 4 | 5 | Fugue Icons 6 | 7 | (C) 2013 Yusuke Kamiyamane. All rights reserved. 8 | 9 | These icons are licensed under a Creative Commons 10 | Attribution 3.0 License. 11 | 12 | 13 | If you can't or don't want to provide attribution, please 14 | purchase a royalty-free license. 15 | 16 | 17 | I'm unavailable for custom icon design work. But your 18 | suggestions are always welcome! 19 | 20 | 21 | ------------------------------------------------------------ 22 | 23 | All logos and trademarks in some icons are property of their 24 | respective owners. 25 | 26 | ------------------------------------------------------------ 27 | 28 | - geotag 29 | 30 | (C) Geotag Icon Project. All rights reserved. 31 | 32 | 33 | Geotag icon is licensed under a Creative Commons 34 | Attribution-Share Alike 3.0 License or LGPL. 35 | 36 | 37 | 38 | - language 39 | 40 | (C) Language Icon Project. All rights reserved. 41 | 42 | 43 | Language icon is licensed under a Creative Commons 44 | Attribution-Share Alike 3.0 License. 45 | 46 | 47 | - open-share 48 | 49 | (C) Open Share Icon Project. All rights reserved. 50 | 51 | 52 | Open Share icon is licensed under a Creative Commons 53 | Attribution-Share Alike 3.0 License. 54 | 55 | 56 | - opml 57 | 58 | (C) OPML Icon Project. All rights reserved. 59 | 60 | 61 | OPML icon is licensed under a Creative Commons 62 | Attribution-Share Alike 2.5 License. 63 | 64 | 65 | - share 66 | 67 | (C) Share Icon Project. All rights reserved. 68 | 69 | 70 | Share icon is licensed under a GPL or LGPL or BSD or 71 | Creative Commons Attribution 2.5 License. 72 | 73 | 74 | 75 | 76 | 77 | - xfn 78 | 79 | (C) Wolfgang Bartelme. All rights reserved. 80 | 81 | 82 | XFN icon is licensed under a Creative Commons 83 | Attribution-Share Alike 2.5 License. 84 | -------------------------------------------------------------------------------- /src/plugins/tool_fill.py: -------------------------------------------------------------------------------- 1 | # tool_floodfill.py 2 | # Simple tool for flood fill. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | from tool import Tool, EventData, MouseButtons, KeyModifiers, Face 19 | from plugin_api import register_plugin 20 | 21 | class FillTool(Tool): 22 | 23 | def __init__(self, api): 24 | super(FillTool, self).__init__(api) 25 | # Create our action / icon 26 | self.action = QtGui.QAction( 27 | QtGui.QPixmap(":/images/gfx/icons/paint-can.png"), 28 | "Fill", None) 29 | self.action.setStatusTip("Flood fill with colour") 30 | self.action.setCheckable(True) 31 | # Register the tool 32 | self.api.register_tool(self) 33 | 34 | # Fill all connected voxels of the same colour with a new colour 35 | def on_mouse_click(self, target): 36 | # We need to have a selected voxel 37 | voxel = target.voxels.get(target.world_x, target.world_y, target.world_z) 38 | if not voxel: 39 | return 40 | # Grab the target colour 41 | search_colour = voxel 42 | # Don't allow invalid fills 43 | c = self.colour.getRgb() 44 | fill_colour = c[0]<<24 | c[1]<<16 | c[2]<<8 | 0xff 45 | if search_colour == fill_colour: 46 | return 47 | # Initialise our search list 48 | search = [] 49 | search.append((target.world_x, target.world_y, target.world_z)) 50 | # Keep iterating over the search list until no more to do 51 | while len(search): 52 | x,y,z = search.pop() 53 | voxel = target.voxels.get(x, y, z) 54 | if not voxel or voxel != search_colour: 55 | continue 56 | # Add all likely neighbours into our search list 57 | if target.voxels.get(x-1,y,z) == search_colour: 58 | search.append((x-1,y,z)) 59 | if target.voxels.get(x+1,y,z) == search_colour: 60 | search.append((x+1,y,z)) 61 | if target.voxels.get(x,y+1,z) == search_colour: 62 | search.append((x,y+1,z)) 63 | if target.voxels.get(x,y-1,z) == search_colour: 64 | search.append((x,y-1,z)) 65 | if target.voxels.get(x,y,z+1) == search_colour: 66 | search.append((x,y,z+1)) 67 | if target.voxels.get(x,y,z-1) == search_colour: 68 | search.append((x,y,z-1)) 69 | # Set the colour of the current voxel 70 | target.voxels.set(x, y, z, self.colour) 71 | 72 | register_plugin(FillTool, "Fill Tool", "1.0") -------------------------------------------------------------------------------- /src/plugin_api.py: -------------------------------------------------------------------------------- 1 | # plugin_api.py 2 | # API for system plugins. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | 19 | class PluginManager(object): 20 | plugins = [] 21 | 22 | class PluginAPI(object): 23 | 24 | def __init__(self): 25 | # All plugins get a reference to our application 26 | self.application = QtGui.QApplication.instance() 27 | # And our main window 28 | self.mainwindow = self.application.mainwindow 29 | 30 | # Register a drawing tool with the system 31 | def register_tool(self, tool, activate = False): 32 | # Create an instance 33 | self.mainwindow.register_tool(tool, activate) 34 | 35 | # Register an importer/exporter with the system 36 | def register_file_handler(self, handler): 37 | self.mainwindow.register_file_handler(handler) 38 | 39 | # Get the currently selected colour from the palette 40 | def get_palette_colour(self): 41 | return self.mainwindow.display.voxel_colour 42 | 43 | # Changee the GUI palette to the given colour. 44 | # Accepts QColors and 32bit integer RGBA 45 | def set_palette_colour(self, colour): 46 | self.mainwindow.colour_palette.colour = colour 47 | 48 | # Returns the current voxel data 49 | def get_voxel_data(self): 50 | return self.mainwindow.display.voxels 51 | 52 | # Returns the current voxel model mesh data 53 | # vertices, colours, normals 54 | def get_voxel_mesh(self): 55 | vert, col, norm, _, _ = self.mainwindow.display.voxels.get_vertices() 56 | return (vert, col, norm) 57 | 58 | # Get and set persistent config values. value can be any serialisable type. 59 | # name should be a hashable type, like a simple string. 60 | def set_config(self, name, value): 61 | self.api.mainwindow.set_setting(name, value) 62 | def get_config(self, name): 63 | return self.api.mainwindow.get_setting(name) 64 | 65 | # Display a warning message 66 | def warning(self, message): 67 | QtGui.QMessageBox.warning(self.mainwindow, "Warning", message) 68 | 69 | # Plugin registration 70 | # Plugins call this function to register with the system. A plugin 71 | # should pass the class which will be instaniated by the application, 72 | # this constructor is passed an instance of the system plugin API. 73 | def register_plugin(plugin_class, name, version): 74 | # Create an instance of the API to send to the plugin 75 | # Plugins access the main app via this API instance 76 | api = PluginAPI() 77 | plugin = plugin_class(api) 78 | PluginManager.plugins.append(plugin) 79 | return api 80 | -------------------------------------------------------------------------------- /src/undo.py: -------------------------------------------------------------------------------- 1 | # undo.py 2 | # An undo buffer. 3 | # Copyright (c) 2014, Graham R King & Bruno F Canella 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | class UndoItem(object): 19 | 20 | @property 21 | def operation(self): 22 | return self._operation 23 | 24 | @property 25 | def olddata(self): 26 | return self._olddata 27 | 28 | @property 29 | def newdata(self): 30 | return self._newdata 31 | 32 | def __init__(self, operation, olddata, newdata): 33 | self._operation = operation 34 | self._olddata = olddata 35 | self._newdata = newdata 36 | 37 | class Undo(object): 38 | 39 | # Types of operation 40 | SET_VOXEL = 1 41 | TRANSLATE = 2 42 | 43 | @property 44 | def enabled(self): 45 | return self._enabled 46 | @enabled.setter 47 | def enabled(self, value): 48 | self._enabled = value 49 | 50 | @property 51 | def frame(self): 52 | return self._frame 53 | @frame.setter 54 | def frame(self, value): 55 | self._frame = value 56 | 57 | def __init__(self): 58 | self._enabled = True 59 | self.clear() 60 | 61 | def add_frame(self, pos): 62 | self._buffer.insert(pos, []) 63 | self._ptr.insert(pos, -1) 64 | 65 | def delete_frame(self, pos): 66 | del self._buffer[pos] 67 | del self._ptr[pos] 68 | 69 | def add(self, item): 70 | if not self._enabled: 71 | return 72 | # Clear future if we're somewhere in the middle of the undo history 73 | if self._ptr[self._frame] < len(self._buffer[self._frame])-1: 74 | self._buffer[self._frame] = self._buffer[self._frame][:self._ptr[self._frame]+1] 75 | self._buffer[self._frame].append(item) 76 | self._ptr[self._frame] = len(self._buffer[self._frame])-1 77 | 78 | def _valid_buffer(self): 79 | return len(self._buffer[self._frame]) > 0 80 | 81 | def undo(self): 82 | if not self._valid_buffer(): 83 | return 84 | item = self._buffer[self._frame][self._ptr[self._frame]] 85 | self._ptr[self._frame] -= 1 86 | if self._ptr[self._frame] < -1: 87 | self._ptr[self._frame] = -1 88 | return item 89 | 90 | def redo(self): 91 | if not self._valid_buffer(): 92 | return 93 | self._ptr[self._frame] += 1 94 | item = None 95 | if self._ptr[self._frame] > len(self._buffer[self._frame])-1: 96 | self._ptr[self._frame] = len(self._buffer[self._frame])-1 97 | else: 98 | item = self._buffer[self._frame][self._ptr[self._frame]] 99 | return item 100 | 101 | def clear(self): 102 | self._buffer = [[]] 103 | self._ptr = [-1] 104 | self._frame = 0 105 | -------------------------------------------------------------------------------- /src/plugins/io_sproxel.py: -------------------------------------------------------------------------------- 1 | # io_sproxel.py 2 | # Sproxel import/exporter 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from plugin_api import register_plugin 18 | 19 | class SproxelFile(object): 20 | 21 | # Description of file type 22 | description = "Sproxel Files" 23 | 24 | # File type filter 25 | filetype = "*.csv" 26 | 27 | def __init__(self, api): 28 | self.api = api 29 | # Register our exporter 30 | self.api.register_file_handler(self) 31 | 32 | # Called when we need to save. Should raise an exception if there is a 33 | # problem saving. 34 | def save(self, filename): 35 | # grab the voxel data 36 | voxels = self.api.get_voxel_data() 37 | 38 | # Open our file 39 | f = open(filename,"wt") 40 | 41 | # First Sproxel line is model dimenstions 42 | f.write("%i,%i,%i\n" % (voxels.width, voxels.height, voxels.depth)) 43 | 44 | # Then we save from the top of the model 45 | for y in xrange(voxels.height-1, -1, -1): 46 | for z in xrange(voxels.depth-1, -1, -1): 47 | line = [] 48 | for x in xrange(voxels.width): 49 | voxel = voxels.get(x, y, z) 50 | if voxel == 0: 51 | line.append("#00000000") 52 | else: 53 | voxel = (voxel & 0xffffff00) | 0xff 54 | voxel = "%x" % voxel 55 | line.append("#"+voxel.upper().rjust(8,"0")) 56 | f.write(",".join(line)+"\n") 57 | f.write("\n") 58 | 59 | # Tidy up 60 | f.close() 61 | 62 | # Load a Sproxel file 63 | def load(self, filename): 64 | # grab the voxel data 65 | voxels = self.api.get_voxel_data() 66 | 67 | # Open our file 68 | f = open(filename,"rt") 69 | size = f.readline().strip() 70 | x,y,z = size.split(",") 71 | x = int(x) 72 | y = int(y) 73 | z = int(z) 74 | voxels.resize(x, y, z) 75 | # Parse the file 76 | for fy in xrange(y-1,-1,-1): 77 | for fz in xrange(z-1,-1,-1): 78 | line = f.readline().strip().split(",") 79 | for fx in xrange(0, x): 80 | if line[fx] == "#00000000": 81 | continue 82 | colour = line[fx][1:] 83 | r = colour[:2] 84 | g = colour[2:4] 85 | b = colour[4:6] 86 | r = int(r, 16) 87 | g = int(g, 16) 88 | b = int(b, 16) 89 | a = 0xff 90 | v = r<<24 | g<<16 | b<<8 | a 91 | voxels.set(fx, fy, fz, v) 92 | 93 | f.readline() # discard empty line 94 | f.close() 95 | 96 | register_plugin(SproxelFile, "Sproxel file format IO", "1.0") 97 | -------------------------------------------------------------------------------- /src/plugins/tool_drag.py: -------------------------------------------------------------------------------- 1 | # tool_drag.py 2 | # Model moving tool. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | from tool import Tool, EventData, MouseButtons, KeyModifiers, Face 19 | from plugin_api import register_plugin 20 | 21 | class DragTool(Tool): 22 | 23 | def __init__(self, api): 24 | super(DragTool, self).__init__(api) 25 | # Create our action / icon 26 | self.action = QtGui.QAction( 27 | QtGui.QPixmap(":/images/gfx/icons/arrow-in-out.png"), 28 | "Move Model", None) 29 | self.action.setStatusTip("Move Model") 30 | self.action.setCheckable(True) 31 | # Register the tool 32 | self.api.register_tool(self) 33 | 34 | # Colour the targeted voxel 35 | def on_drag_start(self, target): 36 | self._mouse = (target.mouse_x, target.mouse_y) 37 | 38 | # Drag the model in voxel space 39 | def on_drag(self, target): 40 | dx = target.mouse_x - self._mouse[0] 41 | dy = target.mouse_y - self._mouse[1] 42 | # Work out some sort of vague translation between screen and voxels 43 | sx = self.api.mainwindow.width() / target.voxels.width 44 | sy = self.api.mainwindow.height() / target.voxels.height 45 | dx = int(round(dx / float(sx))) 46 | dy = int(round(dy / float(sy))) 47 | # Work out translation for x,y 48 | ax, ay = self.api.mainwindow.display.view_axis() 49 | tx = 0 50 | ty = 0 51 | tz = 0 52 | if ax == self.api.mainwindow.display.X_AXIS: 53 | if dx > 0: 54 | tx = 1 55 | elif dx < 0: 56 | tx = -1 57 | if ax == self.api.mainwindow.display.Y_AXIS: 58 | if dx > 0: 59 | ty = 1 60 | elif dx < 0: 61 | ty = -1 62 | if ax == self.api.mainwindow.display.Z_AXIS: 63 | if dx > 0: 64 | tz = 1 65 | elif dx < 0: 66 | tz = -1 67 | if ay == self.api.mainwindow.display.X_AXIS: 68 | if dy > 0: 69 | tx = 1 70 | elif dy < 0: 71 | tx = -1 72 | if ay == self.api.mainwindow.display.Y_AXIS: 73 | if dy > 0: 74 | ty = -1 75 | elif dy < 0: 76 | ty = 1 77 | if ay == self.api.mainwindow.display.Z_AXIS: 78 | if dy > 0: 79 | tz = 1 80 | elif dy < 0: 81 | tz = -1 82 | 83 | if ty != 0 or tx != 0 or tz != 0: 84 | self._mouse = (target.mouse_x, target.mouse_y) 85 | 86 | target.voxels.translate(tx, ty, tz) 87 | 88 | register_plugin(DragTool, "Drag Tool", "1.0") -------------------------------------------------------------------------------- /src/plugins/io_obj.py: -------------------------------------------------------------------------------- 1 | # io_obj.py 2 | # Export mesh to OBJ format 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import os 18 | from plugin_api import register_plugin 19 | 20 | class ObjFile(object): 21 | 22 | # Description of file type 23 | description = "OBJ Files" 24 | 25 | # File type filter 26 | filetype = "*.obj" 27 | 28 | def __init__(self, api): 29 | self.api = api 30 | # Register our exporter 31 | self.api.register_file_handler(self) 32 | 33 | # Called when we need to save. Should raise an exception if there is a 34 | # problem saving. 35 | def save(self, filename): 36 | # grab the voxel data 37 | vertices, colours, _ = self.api.get_voxel_mesh() 38 | 39 | # Open our file 40 | f = open(filename,"wt") 41 | 42 | # Use materials 43 | mat_pathname, mat_filename = os.path.split(filename) 44 | name, ext = os.path.splitext(mat_filename) 45 | if not ext: 46 | filename = filename+'.obj' 47 | mat_filename = os.path.join(mat_pathname, name)+".mtl" 48 | f.write("mtllib %s\r\n" % mat_filename) 49 | 50 | # Export vertices 51 | i = 0 52 | while i < len(vertices): 53 | f.write("v %f %f %f\r\n" % 54 | (vertices[i], vertices[i+1], vertices[i+2])) 55 | i += 3 56 | 57 | # Build a list of unique colours we use so we can assign materials 58 | mats = {} 59 | i = 0 60 | while i < len(colours): 61 | r = colours[i] 62 | g = colours[i+1] 63 | b = colours[i+2] 64 | colour = r<<24 | g<<16 | b<<8 65 | if colour not in mats: 66 | mats[colour] = "material_%i" % len(mats) 67 | i += 3 68 | 69 | # Export faces 70 | faces = (len(vertices)//(3*3))//2 71 | for i in xrange(faces): 72 | n = 1+(i * 6) 73 | r = colours[(i*18)] 74 | g = colours[(i*18)+1] 75 | b = colours[(i*18)+2] 76 | colour = r<<24 | g<<16 | b<<8 77 | f.write("usemtl %s\r\n" % mats[colour]) 78 | f.write("f %i %i %i\r\n" % (n, n+2, n+1)) 79 | f.write("f %i %i %i\r\n" % (n+5, n+4, n+3)) 80 | 81 | # Tidy up 82 | f.close() 83 | 84 | # Create our material file 85 | f = open(mat_filename,"wt") 86 | for colour, material in mats.items(): 87 | f.write("newmtl %s\r\n" % material) 88 | r = (colour & 0xff000000) >> 24 89 | g = (colour & 0xff0000) >> 16 90 | b = (colour & 0xff00) >> 8 91 | r = r / 255.0 92 | g = g / 255.0 93 | b = b / 255.0 94 | f.write("Ka %f %f %f\r\n" % (r, g, b)) 95 | f.write("Kd %f %f %f\r\n" % (r, g, b)) 96 | f.close() 97 | 98 | 99 | register_plugin(ObjFile, "OBJ exporter", "1.0") 100 | -------------------------------------------------------------------------------- /src/plugins/tool_draw.py: -------------------------------------------------------------------------------- 1 | # tool_draw.py 2 | # Simple drawing tool. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtGui 18 | from tool import Tool, EventData, MouseButtons, KeyModifiers, Face 19 | from plugin_api import register_plugin 20 | 21 | class DrawingTool(Tool): 22 | 23 | def __init__(self, api): 24 | super(DrawingTool, self).__init__(api) 25 | # Create our action / icon 26 | self.action = QtGui.QAction( 27 | QtGui.QPixmap(":/images/gfx/icons/pencil.png"), 28 | "Draw", None) 29 | self.action.setStatusTip("Draw Voxels") 30 | self.action.setCheckable(True) 31 | # Register the tool 32 | self.api.register_tool(self, True) 33 | 34 | # Tries to plot a new voxel at target location. 35 | # param choosen_target: The place where the new voxel should be inserted. 36 | # returns A Target object indicating the actual place where the voxel were 37 | # inserted. Returns None when no insertion was made. 38 | def _draw_voxel(self, target): 39 | # Works out where exactly the new voxel goes. It can collide with an existing voxel or with the bottom of the 'y' plane, 40 | #in which case, pos will be different than None. 41 | pos = target.get_neighbour() 42 | if pos: 43 | target.world_x = pos[0] 44 | target.world_y = pos[1] 45 | target.world_z = pos[2] 46 | # Tries to set the voxel on the matrix and then returns the Target 47 | # with it's coordinates, if it exists. 48 | if( target.voxels.set(target.world_x, target.world_y, target.world_z, 49 | self.colour) ): 50 | return target 51 | else: 52 | return None 53 | 54 | def _get_valid_sequence_faces(self, face): 55 | if( face in Face.COLLIDABLE_FACES_PLANE_X ): 56 | return Face.COLLIDABLE_FACES_PLANE_Y + Face.COLLIDABLE_FACES_PLANE_Z 57 | elif( face in Face.COLLIDABLE_FACES_PLANE_Y ): 58 | return Face.COLLIDABLE_FACES_PLANE_X + Face.COLLIDABLE_FACES_PLANE_Z 59 | elif( face in Face.COLLIDABLE_FACES_PLANE_Z ): 60 | return Face.COLLIDABLE_FACES_PLANE_X + Face.COLLIDABLE_FACES_PLANE_Y 61 | else: 62 | return None 63 | 64 | # Draw a new voxel next to the targeted face 65 | def on_mouse_click(self, data): 66 | if data.mouse_button == MouseButtons.LEFT: 67 | self._first_target = self._draw_voxel(data) 68 | elif data.mouse_button == MouseButtons.RIGHT: 69 | data.voxels.set(data.world_x, data.world_y, data.world_z, 0) 70 | 71 | # Start a drag 72 | def on_drag_start(self, data): 73 | self._first_target = data 74 | 75 | # When dragging, Draw a new voxel next to the targeted face 76 | def on_drag(self, data): 77 | # In case the first click has missed a valid target. 78 | if( self._first_target is None ): 79 | return 80 | valid_faces = self._get_valid_sequence_faces(self._first_target.face) 81 | if( ( not valid_faces ) or ( data.face not in valid_faces ) ): 82 | return 83 | self._draw_voxel(data) 84 | 85 | register_plugin(DrawingTool, "Drawing Tool", "1.0") -------------------------------------------------------------------------------- /src/dialog_about.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AboutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 456 10 | 417 11 | 12 | 13 | 14 | About 15 | 16 | 17 | 18 | 19 | 0 20 | 0 21 | 461 22 | 421 23 | 24 | 25 | 26 | 27 | 0 28 | 0 29 | 30 | 31 | 32 | 33 | 34 | 35 | :/images/gfx/about_dialog_image.png 36 | 37 | 38 | Qt::NoTextInteraction 39 | 40 | 41 | 42 | 43 | 44 | 10 45 | 300 46 | 91 47 | 41 48 | 49 | 50 | 51 | 52 | 20 53 | 75 54 | true 55 | 56 | 57 | 58 | Zoxel 59 | 60 | 61 | 62 | 63 | 64 | 10 65 | 350 66 | 301 67 | 16 68 | 69 | 70 | 71 | Copyright (c) 2013-2014, Graham R King 72 | 73 | 74 | 75 | 76 | 77 | 10 78 | 330 79 | 131 80 | 16 81 | 82 | 83 | 84 | Voxel Editor 85 | 86 | 87 | 88 | 89 | 90 | 10 91 | 390 92 | 211 93 | 16 94 | 95 | 96 | 97 | Version 0.0.1 (7th March 2013) 98 | 99 | 100 | 101 | 102 | 103 | 360 104 | 380 105 | 87 106 | 27 107 | 108 | 109 | 110 | OK 111 | 112 | 113 | true 114 | 115 | 116 | 117 | 118 | 119 | 10 120 | 370 121 | 231 122 | 16 123 | 124 | 125 | 126 | <a href="http://zoxel.blogspot.com">http://zoxel.blogspot.com</a> 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | button 136 | clicked() 137 | AboutDialog 138 | accept() 139 | 140 | 141 | 403 142 | 393 143 | 144 | 145 | 227 146 | 208 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /src/dialog_resize.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ResizeDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 345 10 | 193 11 | 12 | 13 | 14 | Model Size 15 | 16 | 17 | 18 | 19 | 20 | QFrame::NoFrame 21 | 22 | 23 | QFrame::Plain 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Enter the model dimensions. 32 | 33 | 34 | 35 | 36 | 37 | 38 | Get Bounding Box 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 1 53 | 54 | 55 | 56 | 57 | 58 | 59 | Width 60 | 61 | 62 | 63 | 64 | 65 | 66 | 1 67 | 68 | 69 | 70 | 71 | 72 | 73 | Height 74 | 75 | 76 | 77 | 78 | 79 | 80 | 1 81 | 82 | 83 | 84 | 85 | 86 | 87 | Depth 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | Qt::Horizontal 97 | 98 | 99 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 100 | 101 | 102 | 103 | 104 | 105 | 106 | width 107 | height 108 | depth 109 | buttonBox 110 | 111 | 112 | 113 | 114 | buttonBox 115 | accepted() 116 | ResizeDialog 117 | accept() 118 | 119 | 120 | 248 121 | 254 122 | 123 | 124 | 157 125 | 274 126 | 127 | 128 | 129 | 130 | buttonBox 131 | rejected() 132 | ResizeDialog 133 | reject() 134 | 135 | 136 | 316 137 | 260 138 | 139 | 140 | 286 141 | 274 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | gfx/icons/Stop.png 4 | gfx/icons/Rewind.png 5 | gfx/icons/Previousrecord.png 6 | gfx/icons/Playback.png 7 | gfx/icons/Play.png 8 | gfx/icons/Pause.png 9 | gfx/icons/Nexttrack.png 10 | gfx/icons/Last.png 11 | gfx/icons/Lastrecord.png 12 | gfx/icons/Go.png 13 | gfx/icons/Goforward.png 14 | gfx/icons/Goback.png 15 | gfx/icons/Forward.png 16 | gfx/icons/First-record.png 17 | gfx/icons/First.png 18 | gfx/icons/Fast-forward.png 19 | gfx/icons/Delete.png 20 | gfx/icons/Create.png 21 | gfx/icons/Cancel.png 22 | gfx/icons/Back.png 23 | gfx/icons/Add.png 24 | gfx/icons/zoom_in.png 25 | gfx/icons/zoom_out.png 26 | gfx/icons/redo.png 27 | gfx/icons/undo.png 28 | gfx/texture.png 29 | gfx/about_dialog_image.png 30 | gfx/icons/application-block.png 31 | gfx/icons/application-resize-actual.png 32 | gfx/icons/application-resize-full.png 33 | gfx/icons/application-resize.png 34 | gfx/icons/arrow-in-out.png 35 | gfx/icons/arrow-in.png 36 | gfx/icons/block--arrow.png 37 | gfx/icons/block--exclamation.png 38 | gfx/icons/block--minus.png 39 | gfx/icons/block--pencil.png 40 | gfx/icons/block--plus.png 41 | gfx/icons/block-share.png 42 | gfx/icons/block.png 43 | gfx/icons/border-all.png 44 | gfx/icons/border-bottom-thick.png 45 | gfx/icons/border-down.png 46 | gfx/icons/border-inside.png 47 | gfx/icons/border-outside-thick.png 48 | gfx/icons/border-outside.png 49 | gfx/icons/border.png 50 | gfx/icons/categories.png 51 | gfx/icons/color-swatch.png 52 | gfx/icons/color.png 53 | gfx/icons/disk-return-black.png 54 | gfx/icons/disk.png 55 | gfx/icons/disks-black.png 56 | gfx/icons/document-block.png 57 | gfx/icons/document-export.png 58 | gfx/icons/document.png 59 | gfx/icons/eye.png 60 | gfx/icons/folder-horizontal-open.png 61 | gfx/icons/grid-dot.png 62 | gfx/icons/grid.png 63 | gfx/icons/guide.png 64 | gfx/icons/highlighter.png 65 | gfx/icons/layer-flip-vertical.png 66 | gfx/icons/layer-flip.png 67 | gfx/icons/layer-resize-actual.png 68 | gfx/icons/layer-resize.png 69 | gfx/icons/layer-rotate-left.png 70 | gfx/icons/layer-rotate.png 71 | gfx/icons/layer-select-point.png 72 | gfx/icons/layer-select.png 73 | gfx/icons/layer-shape-line.png 74 | gfx/icons/layers-alignment-center.png 75 | gfx/icons/layers-ungroup.png 76 | gfx/icons/mouse-select-right.png 77 | gfx/icons/mouse-select-wheel.png 78 | gfx/icons/mouse-select.png 79 | gfx/icons/mouse.png 80 | gfx/icons/object-rotate-left-3.png 81 | gfx/icons/object-rotate-right-3.png 82 | gfx/icons/paint-brush.png 83 | gfx/icons/paint-can.png 84 | gfx/icons/paint-tube.png 85 | gfx/icons/palette.png 86 | gfx/icons/pencil-ruler.png 87 | gfx/icons/pencil.png 88 | gfx/icons/pipette.png 89 | gfx/icons/README.txt 90 | gfx/icons/ruler-triangle.png 91 | gfx/icons/ruler.png 92 | gfx/icons/shovel.png 93 | gfx/icons/spectrum.png 94 | gfx/icons/transform-scale-2.png 95 | gfx/icons/wand.png 96 | gfx/icons/water.png 97 | gfx/icons/weather-clear-2.png 98 | gfx/icons/wrench-screwdriver.png 99 | gfx/icons/wrench.png 100 | gfx/icons/zoom-original-4.png 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/plugins/io_zoxel.py: -------------------------------------------------------------------------------- 1 | # io_zoxel.py 2 | # Zoxel native file format import/exporter 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import json 18 | from plugin_api import register_plugin 19 | from constants import ZOXEL_VERSION 20 | 21 | class ZoxelFile(object): 22 | 23 | # Description of file type 24 | description = "Zoxel Files" 25 | 26 | # File type filter 27 | filetype = "*.zox" 28 | 29 | def __init__(self, api): 30 | self.api = api 31 | # Register our exporter 32 | self.api.register_file_handler(self) 33 | # File version format we support 34 | self._file_version = 1 35 | 36 | # Called when we need to save. Should raise an exception if there is a 37 | # problem saving. 38 | def save(self, filename): 39 | # grab the voxel data 40 | voxels = self.api.get_voxel_data() 41 | 42 | # File version 43 | version = self._file_version 44 | 45 | # Build data structure 46 | data = {'version': version, 'frames': voxels.get_frame_count(), 47 | "creator": "Zoxel Version "+ZOXEL_VERSION} 48 | 49 | for f in xrange(voxels.get_frame_count()): 50 | frame = [] 51 | voxels.select_frame(f) 52 | for y in range(voxels.height): 53 | for z in range(voxels.depth): 54 | for x in range(voxels.width): 55 | v = voxels.get(x, y, z) 56 | if v: 57 | frame.append((x,y,z,v)) 58 | 59 | data['frame{0}'.format(f+1)] = frame 60 | 61 | data['width'] = voxels.width 62 | data['height'] = voxels.height 63 | data['depth'] = voxels.depth 64 | 65 | # Open our file 66 | f = open(filename,"wt") 67 | 68 | f.write(json.dumps(data)) 69 | 70 | # Tidy up 71 | f.close() 72 | 73 | # Called when we need to load a file. Should raise an exception if there 74 | # is a problem. 75 | def load(self, filename): 76 | # grab the voxel data 77 | voxels = self.api.get_voxel_data() 78 | 79 | # Load the file data 80 | f = open(filename, "rt") 81 | try: 82 | data = json.loads(f.read()) 83 | except Exception as Ex: 84 | raise Exception("Doesn't look like a valid Zoxel file (%s)" % Ex) 85 | f.close() 86 | 87 | # Check we understand it 88 | if data['version'] > self._file_version: 89 | raise Exception("More recent version of Zoxel needed to open file.") 90 | 91 | # How many frames? 92 | frames = data['frames'] 93 | 94 | # Load the data 95 | frame = data['frame1'] 96 | 97 | # Do we have model dimensions 98 | if 'width' in data: 99 | # Yes, so resize to them 100 | voxels.resize(data['width'], data['height'], data['depth']) 101 | else: 102 | # Zoxel file with no dimension data, determine size 103 | maxX = -127 104 | maxY = -127 105 | maxZ = -127 106 | for x, y, z, v in frame: 107 | if x > maxX: 108 | maxX = x 109 | if y > maxY: 110 | maxY = y 111 | if z > maxZ: 112 | maxZ = z 113 | # Resize 114 | voxels.resize(maxX+1, maxY+1, maxZ+1) 115 | 116 | # Read the voxel data 117 | for f in xrange(frames): 118 | frame = data['frame{0}'.format(f+1)] 119 | for x, y, z, v in frame: 120 | voxels.set(x, y, z, v) 121 | # Add another frame if required 122 | if f < frames-1: 123 | voxels.add_frame(False) 124 | 125 | # Select the first frame by default 126 | if frames > 1: 127 | voxels.select_frame(0) 128 | 129 | register_plugin(ZoxelFile, "Zoxel file format IO", "1.0") 130 | -------------------------------------------------------------------------------- /src/palette_widget.py: -------------------------------------------------------------------------------- 1 | # palette_widget.py 2 | # A colour picking widget. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from PySide import QtCore, QtGui 18 | from PySide.QtCore import QRect, QPoint 19 | 20 | class PaletteWidget(QtGui.QWidget): 21 | 22 | # Colour changed signal 23 | changed = QtCore.Signal() 24 | 25 | @property 26 | def colour(self): 27 | return QtGui.QColor.fromHsvF(self._hue, self._saturation, self._value) 28 | @colour.setter 29 | def colour(self, value): 30 | # If this is an integer, assume is RGBA 31 | if not isinstance(value,QtGui.QColor): 32 | r = (value & 0xff000000) >> 24 33 | g = (value & 0xff0000) >> 16 34 | b = (value & 0xff00) >> 8 35 | value = QtGui.QColor.fromRgb(r,g,b) 36 | self._set_colour(value) 37 | 38 | def __init__(self, parent = None): 39 | super(PaletteWidget, self).__init__(parent) 40 | self._hue = 1.0 41 | self._saturation = 1.0 42 | self._value = 1.0 43 | self._hue_width = 24 44 | self._gap = 8 45 | self._colour = QtGui.QColor.fromHslF(self._hue, 1.0, 1.0) 46 | self._calculate_bounds() 47 | self._draw_palette() 48 | 49 | # Calculate the sizes of various bits of our UI 50 | def _calculate_bounds(self): 51 | width = self.width() 52 | height = self.height() 53 | # Hue palette 54 | self._hue_rect = QRect( 55 | width-self._hue_width, 0, self._hue_width, height) 56 | # Shades palette 57 | self._shades_rect = QRect( 58 | 0, 0, width-(self._hue_width+self._gap), height) 59 | 60 | # Render our palette to an image 61 | def _draw_palette(self): 62 | 63 | # Create an image with a white background 64 | self._image = QtGui.QImage(QtCore.QSize(self.width(), self.height()), 65 | QtGui.QImage.Format.Format_RGB32) 66 | self._image.fill(QtGui.QColor.fromRgb(0xff, 0xff, 0xff)) 67 | 68 | # Draw on our image with no pen 69 | qp = QtGui.QPainter() 70 | qp.begin(self._image) 71 | qp.setPen(QtCore.Qt.NoPen) 72 | 73 | # Render hues 74 | rect = self._hue_rect 75 | for x in xrange(rect.x(), rect.x()+rect.width()): 76 | for y in xrange(rect.y(), rect.y()+rect.height(), 8): 77 | h = float(y)/rect.height() 78 | s = 1.0 79 | v = 1.0 80 | c = QtGui.QColor.fromHsvF(h, s, v) 81 | qp.setBrush(c) 82 | qp.drawRect(x, y, 8, 8) 83 | 84 | # Render hue selection marker 85 | qp.setBrush(QtGui.QColor.fromRgb(0xff, 0xff, 0xff)) 86 | qp.drawRect(rect.x(), self._hue * rect.height(), 87 | rect.width(), 2) 88 | 89 | # Render shades 90 | rect = self._shades_rect 91 | width = float(rect.width()) 92 | steps = int(round(width / 8.0)) 93 | step_size = width / steps 94 | x = rect.x() 95 | while x < rect.width()+rect.x(): 96 | w = int(round(step_size)) 97 | for y in xrange(rect.y(), rect.y()+rect.height(), 8): 98 | h = self._hue 99 | s = 1-float(y)/rect.height() 100 | v = float(x)/rect.width() 101 | c = QtGui.QColor.fromHsvF(h, s, v) 102 | qp.setBrush(c) 103 | qp.drawRect(x, y, w, 8) 104 | x += w 105 | width -= w 106 | steps -= 1 107 | if steps > 0: 108 | step_size = width / steps 109 | 110 | # Render colour selection marker 111 | qp.setBrush(QtGui.QColor.fromRgb(0xff, 0xff, 0xff)) 112 | qp.drawRect(rect.x(), (1-self._saturation)*rect.height(), rect.width(), 1) 113 | qp.drawRect(self._value*rect.width(), rect.y(), 1, rect.height()) 114 | 115 | qp.end() 116 | 117 | def paintEvent(self, event): 118 | # Render our palette image to the screen 119 | qp = QtGui.QPainter() 120 | qp.begin(self) 121 | qp.drawImage(QPoint(0,0), self._image) 122 | qp.end() 123 | 124 | def mousePressEvent(self, event): 125 | mouse = QPoint(event.pos()) 126 | if event.buttons() & QtCore.Qt.LeftButton: 127 | # Click on hues? 128 | if self._hue_rect.contains(mouse.x(), mouse.y()): 129 | y = mouse.y() 130 | c = QtGui.QColor.fromHsvF( 131 | float(y)/self.height(), self._saturation, self._value) 132 | self.colour = c 133 | # Click on colours? 134 | elif self._shades_rect.contains(mouse.x(), mouse.y()): 135 | # calculate saturation and value 136 | x = mouse.x() 137 | y = mouse.y() 138 | c = QtGui.QColor.fromHsvF( 139 | self._hue, 1-float(y)/self._shades_rect.height(), 140 | float(x)/self._shades_rect.width()) 141 | self.colour = c 142 | 143 | def mouseMoveEvent(self, event): 144 | if event.buttons() & QtCore.Qt.LeftButton: 145 | self.mousePressEvent(event) 146 | 147 | def resizeEvent(self, event): 148 | self._calculate_bounds() 149 | self._draw_palette() 150 | 151 | # Set the current colour 152 | def _set_colour(self, c): 153 | h, s, v, _ = c.getHsvF() 154 | self._hue = h 155 | self._saturation = s 156 | self._value = v 157 | self._draw_palette() 158 | self.repaint() 159 | self.changed.emit() 160 | -------------------------------------------------------------------------------- /src/tool.py: -------------------------------------------------------------------------------- 1 | # tool.py 2 | # Base class for tools 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from PySide import QtGui 19 | 20 | # Enumeration type 21 | def enum(*sequential, **named): 22 | enums = dict(zip(sequential, range(len(sequential))), **named) 23 | return type('Enum', (), enums) 24 | 25 | # Mouse buttons 26 | MouseButtons = enum('LEFT', 'MIDDLE', 'RIGHT') 27 | 28 | # Keyboard modifiers 29 | KeyModifiers = enum('CTRL', 'SHIFT', 'ALT') 30 | 31 | class Face(object): 32 | FRONT = 0 # z- 33 | TOP = 1 # y+ 34 | LEFT = 2 # x+ 35 | RIGHT = 3 # x- 36 | BACK = 4 # z+ 37 | BOTTOM = 5 # y- 38 | # Defining the faces for each plane, where "*_PLANE[0] = +" and "*_PLANE[1] = -" 39 | FACES_PLANE_X = [LEFT, RIGHT] 40 | FACES_PLANE_Y = [TOP, BOTTOM] 41 | FACES_PLANE_Z = [BACK, FRONT] 42 | # Defining the faces for each plane used in the collision detection 43 | # algorithm, where the Y plane is collidable (But X and Z are not). 44 | COLLIDABLE_FACES_PLANE_X = FACES_PLANE_X 45 | COLLIDABLE_FACES_PLANE_Y = FACES_PLANE_Y + [None] 46 | COLLIDABLE_FACES_PLANE_Z = FACES_PLANE_Z 47 | 48 | class EventData(object): 49 | 50 | @property 51 | def face(self): 52 | return self._face 53 | @face.setter 54 | def face(self, value): 55 | self._face = value 56 | 57 | @property 58 | def world_x(self): 59 | return self._world_x 60 | @world_x.setter 61 | def world_x(self, value): 62 | self._world_x = value 63 | 64 | @property 65 | def world_y(self): 66 | return self._world_y 67 | @world_y.setter 68 | def world_y(self, value): 69 | self._world_y = value 70 | 71 | @property 72 | def world_z(self): 73 | return self._world_z 74 | @world_z.setter 75 | def world_z(self, value): 76 | self._world_z = value 77 | 78 | @property 79 | def voxels(self): 80 | return self._voxels 81 | @voxels.setter 82 | def voxels(self, value): 83 | self._voxels = value 84 | 85 | @property 86 | def mouse_x(self): 87 | return self._mouse_x 88 | @mouse_x.setter 89 | def mouse_x(self, value): 90 | self._mouse_x = value 91 | 92 | @property 93 | def mouse_y(self): 94 | return self._mouse_y 95 | @mouse_y.setter 96 | def mouse_y(self, value): 97 | self._mouse_y = value 98 | 99 | @property 100 | def mouse_button(self): 101 | return self._mouse_button 102 | @mouse_button.setter 103 | def mouse_button(self, value): 104 | self._mouse_button = value 105 | 106 | @property 107 | def key_modifiers(self): 108 | return self._key_modifiers 109 | @key_modifiers.setter 110 | def key_modifiers(self, value): 111 | self._key_modifiers = value 112 | 113 | def __repr__(self): 114 | return ('EventData(face={0},world_x={1},world_y={2},world_z={3},' 115 | 'mouse_x={4},mouse_y={5},mouse_button={6},key_modifiers={7})'.format( 116 | self._face, self._world_x, self._world_y, self._world_z, 117 | self._mouse_x, self._mouse_y, self._mouse_button, 118 | self._key_modifiers)) 119 | 120 | def __init__(self): 121 | self._face = None 122 | self._world_x = 0 123 | self._world_y = 0 124 | self._world_z = 0 125 | self._mouse_x = 0 126 | self._mouse_y = 0 127 | self._mouse_button = None 128 | self._key_modifiers = None 129 | self._voxels = None 130 | 131 | def __eq__(self, other): 132 | return ( (self._x == other._x) and 133 | (self._y == other._y ) and 134 | (self._z == other._z ) and 135 | (self._face == other._face ) and 136 | (self._voxels == other._voxels ) ) 137 | 138 | # Returns the coordinates of the voxel next to the selected face. 139 | # Or None if there is not one. 140 | def get_neighbour(self): 141 | if self.face is None: 142 | return None 143 | x = self._world_x 144 | y = self._world_y 145 | z = self._world_z 146 | if self.face == Face.TOP: 147 | y += 1 148 | elif self.face == Face.BOTTOM: 149 | y -=1 150 | elif self.face == Face.BACK: 151 | z += 1 152 | elif self.face == Face.FRONT: 153 | z -= 1 154 | elif self.face == Face.LEFT: 155 | x -= 1 156 | elif self.face == Face.RIGHT: 157 | x += 1 158 | return (x, y, z) 159 | 160 | class Tool(object): 161 | 162 | @property 163 | # Returns the currently selected colour 164 | def colour(self): 165 | return self.api.get_palette_colour() 166 | 167 | def __init__(self, api): 168 | self.api = api 169 | # Create default action 170 | self.action = QtGui.QAction( 171 | QtGui.QPixmap(":/gfx/icons/wrench.png"), 172 | "A Tool", None) 173 | self.action.setStatusTip("Unknown Tool") 174 | 175 | # Mouse click - a mouse button has been pressed and released 176 | def on_mouse_click(self, data): 177 | pass 178 | 179 | # A mouse drag has started 180 | def on_drag_start(self, data): 181 | pass 182 | 183 | # Mouse is dragging 184 | def on_drag(self, data): 185 | pass 186 | 187 | # A mouse drag ended 188 | def on_drag_end(self, data): 189 | pass 190 | 191 | # Signal to the tool to cancel whatever it's doing and reset it's state 192 | # back to default 193 | def on_cancel(self, data): 194 | pass 195 | 196 | # Should return the action for the tool 197 | def get_action(self): 198 | return self.action 199 | -------------------------------------------------------------------------------- /src/plugins/io_qubicle.py: -------------------------------------------------------------------------------- 1 | # io_qubicle.py 2 | # Qubicle Constructor Binary File IO 3 | # Copyright (c) 2014, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from plugin_api import register_plugin 18 | 19 | class QubicleFile(object): 20 | 21 | # Description of file type 22 | description = "Qubicle Files" 23 | 24 | # File type filter 25 | filetype = "*.qb" 26 | 27 | def __init__(self, api): 28 | self.api = api 29 | # Register our exporter 30 | self.api.register_file_handler(self) 31 | 32 | # Helper function to read/write uint32 33 | def uint32(self, f, value = None): 34 | if value is not None: 35 | # Write 36 | data = bytearray() 37 | data.append((value & 0xff)); 38 | data.append((value & 0xff00)>>8); 39 | data.append((value & 0xff0000)>>16); 40 | data.append((value & 0xff000000)>>24); 41 | f.write(data) 42 | else: 43 | # Read 44 | x = bytearray(f.read(4)) 45 | if len(x) == 4: 46 | return x[0] | x[1]<<8 | x[2]<<16 | x[3]<<24 47 | return 0 48 | 49 | # Called when we need to save. Should raise an exception if there is a 50 | # problem saving. 51 | def save(self, filename): 52 | # grab the voxel data 53 | voxels = self.api.get_voxel_data() 54 | 55 | # Open our file 56 | f = open(filename,"wb") 57 | 58 | # Version 59 | self.uint32(f, 0x00000101) 60 | # Colour format RGBA 61 | self.uint32(f, 0) 62 | # Left handed coords 63 | self.uint32(f, 0) 64 | # Uncompressed 65 | self.uint32(f, 0) 66 | # Visability mask 67 | self.uint32(f, 0) 68 | # Matrix count 69 | self.uint32(f, 1) 70 | 71 | # Model name length 72 | name = "Model" 73 | f.write(str(chr(len(name)))) 74 | # Model name 75 | f.write(name) 76 | 77 | # X, Y, Z dimensions 78 | self.uint32(f, voxels.width) 79 | self.uint32(f, voxels.height) 80 | self.uint32(f, voxels.depth) 81 | 82 | # Matrix position 83 | self.uint32(f, 0) 84 | self.uint32(f, 0) 85 | self.uint32(f, 0) 86 | 87 | # Data 88 | for z in xrange(voxels.depth): 89 | for y in xrange(voxels.height): 90 | for x in xrange(voxels.width): 91 | vox = voxels.get(x, y, z) 92 | alpha = 0xff 93 | if not vox: 94 | alpha = 0x00 95 | r = (vox & 0xff000000)>>24 96 | g = (vox & 0xff0000)>>16 97 | b = (vox & 0xff00)>>8 98 | vox = r | g<<8 | b<<16 | alpha<<24 99 | self.uint32(f, vox) 100 | 101 | # Tidy up 102 | f.close() 103 | 104 | # Load a Qubicle Constructor binary file 105 | def load(self, filename): 106 | # grab the voxel data 107 | voxels = self.api.get_voxel_data() 108 | 109 | # Open our file 110 | f = open(filename,"rb") 111 | 112 | # Version 113 | version = self.uint32(f) 114 | # Colour format RGBA 115 | format =self.uint32(f) 116 | if format: 117 | raise Exception("Unsupported colour format") 118 | # Left handed coords 119 | coords = self.uint32(f) 120 | # Uncompressed 121 | compression = self.uint32(f) 122 | if compression: 123 | raise Exception("Compressed .qb files not yet supported") 124 | # Visability mask 125 | mask = self.uint32(f) 126 | # Matrix count 127 | matrix_count = self.uint32(f) 128 | 129 | # Warn about multiple matrices 130 | if matrix_count > 1: 131 | self.api.warning("Qubicle files with more than 1 matrix" 132 | " are not yet properly supported. All " 133 | " matrices will be (badly) merged.") 134 | 135 | max_width = 0 136 | max_height = 0 137 | max_depth = 0 138 | 139 | for i in xrange(matrix_count): 140 | 141 | # Name length 142 | namelen = int(ord(f.read(1))) 143 | name = f.read(namelen) 144 | 145 | # X, Y, Z dimensions 146 | width = self.uint32(f) 147 | height = self.uint32(f) 148 | depth = self.uint32(f) 149 | 150 | # Don't allow huge models 151 | if width > 127 or height > 127 or depth > 127: 152 | raise Exception("Model to large - max 127x127x127") 153 | 154 | if width > max_width: 155 | max_width = width 156 | if height > max_height: 157 | max_height = height 158 | if depth > max_depth: 159 | max_depth = depth 160 | 161 | voxels.resize(max_width, max_height, max_depth) 162 | 163 | # Matrix position - FIXME not yet supported 164 | dx = self.uint32(f) 165 | dy = self.uint32(f) 166 | dz = self.uint32(f) 167 | 168 | # Data 169 | for z in xrange(depth): 170 | for y in xrange(height): 171 | for x in xrange(width): 172 | vox = self.uint32(f) 173 | vox = (vox & 0x00ffffff) 174 | if vox: 175 | r = (vox & 0x000000ff)>>0 176 | g = (vox & 0x0000ff00)>>8 177 | b = (vox & 0x00ff0000)>>16 178 | vox = (r<<24) | (g<<16) | (b<<8) | 0xff 179 | voxels.set(x, y, z, vox) 180 | 181 | f.close() 182 | 183 | 184 | register_plugin(QubicleFile, "Qubicle Constructor file format IO", "1.0") 185 | -------------------------------------------------------------------------------- /src/voxel_grid.py: -------------------------------------------------------------------------------- 1 | # voxel_grid.py 2 | # A 3D grid for the voxel widget area. 3 | # Copyright (c) 2013, Graham R King & Bruno F Canella 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import array 19 | from PySide import QtGui 20 | from OpenGL.GL import * 21 | #from OpenGL.GLU import gluUnProject, gluProject 22 | 23 | ## 24 | # Constants for the planes, to be used with a dictionary. 25 | class GridPlanes(object): 26 | X = "x" 27 | Y = "y" 28 | Z = "z" 29 | 30 | ## 31 | # Represents a plane to be used in the grid 32 | class GridPlane(object): 33 | ## 34 | # @param voxels Reference to the VoxelData 35 | # @param plane The plane where this grid belongs 36 | # @param offset The offset of the plane, relative to their negative end ( X = Left, Y = Bottom, Z = Front ) 37 | # @param visible Indicates if the plane is visible or not 38 | def __init__( self, voxels, plane, offset, visible, color = QtGui.QColor("white") ): 39 | self._voxels = voxels 40 | self._plane = plane 41 | self._offset = 0 42 | self._visible = visible 43 | self._color = color 44 | self._offset_plane_limit = { 45 | GridPlanes.X: lambda: self._voxels.width, 46 | GridPlanes.Y: lambda: self._voxels.height, 47 | GridPlanes.Z: lambda: self._voxels.depth, 48 | } 49 | self._methods_get_plane_vertices = { 50 | GridPlanes.X: self._get_grid_vertices_x_plane, 51 | GridPlanes.Y: self._get_grid_vertices_y_plane, 52 | GridPlanes.Z: self._get_grid_vertices_z_plane 53 | } 54 | self.offset = offset 55 | self.update_vertices() 56 | 57 | @property 58 | def voxels(self): 59 | return self._voxels 60 | @voxels.setter 61 | def voxels(self, voxels): 62 | self._voxels = voxels 63 | 64 | @property 65 | def plane(self): 66 | return self._plane 67 | @plane.setter 68 | def plane(self, value): 69 | assert value in ( GridPlanes.X, GridPlanes.Y, GridPlanes.Z ) 70 | if( value != self._plane ): 71 | self._plane = value 72 | self.update_vertices() 73 | 74 | @property 75 | def offset(self): 76 | return self._offset 77 | @offset.setter 78 | def offset(self, value): 79 | assert isinstance(value, int) 80 | offset_limit = self._offset_plane_limit[self.plane]() 81 | if( value > offset_limit ): 82 | value = offset_limit 83 | if( value != self._offset ): 84 | self._offset = value 85 | self.update_vertices() 86 | 87 | @property 88 | def visible(self): 89 | return self._visible 90 | @visible.setter 91 | def visible(self, value): 92 | assert isinstance(value, bool) 93 | self._visible = value 94 | 95 | @property 96 | def color(self): 97 | return self._color 98 | @color.setter 99 | def color(self, value): 100 | assert isinstance( value, QtGui.QColor ) 101 | self._color = value 102 | 103 | @property 104 | def vertices(self): 105 | return self._vertices 106 | 107 | def update_vertices(self): 108 | self._vertices = self._methods_get_plane_vertices[self._plane]() 109 | self._vertices_array = array.array("f", self._vertices).tostring() 110 | self._num_vertices = len(self._vertices)//3 111 | 112 | def _get_grid_vertices_x_plane(self): 113 | vertices = [] 114 | height = self._voxels.height 115 | depth = self._voxels.depth 116 | for y in xrange(height+1): 117 | vertices += self._voxels.voxel_to_world( self.offset, y, 0 ) 118 | vertices += self._voxels.voxel_to_world( self.offset, y, depth ) 119 | for z in xrange(depth+1): 120 | vertices += self._voxels.voxel_to_world( self.offset, 0, z ) 121 | vertices += self._voxels.voxel_to_world( self.offset, height, z ) 122 | return vertices 123 | 124 | def _get_grid_vertices_y_plane(self): 125 | vertices = [] 126 | width = self._voxels.width 127 | depth = self._voxels.depth 128 | for z in xrange(depth+1): 129 | vertices += self._voxels.voxel_to_world( 0, self.offset, z ) 130 | vertices += self._voxels.voxel_to_world( width, self.offset, z ) 131 | for x in xrange(width+1): 132 | vertices += self._voxels.voxel_to_world( x, self.offset, 0 ) 133 | vertices += self._voxels.voxel_to_world( x, self.offset, depth ) 134 | return vertices 135 | 136 | def _get_grid_vertices_z_plane(self): 137 | vertices = [] 138 | width = self._voxels.width 139 | height = self._voxels.height 140 | for x in xrange(width+1): 141 | vertices += self._voxels.voxel_to_world( x, 0, self.offset ) 142 | vertices += self._voxels.voxel_to_world( x, height, self.offset ) 143 | for y in xrange(height+1): 144 | vertices += self._voxels.voxel_to_world( 0, y, self.offset ) 145 | vertices += self._voxels.voxel_to_world( width, y, self.offset ) 146 | return vertices 147 | 148 | class VoxelGrid(object): 149 | 150 | def __init__(self, widget ): 151 | self._voxels = widget 152 | self._planes = {} 153 | 154 | def add_grid_plane(self, plane, offset, visible, color = QtGui.QColor("white") ): 155 | key = (plane, offset) 156 | if( key in self._planes.keys() ): 157 | self._planes[key].visible = visible 158 | else: 159 | grid_plane = GridPlane( self._voxels, plane, offset, visible, color ) 160 | self._planes[key] = grid_plane 161 | 162 | def remove_grid_plane(self, plane, offset): 163 | key = (plane, offset) 164 | if( key in self._planes.keys() ): 165 | del self._planes[key] 166 | 167 | # Return vertices for a floor grid 168 | def get_grid_plane(self, plane, offset): 169 | key = ( plane, offset ) 170 | if( key in self._planes.keys() ): 171 | return self._planes[key] 172 | else: 173 | return None 174 | 175 | def update_grid_plane(self, voxels): 176 | for plane in self._planes.itervalues(): 177 | plane.voxels = voxels 178 | if plane.plane == GridPlanes.Z: 179 | plane.offset = voxels.depth 180 | plane.update_vertices() 181 | 182 | # Render the grids 183 | def paint(self): 184 | # Disable lighting 185 | glDisable(GL_LIGHTING) 186 | glDisable(GL_TEXTURE_2D) 187 | 188 | for grid in self._planes.itervalues(): 189 | if( not grid.visible ): 190 | continue 191 | 192 | red = grid.color.redF() 193 | green = grid.color.greenF() 194 | blue = grid.color.blueF() 195 | # Grid colour 196 | glColor3f(red,green,blue) 197 | 198 | # Enable vertex buffers 199 | glEnableClientState(GL_VERTEX_ARRAY) 200 | 201 | # Describe our buffers 202 | glVertexPointer(3, GL_FLOAT, 0, grid._vertices_array) 203 | 204 | # Render the buffers 205 | glDrawArrays(GL_LINES, 0, grid._num_vertices) 206 | 207 | # Disable vertex buffers 208 | glDisableClientState(GL_VERTEX_ARRAY) 209 | 210 | # Enable lighting 211 | glEnable(GL_LIGHTING) 212 | glEnable(GL_TEXTURE_2D) 213 | 214 | def scale_offsets(self, width_scale = None, height_scale = None, depth_scale = None ): 215 | for grid in self._planes.itervalues(): 216 | if( grid.plane == GridPlanes.X and width_scale ): 217 | grid.offset = int(round(grid.offset * width_scale)) 218 | elif( grid.plane == GridPlanes.Y and height_scale ): 219 | grid.offset = int(round(grid.offset * height_scale)) 220 | elif( grid.plane == GridPlanes.Z and depth_scale ): 221 | grid.offset = int(round(grid.offset * depth_scale)) 222 | -------------------------------------------------------------------------------- /src/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1024 10 | 679 11 | 12 | 13 | 14 | Zoxel 15 | 16 | 17 | 18 | :/images/gfx/icons/block.png:/images/gfx/icons/block.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 0 33 | 0 34 | 1024 35 | 24 36 | 37 | 38 | 39 | 40 | File 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Help 55 | 56 | 57 | 58 | 59 | 60 | View 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Edit 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | Animation 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | toolBar 105 | 106 | 107 | TopToolBarArea 108 | 109 | 110 | false 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | toolBar 134 | 135 | 136 | LeftToolBarArea 137 | 138 | 139 | false 140 | 141 | 142 | 143 | 144 | Palette 145 | 146 | 147 | 2 148 | 149 | 150 | 151 | 152 | 140 153 | 0 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | toolBar 162 | 163 | 164 | TopToolBarArea 165 | 166 | 167 | false 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | :/gfx/gfx/application-exit.png:/gfx/gfx/application-exit.png 179 | 180 | 181 | Exit 182 | 183 | 184 | true 185 | 186 | 187 | 188 | 189 | 190 | :/gfx/gfx/help-contents.png:/gfx/gfx/help-contents.png 191 | 192 | 193 | About 194 | 195 | 196 | 197 | 198 | 199 | :/images/gfx/icons/disk-return-black.png:/images/gfx/icons/disk-return-black.png 200 | 201 | 202 | Save 203 | 204 | 205 | 206 | 207 | true 208 | 209 | 210 | true 211 | 212 | 213 | 214 | :/images/gfx/icons/grid.png:/images/gfx/icons/grid.png 215 | 216 | 217 | Axis Grids 218 | 219 | 220 | Toggle Axis Grids 221 | 222 | 223 | 224 | 225 | true 226 | 227 | 228 | 229 | :/images/gfx/icons/border.png:/images/gfx/icons/border.png 230 | 231 | 232 | Wireframe 233 | 234 | 235 | 236 | 237 | 238 | :/images/gfx/icons/document.png:/images/gfx/icons/document.png 239 | 240 | 241 | New 242 | 243 | 244 | 245 | 246 | 247 | :/images/gfx/icons/disk-return-black.png:/images/gfx/icons/disk-return-black.png 248 | 249 | 250 | Save As... 251 | 252 | 253 | 254 | 255 | 256 | :/images/gfx/icons/folder-horizontal-open.png:/images/gfx/icons/folder-horizontal-open.png 257 | 258 | 259 | Open... 260 | 261 | 262 | 263 | 264 | 265 | :/images/gfx/icons/palette.png:/images/gfx/icons/palette.png 266 | 267 | 268 | Background 269 | 270 | 271 | Choose background colour 272 | 273 | 274 | 275 | 276 | 277 | :/images/gfx/icons/transform-scale-2.png:/images/gfx/icons/transform-scale-2.png 278 | 279 | 280 | Resize 281 | 282 | 283 | Set dimensions 284 | 285 | 286 | 287 | 288 | true 289 | 290 | 291 | 292 | :/images/gfx/icons/weather-clear-2.png:/images/gfx/icons/weather-clear-2.png 293 | 294 | 295 | Occlusion 296 | 297 | 298 | Render with ambient occlusion 299 | 300 | 301 | 302 | 303 | 304 | :/images/gfx/icons/zoom-original-4.png:/images/gfx/icons/zoom-original-4.png 305 | 306 | 307 | Reset Camera 308 | 309 | 310 | Reset camera position 311 | 312 | 313 | 314 | 315 | true 316 | 317 | 318 | 319 | :/images/gfx/icons/categories.png:/images/gfx/icons/categories.png 320 | 321 | 322 | Voxel Edges 323 | 324 | 325 | Toggle view voxel edges 326 | 327 | 328 | 329 | 330 | 331 | :/images/gfx/icons/color.png:/images/gfx/icons/color.png 332 | 333 | 334 | Voxel Colour 335 | 336 | 337 | Pick Voxel Colour 338 | 339 | 340 | 341 | 342 | 343 | :/images/gfx/icons/undo.png:/images/gfx/icons/undo.png 344 | 345 | 346 | Undo 347 | 348 | 349 | Undo 350 | 351 | 352 | Ctrl+Z 353 | 354 | 355 | 356 | 357 | 358 | :/images/gfx/icons/redo.png:/images/gfx/icons/redo.png 359 | 360 | 361 | Redo 362 | 363 | 364 | Redo 365 | 366 | 367 | Ctrl+Shift+Z 368 | 369 | 370 | 371 | 372 | 373 | :/images/gfx/icons/zoom_in.png:/images/gfx/icons/zoom_in.png 374 | 375 | 376 | Zoom in 377 | 378 | 379 | Zoom in 380 | 381 | 382 | Ctrl++ 383 | 384 | 385 | 386 | 387 | 388 | :/images/gfx/icons/zoom_out.png:/images/gfx/icons/zoom_out.png 389 | 390 | 391 | Zoom out 392 | 393 | 394 | Zoom out 395 | 396 | 397 | Ctrl+- 398 | 399 | 400 | 401 | 402 | 403 | :/images/gfx/icons/Create.png:/images/gfx/icons/Create.png 404 | 405 | 406 | Add Frame 407 | 408 | 409 | Add new animation frame 410 | 411 | 412 | 413 | 414 | 415 | :/images/gfx/icons/Go.png:/images/gfx/icons/Go.png 416 | 417 | 418 | Play 419 | 420 | 421 | Play animation 422 | 423 | 424 | 425 | 426 | 427 | :/images/gfx/icons/Stop.png:/images/gfx/icons/Stop.png 428 | 429 | 430 | Stop 431 | 432 | 433 | Stop animation 434 | 435 | 436 | 437 | 438 | 439 | :/images/gfx/icons/Goforward.png:/images/gfx/icons/Goforward.png 440 | 441 | 442 | Next Frame 443 | 444 | 445 | Go to next frame 446 | 447 | 448 | Ctrl+Right 449 | 450 | 451 | 452 | 453 | 454 | :/images/gfx/icons/Goback.png:/images/gfx/icons/Goback.png 455 | 456 | 457 | Previous Frame 458 | 459 | 460 | Go to previous frame 461 | 462 | 463 | Ctrl+Left 464 | 465 | 466 | 467 | 468 | true 469 | 470 | 471 | 472 | :/images/gfx/icons/wrench-screwdriver.png:/images/gfx/icons/wrench-screwdriver.png 473 | 474 | 475 | Animation Settings 476 | 477 | 478 | Animation Settings 479 | 480 | 481 | 482 | 483 | 484 | :/images/gfx/icons/Cancel.png:/images/gfx/icons/Cancel.png 485 | 486 | 487 | Delete Frame 488 | 489 | 490 | Delete current frame 491 | 492 | 493 | 494 | 495 | Rotate Model X-Axis 496 | 497 | 498 | Rotate voxels around X axis 499 | 500 | 501 | 502 | 503 | Rotate Model Y-Axis 504 | 505 | 506 | Rotate voxels around Y axis 507 | 508 | 509 | 510 | 511 | Rotate Model Z-Axis 512 | 513 | 514 | Rotate voxels around Z axis 515 | 516 | 517 | 518 | 519 | 520 | :/images/gfx/icons/disk.png:/images/gfx/icons/disk.png 521 | 522 | 523 | Export Image... 524 | 525 | 526 | Export image of current model view 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | action_exit 536 | triggered() 537 | MainWindow 538 | close() 539 | 540 | 541 | -1 542 | -1 543 | 544 | 545 | 501 546 | 361 547 | 548 | 549 | 550 | 551 | 552 | -------------------------------------------------------------------------------- /src/mainwindow.py: -------------------------------------------------------------------------------- 1 | # mainwindow.py 2 | # The Zoxel main window. 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from PySide import QtCore 19 | from PySide import QtGui 20 | from dialog_about import AboutDialog 21 | from dialog_resize import ResizeDialog 22 | from ui_mainwindow import Ui_MainWindow 23 | from voxel_widget import GLWidget 24 | import json 25 | from palette_widget import PaletteWidget 26 | import os 27 | 28 | class MainWindow(QtGui.QMainWindow): 29 | 30 | def __init__(self, parent=None): 31 | # Initialise the UI 32 | self.display = None 33 | super(MainWindow, self).__init__(parent) 34 | self.ui = Ui_MainWindow() 35 | self.ui.setupUi(self) 36 | # Current file 37 | self._filename = None 38 | self._last_file_handler = None 39 | # Importers / Exporters 40 | self._file_handlers = [] 41 | # Update our window caption 42 | self._caption = "Zoxel" 43 | # Our global state 44 | self.settings = QtCore.QSettings("Zoxel", "Zoxel") 45 | self.state = {} 46 | # Our animation timer 47 | self._timer = QtCore.QTimer(self) 48 | self.connect(self._timer, QtCore.SIGNAL("timeout()"), 49 | self.on_animation_tick) 50 | self._anim_speed = 200 51 | # Load our state if possible 52 | self.load_state() 53 | # Create our GL Widget 54 | try: 55 | voxels = GLWidget(self.ui.glparent) 56 | self.ui.glparent.layout().addWidget(voxels) 57 | self.display = voxels 58 | except Exception as E: 59 | QtGui.QMessageBox.warning(self, "Initialisation Failed", 60 | str(E)) 61 | exit(1) 62 | # Load default model dimensions 63 | width = self.get_setting("default_model_width") 64 | height = self.get_setting("default_model_height") 65 | depth = self.get_setting("default_model_depth") 66 | if width: 67 | self.resize_voxels(width, height, depth) 68 | # Resize is detected as a change, discard changes 69 | self.display.voxels.saved() 70 | # Create our palette widget 71 | voxels = PaletteWidget(self.ui.palette) 72 | self.ui.palette.layout().addWidget(voxels) 73 | self.colour_palette = voxels 74 | # More UI state 75 | value = self.get_setting("display_axis_grids") 76 | if value is not None: 77 | self.ui.action_axis_grids.setChecked(value) 78 | self.display.axis_grids = value 79 | value = self.get_setting("background_colour") 80 | if value is not None: 81 | self.display.background = QtGui.QColor.fromRgb(*value) 82 | value = self.get_setting("voxel_edges") 83 | if value is not None: 84 | self.display.voxel_edges = value 85 | self.ui.action_voxel_edges.setChecked(value) 86 | else: 87 | self.ui.action_voxel_edges.setChecked(self.display.voxel_edges) 88 | value = self.get_setting("occlusion") 89 | if value is None: 90 | value = True 91 | self.display.voxels.occlusion = value 92 | self.ui.action_occlusion.setChecked(value) 93 | # Connect some signals 94 | if self.display: 95 | self.display.voxels.notify = self.on_data_changed 96 | self.display.mouse_click_event.connect(self.on_tool_mouse_click) 97 | self.display.start_drag_event.connect(self.on_tool_drag_start) 98 | self.display.end_drag_event.connect(self.on_tool_drag_end) 99 | self.display.drag_event.connect(self.on_tool_drag) 100 | if self.colour_palette: 101 | self.colour_palette.changed.connect(self.on_colour_changed) 102 | # Initialise our tools 103 | self._tool_group = QtGui.QActionGroup(self.ui.toolbar_drawing) 104 | self._tools = [] 105 | # Setup window 106 | self.update_caption() 107 | self.refresh_actions() 108 | 109 | def on_animation_tick(self): 110 | self.on_action_anim_next_triggered() 111 | 112 | @QtCore.Slot() 113 | def on_action_about_triggered(self): 114 | dialog = AboutDialog(self) 115 | if dialog.exec_(): 116 | pass 117 | 118 | @QtCore.Slot() 119 | def on_action_axis_grids_triggered(self): 120 | self.display.axis_grids = self.ui.action_axis_grids.isChecked() 121 | self.set_setting("display_axis_grids", self.display.axis_grids) 122 | 123 | @QtCore.Slot() 124 | def on_action_voxel_edges_triggered(self): 125 | self.display.voxel_edges = self.ui.action_voxel_edges.isChecked() 126 | self.set_setting("voxel_edges", self.display.voxel_edges) 127 | 128 | @QtCore.Slot() 129 | def on_action_zoom_in_triggered(self): 130 | self.display.zoom_in() 131 | 132 | @QtCore.Slot() 133 | def on_action_zoom_out_triggered(self): 134 | self.display.zoom_out() 135 | 136 | @QtCore.Slot() 137 | def on_action_new_triggered(self): 138 | if self.display.voxels.changed: 139 | if not self.confirm_save(): 140 | return 141 | # Clear our data 142 | self._filename = "" 143 | self.display.clear() 144 | self.display.voxels.saved() 145 | self.update_caption() 146 | self.refresh_actions() 147 | 148 | @QtCore.Slot() 149 | def on_action_wireframe_triggered(self): 150 | self.display.wireframe = self.ui.action_wireframe.isChecked() 151 | self.set_setting("display_wireframe", self.display.wireframe) 152 | 153 | @QtCore.Slot() 154 | def on_action_save_triggered(self): 155 | # Save 156 | self.save() 157 | 158 | @QtCore.Slot() 159 | def on_action_saveas_triggered(self): 160 | # Save 161 | self.save(True) 162 | 163 | @QtCore.Slot() 164 | def on_action_open_triggered(self): 165 | # Load 166 | self.load() 167 | 168 | @QtCore.Slot() 169 | def on_action_undo_triggered(self): 170 | # Undo 171 | self.display.voxels.undo() 172 | self.display.refresh() 173 | 174 | @QtCore.Slot() 175 | def on_action_redo_triggered(self): 176 | # Redo 177 | self.display.voxels.redo() 178 | self.display.refresh() 179 | 180 | @QtCore.Slot() 181 | def on_action_resize_triggered(self): 182 | # Resize model dimensions 183 | dialog = ResizeDialog(self) 184 | dialog.ui.width.setValue(self.display.voxels.width) 185 | dialog.ui.height.setValue(self.display.voxels.height) 186 | dialog.ui.depth.setValue(self.display.voxels.depth) 187 | if dialog.exec_(): 188 | width = dialog.ui.width.value() 189 | height = dialog.ui.height.value() 190 | depth = dialog.ui.depth.value() 191 | self.resize_voxels(width, height, depth) 192 | 193 | def resize_voxels(self, width, height, depth): 194 | new_width_scale = float(width) / self.display.voxels.width 195 | new_height_scale = float(height) / self.display.voxels.height 196 | new_depth_scale = float(depth) / self.display.voxels.depth 197 | self.display.voxels.resize(width, height, depth) 198 | self.display.grids.scale_offsets( new_width_scale, new_height_scale, new_depth_scale ) 199 | self.display.refresh() 200 | # Remember these dimensions 201 | self.set_setting("default_model_width", width) 202 | self.set_setting("default_model_height", height) 203 | self.set_setting("default_model_depth", depth) 204 | 205 | @QtCore.Slot() 206 | def on_action_reset_camera_triggered(self): 207 | self.display.reset_camera() 208 | 209 | @QtCore.Slot() 210 | def on_action_occlusion_triggered(self): 211 | self.display.voxels.occlusion = self.ui.action_occlusion.isChecked() 212 | self.set_setting("occlusion", self.display.voxels.occlusion) 213 | self.display.refresh() 214 | 215 | @QtCore.Slot() 216 | def on_action_background_triggered(self): 217 | # Choose a background colour 218 | colour = QtGui.QColorDialog.getColor() 219 | if colour.isValid(): 220 | self.display.background = colour 221 | colour = (colour.red(), colour.green(), colour.blue()) 222 | self.set_setting("background_colour", colour) 223 | 224 | @QtCore.Slot() 225 | def on_action_anim_add_triggered(self): 226 | self.display.voxels.add_frame() 227 | self.display.refresh() 228 | self.refresh_actions() 229 | 230 | @QtCore.Slot() 231 | def on_action_anim_delete_triggered(self): 232 | self.display.voxels.delete_frame() 233 | self.display.refresh() 234 | self.refresh_actions() 235 | 236 | @QtCore.Slot() 237 | def on_action_anim_play_triggered(self): 238 | self._timer.start(self._anim_speed) 239 | self.refresh_actions() 240 | 241 | @QtCore.Slot() 242 | def on_action_anim_stop_triggered(self): 243 | self._timer.stop() 244 | self.refresh_actions() 245 | 246 | @QtCore.Slot() 247 | def on_action_anim_next_triggered(self): 248 | self.display.voxels.select_next_frame() 249 | self.display.refresh() 250 | self.refresh_actions() 251 | 252 | @QtCore.Slot() 253 | def on_action_anim_previous_triggered(self): 254 | self.display.voxels.select_previous_frame() 255 | self.display.refresh() 256 | self.refresh_actions() 257 | 258 | @QtCore.Slot() 259 | def on_action_anim_settings_triggered(self): 260 | pass 261 | 262 | @QtCore.Slot() 263 | def on_action_rotate_x_triggered(self): 264 | self.display.voxels.rotate_about_axis(self.display.voxels.X_AXIS) 265 | self.display.refresh() 266 | 267 | @QtCore.Slot() 268 | def on_action_rotate_y_triggered(self): 269 | self.display.voxels.rotate_about_axis(self.display.voxels.Y_AXIS) 270 | self.display.refresh() 271 | 272 | @QtCore.Slot() 273 | def on_action_rotate_z_triggered(self): 274 | self.display.voxels.rotate_about_axis(self.display.voxels.Z_AXIS) 275 | self.display.refresh() 276 | 277 | @QtCore.Slot() 278 | def on_action_voxel_colour_triggered(self): 279 | # Choose a voxel colour 280 | colour = QtGui.QColorDialog.getColor() 281 | if colour.isValid(): 282 | self.colour_palette.colour = colour 283 | 284 | @QtCore.Slot() 285 | def on_action_export_image_triggered(self): 286 | png = QtGui.QPixmap.grabWidget(self.display) 287 | choices = "PNG Image (*.png);;JPEG Image (*.jpg)" 288 | 289 | # Grab our default location 290 | directory = self.get_setting("default_directory") 291 | # grab a filename 292 | filename, filetype = QtGui.QFileDialog.getSaveFileName(self, 293 | caption = "Export Image As", 294 | filter = choices, 295 | dir = directory) 296 | if not filename: 297 | return 298 | 299 | # Remember the location 300 | directory = os.path.dirname(filename) 301 | self.set_setting("default_directory", directory) 302 | 303 | # Save the PNG 304 | png.save(filename,filetype.split()[0]) 305 | 306 | def on_tool_mouse_click(self): 307 | tool = self.get_active_tool() 308 | if not tool: 309 | return 310 | data = self.display.target 311 | tool.on_mouse_click(data) 312 | 313 | def on_tool_drag_start(self): 314 | tool = self.get_active_tool() 315 | if not tool: 316 | return 317 | data = self.display.target 318 | tool.on_drag_start(data) 319 | 320 | def on_tool_drag(self): 321 | tool = self.get_active_tool() 322 | if not tool: 323 | return 324 | data = self.display.target 325 | tool.on_drag(data) 326 | 327 | def on_tool_drag_end(self): 328 | tool = self.get_active_tool() 329 | if not tool: 330 | return 331 | data = self.display.target 332 | tool.on_drag_end(data) 333 | 334 | # Confirm if user wants to save before doing something drastic. 335 | # returns True if we should continue 336 | def confirm_save(self): 337 | responce = QtGui.QMessageBox.question(self,"Save changes?", 338 | "Save changes before discarding?", 339 | buttons = (QtGui.QMessageBox.Save | QtGui.QMessageBox.Cancel 340 | | QtGui.QMessageBox.No)) 341 | if responce == QtGui.QMessageBox.StandardButton.Save: 342 | if not self.save(): 343 | return False 344 | elif responce == QtGui.QMessageBox.StandardButton.Cancel: 345 | return False 346 | return True 347 | 348 | # Voxel data changed signal handler 349 | def on_data_changed(self): 350 | self.update_caption() 351 | self.refresh_actions() 352 | 353 | # Colour selection changed handler 354 | def on_colour_changed(self): 355 | self.display.voxel_colour = self.colour_palette.colour 356 | 357 | # Return a section of our internal config 358 | def get_setting(self, name): 359 | if name in self.state: 360 | return self.state[name] 361 | return None 362 | 363 | # Set some config. Value should be a serialisable type 364 | def set_setting(self, name, value): 365 | self.state[name] = value 366 | 367 | def closeEvent(self, event): 368 | # Save state 369 | self.save_state() 370 | if self.display.voxels.changed: 371 | if not self.confirm_save(): 372 | event.ignore() 373 | return 374 | event.accept() 375 | 376 | # Save our state 377 | def save_state(self): 378 | try: 379 | state = json.dumps(self.state) 380 | self.settings.setValue("system/state", state) 381 | except Exception as E: 382 | # XXX Fail. Never displays because we're on our way out 383 | error = QtGui.QErrorMessage(self) 384 | error.showMessage(str(E)) 385 | print str(E) 386 | 387 | # Load our state 388 | def load_state(self): 389 | try: 390 | state = self.settings.value("system/state") 391 | if state: 392 | self.state = json.loads(state) 393 | except Exception as E: 394 | error = QtGui.QErrorMessage(self) 395 | error.showMessage(str(E)) 396 | 397 | # Update the window caption to reflect the current state 398 | def update_caption(self): 399 | caption = "Zoxel" 400 | if self._filename: 401 | caption += " - [%s]" % self._filename 402 | else: 403 | caption += " - [Unsaved model]" 404 | if self.display and self.display.voxels.changed: 405 | caption += " *" 406 | numframes = self.display.voxels.get_frame_count() 407 | frame = self.display.voxels.get_frame_number()+1 408 | if numframes > 1: 409 | caption += " - Frame {0} of {1}".format(frame, numframes) 410 | if caption != self._caption: 411 | self.setWindowTitle(caption) 412 | self._caption = caption 413 | 414 | # Save the current data 415 | def save(self, newfile = False): 416 | 417 | # Find the handlers that support saving 418 | handlers = [x for x in self._file_handlers if hasattr(x, 'save')] 419 | 420 | saved = False 421 | filename = self._filename 422 | handler = self._last_file_handler 423 | if handler: 424 | filetype = handler.filetype 425 | 426 | # Build list of available types 427 | choices = [] 428 | for exporter in handlers: 429 | choices.append( "%s (%s)" % (exporter.description, exporter.filetype)) 430 | choices = ";;".join(choices) 431 | 432 | # Grab our default location 433 | directory = self.get_setting("default_directory") 434 | 435 | # Get a filename if we need one 436 | if newfile or not filename: 437 | filename, filetype = QtGui.QFileDialog.getSaveFileName(self, 438 | caption = "Save As", 439 | filter = choices, 440 | dir = directory, 441 | selectedFilter="Zoxel Files (*.zox)") 442 | if not filename: 443 | return 444 | handler = None 445 | 446 | # Remember the location 447 | directory = os.path.dirname(filename) 448 | self.set_setting("default_directory", directory) 449 | 450 | # Find the handler if we need to 451 | if not handler: 452 | for exporter in handlers: 453 | ourtype = "%s (%s)" % (exporter.description, exporter.filetype) 454 | if filetype == ourtype: 455 | handler = exporter 456 | 457 | # Call the save handler 458 | try: 459 | handler.save(filename) 460 | saved = True 461 | except Exception as Ex: 462 | QtGui.QMessageBox.warning(self, "Save Failed", 463 | str(Ex)) 464 | 465 | # If we saved, clear edited state 466 | if saved: 467 | self._filename = filename 468 | self._last_file_handler = handler 469 | self.display.voxels.saved() 470 | self.update_caption() 471 | self.refresh_actions() 472 | return saved 473 | 474 | # Registers an file handler (importer/exporter) with the system 475 | def register_file_handler(self, handler): 476 | self._file_handlers.append(handler) 477 | 478 | # load a file 479 | def load(self): 480 | # If we have changes, perhaps we should save? 481 | if self.display.voxels.changed: 482 | if not self.confirm_save(): 483 | return 484 | 485 | # Find the handlers that support loading 486 | handler = None 487 | handlers = [x for x in self._file_handlers if hasattr(x, 'load')] 488 | 489 | # Build list of types we can load 490 | choices = [] 491 | for importer in handlers: 492 | choices.append( "%s (%s)" % (importer.description, importer.filetype)) 493 | choices = ";;".join(choices) 494 | 495 | # Grab our default location 496 | directory = self.get_setting("default_directory") 497 | 498 | # Get a filename 499 | filename, filetype = QtGui.QFileDialog.getOpenFileName(self, 500 | caption="Open file", 501 | filter=choices, 502 | dir = directory, 503 | selectedFilter="Zoxel Files (*.zox)") 504 | if not filename: 505 | return 506 | 507 | # Remember the location 508 | directory = os.path.dirname(filename) 509 | self.set_setting("default_directory", directory) 510 | 511 | # Find the handler 512 | for importer in handlers: 513 | ourtype = "%s (%s)" % (importer.description, importer.filetype) 514 | if filetype == ourtype: 515 | handler = importer 516 | self._last_file_handler = handler 517 | 518 | # Load the file 519 | self.display.clear() 520 | self.display.voxels.disable_undo() 521 | self._filename = None 522 | try: 523 | handler.load(filename) 524 | self._filename = filename 525 | except Exception as Ex: 526 | self.display.voxels.enable_undo() 527 | QtGui.QMessageBox.warning(self, "Could not load file", 528 | str(Ex)) 529 | 530 | self.display.build_grids() 531 | #self.display.voxels.resize() 532 | self.display.voxels.saved() 533 | self.display.reset_camera() 534 | self.update_caption() 535 | self.refresh_actions() 536 | self.display.voxels.enable_undo() 537 | self.display.refresh() 538 | 539 | # Registers a tool in the drawing toolbar 540 | def register_tool(self, tool, activate = False): 541 | self._tools.append(tool) 542 | self._tool_group.addAction(tool.get_action()) 543 | self.ui.toolbar_drawing.addAction(tool.get_action()) 544 | if activate: 545 | tool.get_action().setChecked(True) 546 | 547 | # Return the active tool 548 | def get_active_tool(self): 549 | action = self._tool_group.checkedAction() 550 | if not action: 551 | return None 552 | # Find who owns this action and activate 553 | for tool in self._tools: 554 | if tool.get_action() is action: 555 | return tool 556 | return None 557 | 558 | # Load and initialise all plugins 559 | def load_plugins(self): 560 | import plugin_loader 561 | 562 | # Update the state of the UI actions 563 | def refresh_actions(self): 564 | num_frames = self.display.voxels.get_frame_count() 565 | self.ui.action_anim_delete.setEnabled(num_frames > 1) 566 | self.ui.action_anim_previous.setEnabled(num_frames > 1) 567 | self.ui.action_anim_next.setEnabled(num_frames > 1) 568 | self.ui.action_anim_play.setEnabled(num_frames > 1 569 | and not self._timer.isActive()) 570 | self.ui.action_anim_stop.setEnabled(self._timer.isActive()) 571 | self.update_caption() 572 | -------------------------------------------------------------------------------- /src/voxel_widget.py: -------------------------------------------------------------------------------- 1 | # voxel_widget.py 2 | # A 3D OpenGL QT Widget 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import math 19 | import array 20 | import sys 21 | from PySide import QtCore, QtGui, QtOpenGL 22 | from OpenGL.GL import * 23 | from OpenGL.GLU import gluUnProject, gluProject 24 | import voxel 25 | from euclid import LineSegment3, Plane, Point3, Vector3 26 | from tool import EventData, MouseButtons, KeyModifiers 27 | from voxel_grid import GridPlanes 28 | from voxel_grid import VoxelGrid 29 | import time 30 | 31 | class GLWidget(QtOpenGL.QGLWidget): 32 | 33 | # Constants for referring to axis 34 | X_AXIS = 1 35 | Y_AXIS = 2 36 | Z_AXIS = 3 37 | 38 | # Drag types 39 | DRAG_START = 1 40 | DRAG_END = 2 41 | DRAG = 3 42 | 43 | @property 44 | def axis_grids(self): 45 | return self._display_axis_grids 46 | @axis_grids.setter 47 | def axis_grids(self, value): 48 | self._display_axis_grids = value 49 | self.updateGL() 50 | 51 | @property 52 | def wireframe(self): 53 | return self._display_wireframe 54 | @wireframe.setter 55 | def wireframe(self, value): 56 | self._display_wireframe = value 57 | self.updateGL() 58 | 59 | @property 60 | def voxel_colour(self): 61 | return self._voxel_colour 62 | @voxel_colour.setter 63 | def voxel_colour(self, value): 64 | self._voxel_colour = value 65 | 66 | @property 67 | def background(self): 68 | return self._background_colour 69 | @background.setter 70 | def background(self, value): 71 | self._background_colour = value 72 | self.updateGL() 73 | 74 | @property 75 | def voxel_edges(self): 76 | return self._voxeledges 77 | @voxel_edges.setter 78 | def voxel_edges(self, value): 79 | self._voxeledges = value 80 | self.updateGL() 81 | 82 | @property 83 | def grids(self): 84 | return self._grids 85 | 86 | # Our signals 87 | mouse_click_event = QtCore.Signal() 88 | start_drag_event = QtCore.Signal() 89 | drag_event = QtCore.Signal() 90 | end_drag_event = QtCore.Signal() 91 | 92 | def __init__(self, parent = None): 93 | glformat = QtOpenGL.QGLFormat() 94 | glformat.setVersion(1, 1) 95 | glformat.setProfile(QtOpenGL.QGLFormat.CoreProfile) 96 | QtOpenGL.QGLWidget.__init__(self, glformat, parent) 97 | # Test we have a valid context 98 | ver = QtOpenGL.QGLFormat.openGLVersionFlags() 99 | if not ver & QtOpenGL.QGLFormat.OpenGL_Version_1_1: 100 | raise Exception("Requires OpenGL Version 1.1 or above.") 101 | # Default values 102 | self._background_colour = QtGui.QColor("silver") 103 | self._display_wireframe = False 104 | self._voxel_colour = QtGui.QColor.fromHsvF(0, 1.0, 1.0) 105 | self._voxeledges = True 106 | # Mouse position 107 | self._mouse = QtCore.QPoint() 108 | self._mouse_absolute = QtCore.QPoint() 109 | self.mouse_delta_relative = (0,0) 110 | self.mouse_delta_absolute = (0,0) 111 | self.mouse_position = (0,0) 112 | # Default camera 113 | self.reset_camera(False) 114 | # zoom 115 | self._zoom_speed = 0.1 116 | # Render axis grids? 117 | self._display_axis_grids = True 118 | # Our voxel scene 119 | self.voxels = voxel.VoxelData() 120 | # Grid manager 121 | self._grids = VoxelGrid(self.voxels) 122 | # create the default _grids 123 | self.grids.add_grid_plane(GridPlanes.X, offset = 0, visible = True, 124 | color = QtGui.QColor(0x6c, 0x7d, 0x67)) 125 | self.grids.add_grid_plane(GridPlanes.Y, offset = 0, visible = True, 126 | color = QtGui.QColor(0x65, 0x65, 0x7b)) 127 | self.grids.add_grid_plane(GridPlanes.Z, offset = self.voxels.depth, 128 | visible = True, color = QtGui.QColor(0x7b, 0x65, 0x68)) 129 | # Used to track the z component of various mouse activity 130 | self._depth_focus = 1 131 | # Keep track how long mouse buttons are down for 132 | self._mousedown_time = 0 133 | self.button_down = None 134 | self._dragging = False 135 | self._key_modifiers = 0 136 | 137 | # Reset the control and clear all data 138 | def clear(self): 139 | self.voxels.clear() 140 | self.refresh() 141 | 142 | # Force an update of our internal data 143 | def refresh(self): 144 | self.build_mesh() 145 | self.build_grids() 146 | self.updateGL() 147 | 148 | # Reset camera position to defaults 149 | def reset_camera(self, update = True): 150 | self._translate_x = 0 151 | self._translate_y = 0 152 | self._translate_z = -30 153 | self._rotate_x = 0 154 | self._rotate_y = 0 155 | self._rotate_z = 0 156 | if update: 157 | self.updateGL() 158 | 159 | # Initialise OpenGL 160 | def initializeGL(self): 161 | # Set background colour 162 | self.qglClearColor(self._background_colour) 163 | # Our polygon winding order is clockwise 164 | glFrontFace(GL_CW) 165 | # Enable depth testing 166 | glEnable(GL_DEPTH_TEST) 167 | # Enable backface culling 168 | glCullFace(GL_BACK) 169 | glEnable(GL_CULL_FACE) 170 | # Shade model 171 | glShadeModel(GL_SMOOTH) 172 | # Texture support 173 | glEnable(GL_TEXTURE_2D) 174 | # Load our texture 175 | pixmap = QtGui.QPixmap(":/images/gfx/texture.png") 176 | self._texture = self.bindTexture(pixmap) 177 | self.build_mesh() 178 | # Setup our lighting 179 | self.setup_lights() 180 | 181 | # Render our scene 182 | def paintGL(self): 183 | self.qglClearColor(self._background_colour) 184 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 185 | glLoadIdentity() 186 | glTranslatef(self._translate_x, self._translate_y, self._translate_z) 187 | glRotated(self._rotate_x, 1.0, 0.0, 0.0) 188 | glRotated(self._rotate_y, 0.0, 1.0, 0.0) 189 | glRotated(self._rotate_z, 0.0, 0.0, 1.0) 190 | 191 | # Enable vertex buffers 192 | glEnableClientState(GL_VERTEX_ARRAY) 193 | glEnableClientState(GL_TEXTURE_COORD_ARRAY) 194 | glEnableClientState(GL_COLOR_ARRAY) 195 | glEnableClientState(GL_NORMAL_ARRAY) 196 | 197 | # Wireframe? 198 | if self.wireframe: 199 | glPolygonMode(GL_FRONT, GL_LINE) 200 | 201 | # Bind our texture 202 | glBindTexture(GL_TEXTURE_2D, self._texture) 203 | 204 | # Describe our buffers 205 | glVertexPointer(3, GL_FLOAT, 0, self._vertices) 206 | if self._voxeledges: 207 | glTexCoordPointer(2, GL_FLOAT, 0, self._uvs) 208 | else: 209 | glDisable(GL_TEXTURE_2D) 210 | glColorPointer(3, GL_UNSIGNED_BYTE, 0, self._colours) 211 | glNormalPointer(GL_FLOAT, 0, self._normals) 212 | 213 | # Render the buffers 214 | glDrawArrays(GL_TRIANGLES, 0, self._num_vertices) 215 | 216 | glDisableClientState(GL_VERTEX_ARRAY) 217 | glDisableClientState(GL_TEXTURE_COORD_ARRAY) 218 | glDisableClientState(GL_COLOR_ARRAY) 219 | glDisableClientState(GL_NORMAL_ARRAY) 220 | 221 | if not self._voxeledges: 222 | glEnable(GL_TEXTURE_2D) 223 | 224 | # draw the grids 225 | if self._display_axis_grids: 226 | self.grids.paint() 227 | 228 | # Default back to filled rendering 229 | glPolygonMode(GL_FRONT, GL_FILL) 230 | 231 | # Window is resizing 232 | def resizeGL(self, width, height): 233 | self._width = width 234 | self._height = height 235 | glViewport(0, 0, width, height) 236 | glMatrixMode(GL_PROJECTION) 237 | glLoadIdentity() 238 | self.perspective(45.0, float(width) / height, 0.1, 300) 239 | glMatrixMode(GL_MODELVIEW) 240 | 241 | # Render scene as colour ID's 242 | def paintID(self): 243 | # Disable lighting 244 | glDisable(GL_LIGHTING) 245 | glDisable(GL_TEXTURE_2D) 246 | 247 | # Render with white background 248 | self.qglClearColor(QtGui.QColor.fromRgb(0xff, 0xff, 0xff)) 249 | 250 | # Ensure we fill our polygons 251 | glPolygonMode(GL_FRONT, GL_FILL) 252 | 253 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 254 | glLoadIdentity() 255 | glTranslatef(self._translate_x, self._translate_y, self._translate_z) 256 | glRotated(self._rotate_x, 1.0, 0.0, 0.0) 257 | glRotated(self._rotate_y, 0.0, 1.0, 0.0) 258 | glRotated(self._rotate_z, 0.0, 0.0, 1.0) 259 | 260 | # Enable vertex buffers 261 | glEnableClientState(GL_VERTEX_ARRAY) 262 | glEnableClientState(GL_COLOR_ARRAY) 263 | glEnableClientState(GL_NORMAL_ARRAY) 264 | 265 | # Describe our buffers 266 | glVertexPointer(3, GL_FLOAT, 0, self._vertices) 267 | glColorPointer(3, GL_UNSIGNED_BYTE, 0, self._colour_ids) 268 | glNormalPointer(GL_FLOAT, 0, self._normals) 269 | 270 | # Render the buffers 271 | glDrawArrays(GL_TRIANGLES, 0, self._num_vertices) 272 | 273 | glDisableClientState(GL_VERTEX_ARRAY) 274 | glDisableClientState(GL_COLOR_ARRAY) 275 | glDisableClientState(GL_NORMAL_ARRAY) 276 | 277 | # Set background colour back to original 278 | self.qglClearColor(self._background_colour) 279 | 280 | # Re-enable lighting 281 | glEnable(GL_LIGHTING) 282 | glEnable(GL_TEXTURE_2D) 283 | 284 | def perspective(self, fovY, aspect, zNear, zFar): 285 | fH = math.tan(fovY / 360.0 * math.pi) * zNear 286 | fW = fH * aspect 287 | glFrustum(-fW, fW, -fH, fH, zNear, zFar) 288 | 289 | def setup_lights(self): 290 | glEnable(GL_LIGHTING) 291 | glEnable(GL_LIGHT0) 292 | glEnable(GL_COLOR_MATERIAL) 293 | 294 | # Build a mesh from our current voxel data 295 | def build_mesh(self): 296 | # Grab the voxel vertices 297 | (self._vertices, self._colours, self._normals, 298 | self._colour_ids, self._uvs) = self.voxels.get_vertices() 299 | self._num_vertices = len(self._vertices) // 3 300 | self._vertices = array.array("f", self._vertices).tostring() 301 | self._colours = array.array("B", self._colours).tostring() 302 | self._colour_ids = array.array("B", self._colour_ids).tostring() 303 | self._normals = array.array("f", self._normals).tostring() 304 | self._uvs = array.array("f", self._uvs).tostring() 305 | 306 | # Build axis grids 307 | def build_grids(self): 308 | self.grids.update_grid_plane(self.voxels) 309 | 310 | def mousePressEvent(self, event): 311 | 312 | self._key_modifiers = event.modifiers() 313 | self._mousedown_time = time.time() 314 | 315 | self._mouse = QtCore.QPoint(event.pos()) 316 | self._mouse_absolute = QtCore.QPoint(event.pos()) 317 | self.mouse_position = (self._mouse.x(),self._mouse.y()) 318 | 319 | self._button_down = None 320 | if event.buttons() & QtCore.Qt.LeftButton: 321 | self._button_down = MouseButtons.LEFT 322 | elif event.buttons() & QtCore.Qt.MiddleButton: 323 | self._button_down = MouseButtons.MIDDLE 324 | elif event.buttons() & QtCore.Qt.RightButton: 325 | self._button_down = MouseButtons.RIGHT 326 | 327 | # Remember the 3d coordinates of this click 328 | mx, my, mz, d = self.window_to_world(event.x(), event.y()) 329 | mxd, myd, mzd, _ = self.window_to_world(event.x() + 1, event.y(), d) 330 | self._htranslate = ((mxd - mx), (myd - my), (mzd - mz)) 331 | mxd, myd, mzd, _ = self.window_to_world(event.x(), event.y() + 1, d) 332 | self._vtranslate = ((mxd - mx), (myd - my), (mzd - mz)) 333 | # Work out translation for x,y 334 | ax, ay = self.view_axis() 335 | if ax == self.X_AXIS: 336 | self._htranslate = abs(self._htranslate[0]) 337 | if ax == self.Y_AXIS: 338 | self._htranslate = abs(self._htranslate[1]) 339 | if ax == self.Z_AXIS: 340 | self._htranslate = abs(self._htranslate[2]) 341 | if ay == self.X_AXIS: 342 | self._vtranslate = abs(self._vtranslate[0]) 343 | if ay == self.Y_AXIS: 344 | self._vtranslate = abs(self._vtranslate[1]) 345 | if ay == self.Z_AXIS: 346 | self._vtranslate = abs(self._vtranslate[2]) 347 | self._depth_focus = d 348 | 349 | def mouseMoveEvent(self, event): 350 | 351 | self.mouse_position = (self._mouse.x(),self._mouse.y()) 352 | 353 | ctrl = (self._key_modifiers 354 | & QtCore.Qt.KeyboardModifier.ControlModifier) != 0 355 | shift = (self._key_modifiers 356 | & QtCore.Qt.KeyboardModifier.ShiftModifier) != 0 357 | 358 | # Screen units delta 359 | dx = event.x() - self._mouse.x() 360 | dy = event.y() - self._mouse.y() 361 | 362 | # Remember the mouse deltas 363 | self.mouse_delta_relative = (dx, dy) 364 | self.mouse_delta_absolute = (event.x() - self._mouse_absolute.x(), 365 | event.y() - self._mouse_absolute.y()) 366 | 367 | # Maybe we are dragging 368 | if time.time() - self._mousedown_time > 0.3 and not self._dragging: 369 | self._dragging = True 370 | # Announce the start of a drag 371 | x, y, z, face = self.window_to_voxel(event.x(), event.y()) 372 | self.send_drag(self.DRAG_START, x, y, z, event.x(), event.y(), face) 373 | self.refresh() 374 | elif time.time() - self._mousedown_time > 0.3 and self._dragging: 375 | # Already dragging - send a drag event 376 | x, y, z, face = self.window_to_voxel(event.x(), event.y()) 377 | self.send_drag(self.DRAG, x, y, z, event.x(), event.y(), face) 378 | self.refresh() 379 | 380 | # Right mouse button held down with CTRL key - rotate 381 | # Or middle mouse button held 382 | if ((event.buttons() & QtCore.Qt.RightButton and ctrl) 383 | or ((event.buttons() & QtCore.Qt.MiddleButton) and not ctrl)): 384 | self._rotate_x = self._rotate_x + dy 385 | self._rotate_y = self._rotate_y + dx 386 | self.updateGL() 387 | 388 | # Middle mouse button held down with CTRL - translate 389 | if event.buttons() & QtCore.Qt.MiddleButton and ctrl: 390 | 391 | # Work out the translation in 3d space 392 | self._translate_x = self._translate_x + dx * self._htranslate 393 | self._translate_y = self._translate_y + ((-dy) * self._vtranslate) 394 | self.refresh() 395 | 396 | self._mouse = QtCore.QPoint(event.pos()) 397 | 398 | def mouseReleaseEvent(self, event): 399 | self._mouse = QtCore.QPoint(event.pos()) 400 | x, y, z, face = self.window_to_voxel(event.x(), event.y()) 401 | # Send event depenand if this is a click or a drag 402 | if self._dragging: 403 | self._dragging = False 404 | self.send_drag(self.DRAG_END, x, y, z, event.x(), event.y(), face) 405 | else: 406 | self.send_mouse_click(x, y, z, event.x(), event.y(), face) 407 | self.refresh() 408 | 409 | def zoom_in(self): 410 | self._translate_z *= 1 - self._zoom_speed 411 | self.updateGL() 412 | 413 | def zoom_out(self): 414 | self._translate_z *= 1 + self._zoom_speed 415 | self.updateGL() 416 | 417 | def wheelEvent(self, event): 418 | if event.delta() > 0: 419 | self._translate_z *= 1 + self._zoom_speed 420 | else: 421 | self._translate_z *= 1 - self._zoom_speed 422 | self.updateGL() 423 | 424 | # Return voxel space x,y,z coordinates given x, y window coordinates 425 | # Also return an identifier which indicates which face was clicked on. 426 | # If the background was clicked on rather than a voxel, calculate and return 427 | # the location on the floor grid. 428 | def window_to_voxel(self, x, y): 429 | # We must invert y coordinates 430 | y = self._height - y 431 | # Render our scene (to the back buffer) using colour IDs 432 | self.paintID() 433 | # Grab the colour / ID at the coordinates 434 | c = glReadPixels(x, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE) 435 | if type(c) is str: 436 | # This is what MESA on Linux seems to return 437 | # Grab the colour (ID) which was clicked on 438 | voxelid = ord(c[0]) << 16 | ord(c[1]) << 8 | ord(c[2]) 439 | else: 440 | # Windows seems to return an array 441 | voxelid = c[0][0][0] << 16 | c[1][0][0] << 8 | c[2][0][0] 442 | 443 | # Perhaps we clicked on the background? 444 | if voxelid == 0xffffff: 445 | x, y, z = self.plane_intersection(x, y) 446 | if x is None: 447 | return None, None, None, None 448 | return x, y, z, None 449 | # Decode the colour ID into x,y,z,face 450 | x = (voxelid & 0xfe0000) >> 17 451 | y = (voxelid & 0x1fc00) >> 10 452 | z = (voxelid & 0x3f8) >> 3 453 | face = voxelid & 0x07 454 | # Return what we learned 455 | return x, y, z, face 456 | 457 | # Calculate the intersection between mouse coordinates and a plane 458 | def plane_intersection(self, x, y): 459 | # Unproject coordinates into object space 460 | nx, ny, nz = gluUnProject(x, y, 0.0) 461 | fx, fy, fz = gluUnProject(x, y, 1.0) 462 | # Calculate the ray 463 | near = Point3(nx, ny, nz) 464 | far = Point3(fx, fy, fz) 465 | ray = LineSegment3(near, far) 466 | # Define our planes 467 | # XXX origin assumes planes are at zero offsets, should really 468 | # XXX respect any grid plane offset here 469 | origin = self.voxels.voxel_to_world(0, 0, self.voxels.depth) 470 | planes = ( 471 | Plane(Vector3(1, 0, 0), origin[0]), 472 | Plane(Vector3(0, 1, 0), origin[1]), 473 | Plane(Vector3(0, 0, 1), origin[2]+0.001)) 474 | intersection = None, None, None 475 | distance = sys.maxint 476 | for plane in planes: 477 | # Get intersection point 478 | intersect = plane.intersect(ray) 479 | if intersect: 480 | # Adjust to voxel space coordinates 481 | x, y, z = self.voxels.world_to_voxel(intersect.x, 482 | intersect.y, intersect.z) 483 | x = int(x) 484 | y = int(y) 485 | z = int(z) 486 | # Ignore out of bounds insections 487 | if not self.voxels.is_valid_bounds(x, y, z): 488 | continue 489 | length = near.distance(Point3(intersect.x, intersect.y, intersect.z)) 490 | if length < distance: 491 | intersection = int(x), int(y), int(z) 492 | distance = length 493 | return intersection 494 | 495 | # Determine the axis which are perpendicular to our viewing ray, ish 496 | def view_axis(self): 497 | # Shoot a ray into the scene 498 | x1, y1, z1 = gluUnProject(self.width() // 2, self.height() // 2, 0.0) 499 | x2, y2, z2 = gluUnProject(self.width() // 2, self.height() // 2, 1.0) 500 | dx = abs(x2 - x1) 501 | dy = abs(y2 - y1) 502 | dz = abs(z2 - z1) 503 | # The largest deviation is the axis we're looking down 504 | if dz >= dx and dz >= dy: 505 | return (self.X_AXIS, self.Y_AXIS) 506 | elif dy >= dx and dy >= dz: 507 | return (self.X_AXIS, self.Z_AXIS) 508 | return (self.Z_AXIS, self.Y_AXIS) 509 | 510 | # Convert window x,y coordinates into x,y,z world coordinates, also return 511 | # the depth 512 | def window_to_world(self, x, y, z = None): 513 | # Find depth 514 | y = self._height - y 515 | if z is None: 516 | z = glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)[0][0] 517 | fx, fy, fz = gluUnProject(x, y, z) 518 | return fx, fy, fz, z 519 | 520 | # Convert x,y,z world coorindates to x,y window coordinates 521 | def world_to_window(self, x, y, z): 522 | x, y, z = gluProject(x, y, z) 523 | y = self._height - y 524 | return x, y 525 | 526 | def _build_event(self, x, y, z, mouse_x, mouse_y, face): 527 | data = EventData() 528 | data.world_x = x 529 | data.world_y = y 530 | data.world_z = z 531 | data.face = face 532 | data.mouse_x = mouse_x 533 | data.mouse_y = mouse_y 534 | data.mouse_button = self._button_down 535 | data.key_modifiers = self._key_modifiers 536 | data.voxels = self.voxels 537 | return data 538 | 539 | def send_mouse_click(self, x, y, z, mouse_x, mouse_y, face): 540 | self.target = self._build_event(x, y, z, mouse_x, mouse_y, face) 541 | self.mouse_click_event.emit() 542 | 543 | def send_drag(self, dragtype, x, y, z, mouse_x, mouse_y, face): 544 | self.target = self._build_event(x, y, z, mouse_x, mouse_y, face) 545 | if dragtype == self.DRAG_START: 546 | self.start_drag_event.emit() 547 | elif dragtype == self.DRAG_END: 548 | self.end_drag_event.emit() 549 | elif dragtype == self.DRAG: 550 | self.drag_event.emit() 551 | -------------------------------------------------------------------------------- /src/voxel.py: -------------------------------------------------------------------------------- 1 | # voxel.py 2 | # Simple voxel data structure 3 | # Copyright (c) 2013, Graham R King 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # The VoxelData class presents a simple interface to a voxel world. X, Y & Z 19 | # coordinates of voxels run from zero up the positive axis. 20 | # 21 | # Voxel types can be set with a simple call to set(), passing in voxel 22 | # coordinates. 23 | # 24 | # get_vertices() returns a list of vertices, along with normals and colours 25 | # which describes the current state of the voxel world. 26 | 27 | import math 28 | import copy 29 | from undo import Undo, UndoItem 30 | 31 | # Default world dimensions (in voxels) 32 | # We are an editor for "small" voxel models. So this needs to be small. 33 | # Dimensions are fundamentally limited by our encoding of face ID's into 34 | # colours (for picking) to 127x127x126. 35 | _WORLD_WIDTH = 16 36 | _WORLD_HEIGHT = 16 37 | _WORLD_DEPTH = 16 38 | 39 | # Types of voxel 40 | EMPTY = 0 41 | FULL = 1 42 | 43 | # Occlusion factor 44 | OCCLUSION = 0.7 45 | 46 | class VoxelData(object): 47 | 48 | # Constants for referring to axis 49 | X_AXIS = 1 50 | Y_AXIS = 2 51 | Z_AXIS = 3 52 | 53 | # World dimension properties 54 | @property 55 | def width(self): 56 | return self._width 57 | @property 58 | def height(self): 59 | return self._height 60 | @property 61 | def depth(self): 62 | return self._depth 63 | 64 | @property 65 | def changed(self): 66 | return self._changed 67 | @changed.setter 68 | def changed(self, value): 69 | if value and not self._changed: 70 | # Let whoever is watching us know about the change 71 | self._changed = value 72 | if self.notify_changed: 73 | self.notify_changed() 74 | self._changed = value 75 | 76 | @property 77 | def occlusion(self): 78 | return self._occlusion 79 | @occlusion.setter 80 | def occlusion(self, value): 81 | self._occlusion = value 82 | 83 | def __init__(self): 84 | # Default size 85 | self._width = _WORLD_WIDTH 86 | self._height = _WORLD_HEIGHT 87 | self._depth = _WORLD_DEPTH 88 | # Our undo buffer 89 | self._undo = Undo() 90 | # Init data 91 | self._initialise_data() 92 | # Callback when our data changes 93 | self.notify_changed = None 94 | # Ambient occlusion type effect 95 | self._occlusion = True 96 | 97 | # Initialise our data 98 | def _initialise_data(self): 99 | # Our scene data 100 | self._data = self.blank_data() 101 | # Our cache of non-empty voxels (coordinate groups) 102 | self._cache = [] 103 | # Flag indicating if our data has changed 104 | self._changed = False 105 | # Reset undo buffer 106 | self._undo.clear() 107 | # Animation 108 | self._frame_count = 1 109 | self._current_frame = 0 110 | self._frames = [self._data] 111 | 112 | # Return an empty voxel space 113 | def blank_data(self): 114 | return [[[0 for _ in xrange(self.depth)] 115 | for _ in xrange(self.height)] 116 | for _ in xrange(self.width)] 117 | 118 | def is_valid_bounds(self, x, y, z): 119 | return ( 120 | x >= 0 and x < self.width and 121 | y >= 0 and y < self.height and 122 | z >= 0 and z < self.depth 123 | ) 124 | 125 | # Return the number of animation frames 126 | def get_frame_count(self): 127 | return self._frame_count 128 | 129 | # Change to the given frame 130 | def select_frame(self, frame_number): 131 | # Sanity 132 | if frame_number < 0 or frame_number >= self._frame_count: 133 | return 134 | # Make sure we really have a pointer to the current data 135 | self._frames[self._current_frame] = self._data 136 | # Change to new frame 137 | self._data = self._frames[frame_number] 138 | self._current_frame = frame_number 139 | self._undo.frame = self._current_frame 140 | self._cache_rebuild() 141 | self.changed = True 142 | 143 | # Add a new frame by copying the current one 144 | def add_frame(self, copy_current = True): 145 | if copy_current: 146 | data = self.get_data() 147 | else: 148 | data = self.blank_data() 149 | self._frames.insert(self._current_frame+1, data) 150 | self._undo.add_frame(self._current_frame+1) 151 | self._frame_count += 1 152 | self.select_frame(self._current_frame+1) 153 | 154 | # Delete the current frame 155 | def delete_frame(self): 156 | # Sanity - we can't have no frames at all 157 | if self._frame_count <= 1: 158 | return 159 | # Remember the frame we want to delete 160 | killframe = self._current_frame 161 | # Select a different frame 162 | self.select_previous_frame() 163 | # Remove the old frame 164 | del self._frames[killframe] 165 | self._undo.delete_frame(killframe) 166 | self._frame_count -= 1 167 | # If we only have one frame left, must be first frame 168 | if self._frame_count == 1: 169 | self._current_frame = 0 170 | # If we wrapped around, fix the frame pointer 171 | if self._current_frame > killframe: 172 | self._current_frame -= 1 173 | 174 | # Change to the next frame (with wrap) 175 | def select_next_frame(self): 176 | nextframe = self._current_frame+1 177 | if nextframe >= self._frame_count: 178 | nextframe = 0 179 | self.select_frame(nextframe) 180 | 181 | # Change to the previous frame (with wrap) 182 | def select_previous_frame(self): 183 | prevframe = self._current_frame-1 184 | if prevframe < 0: 185 | prevframe = self._frame_count-1 186 | self.select_frame(prevframe) 187 | 188 | # Get current frame number 189 | def get_frame_number(self): 190 | return self._current_frame 191 | 192 | # Set a voxel to the given state 193 | def set(self, x, y, z, state, undo = True): 194 | # If this looks like a QT Color instance, convert it 195 | if hasattr(state, "getRgb"): 196 | c = state.getRgb() 197 | state = c[0]<<24 | c[1]<<16 | c[2]<<8 | 0xff 198 | 199 | # Check bounds 200 | if ( not self.is_valid_bounds(x, y, z ) ): 201 | return False 202 | # Set the voxel 203 | if ( self.is_valid_bounds(x, y, z ) ): 204 | # Add to undo 205 | if undo: 206 | self._undo.add(UndoItem(Undo.SET_VOXEL, 207 | (x, y, z, self._data[x][y][z]), (x, y, z, state))) 208 | self._data[x][y][z] = state 209 | if state != EMPTY: 210 | if (x,y,z) not in self._cache: 211 | self._cache.append((x,y,z)) 212 | else: 213 | if (x,y,z) in self._cache: 214 | self._cache.remove((x,y,z)) 215 | self.changed = True 216 | return True 217 | 218 | # Get the state of the given voxel 219 | def get(self, x, y, z): 220 | if ( not self.is_valid_bounds(x, y, z ) ): 221 | return EMPTY 222 | return self._data[x][y][z] 223 | 224 | # Return a copy of the voxel data 225 | def get_data(self): 226 | return copy.deepcopy(self._data) 227 | 228 | # Set all of our data at once 229 | def set_data(self, data): 230 | self._data = copy.deepcopy(data) 231 | self._cache_rebuild() 232 | self.changed = True 233 | 234 | # Clear our voxel data 235 | def clear(self): 236 | self._initialise_data() 237 | 238 | # Return full vertex list 239 | def get_vertices(self): 240 | vertices = [] 241 | colours = [] 242 | colour_ids = [] 243 | normals = [] 244 | uvs = [] 245 | for x,y,z in self._cache: 246 | v, c, n, cid, uv = self._get_voxel_vertices(x, y, z) 247 | vertices += v 248 | colours += c 249 | normals += n 250 | colour_ids += cid 251 | uvs += uv 252 | return (vertices, colours, normals, colour_ids, uvs) 253 | 254 | # Called to notify us that our data has been saved. i.e. we can set 255 | # our "changed" status back to False. 256 | def saved(self): 257 | self.changed = False 258 | 259 | # Count the number of non-empty voxels from the list of coordinates 260 | def _count_voxels(self, coordinates): 261 | count = 0 262 | for x,y,z in coordinates: 263 | if self.get(x, y, z) != EMPTY: 264 | count += 1 265 | return count 266 | 267 | # Return the verticies for the given voxel. We center our vertices at the origin 268 | def _get_voxel_vertices(self, x, y, z): 269 | vertices = [] 270 | colours = [] 271 | normals = [] 272 | colour_ids = [] 273 | uvs = [] 274 | 275 | # Remember voxel coordinates 276 | vx, vy, vz = x,y,z 277 | 278 | # Determine if we have filled voxels around us 279 | front = self.get(x, y, z-1) == EMPTY 280 | left = self.get(x-1, y, z) == EMPTY 281 | right = self.get(x+1, y, z) == EMPTY 282 | top = self.get(x, y+1, z) == EMPTY 283 | back = self.get(x, y, z+1) == EMPTY 284 | bottom = self.get(x, y-1, z) == EMPTY 285 | 286 | # Get our colour 287 | c = self.get(x, y, z) 288 | r = (c & 0xff000000)>>24 289 | g = (c & 0xff0000)>>16 290 | b = (c & 0xff00)>>8 291 | # Calculate shades for our 4 occlusion levels 292 | shades = [] 293 | for c in range(5): 294 | shades.append(( 295 | int(r*math.pow(OCCLUSION,c)), 296 | int(g*math.pow(OCCLUSION,c)), 297 | int(b*math.pow(OCCLUSION,c)))) 298 | 299 | # Encode our voxel space coordinates as colours, used for face selection 300 | # We use 7 bits per coordinate and the bottom 3 bits for face: 301 | # 0 - front 302 | # 1 - top 303 | # 2 - left 304 | # 3 - right 305 | # 4 - back 306 | # 5 - bottom 307 | voxel_id = (x & 0x7f)<<17 | (y & 0x7f)<<10 | (z & 0x7f)<<3 308 | id_r = (voxel_id & 0xff0000)>>16 309 | id_g = (voxel_id & 0xff00)>>8 310 | id_b = (voxel_id & 0xff) 311 | 312 | # Adjust coordinates to the origin 313 | x, y, z = self.voxel_to_world(x, y, z) 314 | 315 | # Front face 316 | if front: 317 | occ1 = 0 318 | occ2 = 0 319 | occ3 = 0 320 | occ4 = 0 321 | if self._occlusion: 322 | if self.get(vx,vy+1,vz-1) != EMPTY: 323 | occ2 += 1 324 | occ4 += 1 325 | if self.get(vx-1,vy,vz-1) != EMPTY: 326 | occ1 += 1 327 | occ2 += 1 328 | if self.get(vx+1,vy,vz-1) != EMPTY: 329 | occ3 += 1 330 | occ4 += 1 331 | if self.get(vx,vy-1,vz-1) != EMPTY: 332 | occ1 += 1 333 | occ3 += 1 334 | if self.get(vx-1,vy-1,vz-1) != EMPTY: 335 | occ1 += 1 336 | if self.get(vx-1,vy+1,vz-1) != EMPTY: 337 | occ2 += 1 338 | if self.get(vx+1,vy-1,vz-1) != EMPTY: 339 | occ3 += 1 340 | if self.get(vx+1,vy+1,vz-1) != EMPTY: 341 | occ4 += 1 342 | vertices += (x, y, z) 343 | colours += shades[occ1] 344 | vertices += (x, y+1, z) 345 | colours += shades[occ2] 346 | vertices += (x+1, y, z) 347 | colours += shades[occ3] 348 | vertices += (x+1, y, z) 349 | colours += shades[occ3] 350 | vertices += (x, y+1, z) 351 | colours += shades[occ2] 352 | vertices += (x+1, y+1, z) 353 | colours += shades[occ4] 354 | uvs += (0,0,0,1,1,0,1,0,0,1,1,1) 355 | normals += (0, 0, 1) * 6 356 | colour_ids += (id_r, id_g, id_b) * 6 357 | # Top face 358 | if top: 359 | occ1 = 0 360 | occ2 = 0 361 | occ3 = 0 362 | occ4 = 0 363 | if self._occlusion: 364 | if self.get(vx,vy+1,vz+1) != EMPTY: 365 | occ2 += 1 366 | occ4 += 1 367 | if self.get(vx-1,vy+1,vz) != EMPTY: 368 | occ1 += 1 369 | occ2 += 1 370 | if self.get(vx+1,vy+1,vz) != EMPTY: 371 | occ3 += 1 372 | occ4 += 1 373 | if self.get(vx,vy+1,vz-1) != EMPTY: 374 | occ1 += 1 375 | occ3 += 1 376 | if self.get(vx-1,vy+1,vz-1) != EMPTY: 377 | occ1 += 1 378 | if self.get(vx+1,vy+1,vz-1) != EMPTY: 379 | occ3 += 1 380 | if self.get(vx+1,vy+1,vz+1) != EMPTY: 381 | occ4 += 1 382 | if self.get(vx-1,vy+1,vz+1) != EMPTY: 383 | occ2 += 1 384 | vertices += (x, y+1, z) 385 | colours += shades[occ1] 386 | vertices += (x, y+1, z-1) 387 | colours += shades[occ2] 388 | vertices += (x+1, y+1, z) 389 | colours += shades[occ3] 390 | vertices += (x+1, y+1, z) 391 | colours += shades[occ3] 392 | vertices += (x, y+1, z-1) 393 | colours += shades[occ2] 394 | vertices += (x+1, y+1, z-1) 395 | colours += shades[occ4] 396 | uvs += (0,0,0,1,1,0,1,0,0,1,1,1) 397 | normals += (0, 1, 0) * 6 398 | colour_ids += (id_r, id_g, id_b | 1) * 6 399 | # Right face 400 | if right: 401 | occ1 = 0 402 | occ2 = 0 403 | occ3 = 0 404 | occ4 = 0 405 | if self._occlusion: 406 | if self.get(vx+1,vy+1,vz) != EMPTY: 407 | occ2 += 1 408 | occ4 += 1 409 | if self.get(vx+1,vy,vz-1) != EMPTY: 410 | occ1 += 1 411 | occ2 += 1 412 | if self.get(vx+1,vy,vz+1) != EMPTY: 413 | occ3 += 1 414 | occ4 += 1 415 | if self.get(vx+1,vy-1,vz) != EMPTY: 416 | occ1 += 1 417 | occ3 += 1 418 | if self.get(vx+1,vy-1,vz-1) != EMPTY: 419 | occ1 += 1 420 | if self.get(vx+1,vy+1,vz-1) != EMPTY: 421 | occ2 += 1 422 | if self.get(vx+1,vy-1,vz+1) != EMPTY: 423 | occ3 += 1 424 | if self.get(vx+1,vy+1,vz+1) != EMPTY: 425 | occ4 += 1 426 | vertices += (x+1, y, z) 427 | colours += shades[occ1] 428 | vertices += (x+1, y+1, z) 429 | colours += shades[occ2] 430 | vertices += (x+1, y, z-1) 431 | colours += shades[occ3] 432 | vertices += (x+1, y, z-1) 433 | colours += shades[occ3] 434 | vertices += (x+1, y+1, z) 435 | colours += shades[occ2] 436 | vertices += (x+1, y+1, z-1) 437 | colours += shades[occ4] 438 | uvs += (0,0,0,1,1,0,1,0,0,1,1,1) 439 | normals += (1, 0, 0) * 6 440 | colour_ids += (id_r, id_g, id_b | 3) * 6 441 | # Left face 442 | if left: 443 | occ1 = 0 444 | occ2 = 0 445 | occ3 = 0 446 | occ4 = 0 447 | if self._occlusion: 448 | if self.get(vx-1,vy+1,vz) != EMPTY: 449 | occ2 += 1 450 | occ4 += 1 451 | if self.get(vx-1,vy,vz+1) != EMPTY: 452 | occ1 += 1 453 | occ2 += 1 454 | if self.get(vx-1,vy,vz-1) != EMPTY: 455 | occ3 += 1 456 | occ4 += 1 457 | if self.get(vx-1,vy-1,vz) != EMPTY: 458 | occ1 += 1 459 | occ3 += 1 460 | if self.get(vx-1,vy-1,vz+1) != EMPTY: 461 | occ1 += 1 462 | if self.get(vx-1,vy+1,vz+1) != EMPTY: 463 | occ2 += 1 464 | if self.get(vx-1,vy-1,vz-1) != EMPTY: 465 | occ3 += 1 466 | if self.get(vx-1,vy+1,vz-1) != EMPTY: 467 | occ4 += 1 468 | vertices += (x, y, z-1) 469 | colours += shades[occ1] 470 | vertices += (x, y+1, z-1) 471 | colours += shades[occ2] 472 | vertices += (x, y, z) 473 | colours += shades[occ3] 474 | vertices += (x, y, z) 475 | colours += shades[occ3] 476 | vertices += (x, y+1, z-1) 477 | colours += shades[occ2] 478 | vertices += (x, y+1, z) 479 | colours += shades[occ4] 480 | uvs += (0,0,0,1,1,0,1,0,0,1,1,1) 481 | normals += (-1, 0, 0) * 6 482 | colour_ids += (id_r, id_g, id_b | 2) * 6 483 | # Back face 484 | if back: 485 | occ1 = 0 486 | occ2 = 0 487 | occ3 = 0 488 | occ4 = 0 489 | if self._occlusion: 490 | if self.get(vx,vy+1,vz+1) != EMPTY: 491 | occ2 += 1 492 | occ4 += 1 493 | if self.get(vx+1,vy,vz+1) != EMPTY: 494 | occ1 += 1 495 | occ2 += 1 496 | if self.get(vx-1,vy,vz+1) != EMPTY: 497 | occ3 += 1 498 | occ4 += 1 499 | if self.get(vx,vy-1,vz+1) != EMPTY: 500 | occ1 += 1 501 | occ3 += 1 502 | if self.get(vx+1,vy-1,vz+1) != EMPTY: 503 | occ1 += 1 504 | if self.get(vx+1,vy+1,vz+1) != EMPTY: 505 | occ2 += 1 506 | if self.get(vx-1,vy-1,vz+1) != EMPTY: 507 | occ3 += 1 508 | if self.get(vx-1,vy+1,vz+1) != EMPTY: 509 | occ4 += 1 510 | vertices += (x+1, y, z-1) 511 | colours += shades[occ1] 512 | vertices += (x+1, y+1, z-1) 513 | colours += shades[occ2] 514 | vertices += (x, y, z-1) 515 | colours += shades[occ3] 516 | vertices += (x, y, z-1) 517 | colours += shades[occ3] 518 | vertices += (x+1, y+1, z-1) 519 | colours += shades[occ2] 520 | vertices += (x, y+1, z-1) 521 | colours += shades[occ4] 522 | uvs += (0,0,0,1,1,0,1,0,0,1,1,1) 523 | normals += (0, 0, -1) * 6 524 | colour_ids += (id_r, id_g, id_b | 4) * 6 525 | # Bottom face 526 | if bottom: 527 | occ1 = 0 528 | occ2 = 0 529 | occ3 = 0 530 | occ4 = 0 531 | if self._occlusion: 532 | if self.get(vx,vy-1,vz-1) != EMPTY: 533 | occ2 += 1 534 | occ4 += 1 535 | if self.get(vx-1,vy-1,vz) != EMPTY: 536 | occ1 += 1 537 | occ2 += 1 538 | if self.get(vx+1,vy-1,vz) != EMPTY: 539 | occ3 += 1 540 | occ4 += 1 541 | if self.get(vx,vy-1,vz+1) != EMPTY: 542 | occ1 += 1 543 | occ3 += 1 544 | if self.get(vx-1,vy-1,vz+1) != EMPTY: 545 | occ1 += 1 546 | if self.get(vx-1,vy-1,vz-1) != EMPTY: 547 | occ2 += 1 548 | if self.get(vx+1,vy-1,vz+1) != EMPTY: 549 | occ3 += 1 550 | if self.get(vx+1,vy-1,vz-1) != EMPTY: 551 | occ4 += 1 552 | vertices += (x, y, z-1) 553 | colours += shades[occ1] 554 | vertices += (x, y, z) 555 | colours += shades[occ2] 556 | vertices += (x+1, y, z-1) 557 | colours += shades[occ3] 558 | vertices += (x+1, y, z-1) 559 | colours += shades[occ3] 560 | vertices += (x, y, z) 561 | colours += shades[occ2] 562 | vertices += (x+1, y, z) 563 | colours += shades[occ4] 564 | uvs += (0,0,0,1,1,0,1,0,0,1,1,1) 565 | normals += (0, -1, 0) * 6 566 | colour_ids += (id_r, id_g, id_b | 5) * 6 567 | 568 | return (vertices, colours, normals, colour_ids, uvs) 569 | 570 | 571 | # Return vertices for a floor grid 572 | def get_grid_vertices(self): 573 | grid = [] 574 | #builds the Y_plane 575 | for z in xrange(self.depth+1): 576 | gx, gy, gz = self.voxel_to_world(0, 0, z) 577 | grid += (gx, gy, gz) 578 | gx, gy, gz = self.voxel_to_world(self.width, 0, z) 579 | grid += (gx, gy, gz) 580 | for x in xrange(self.width+1): 581 | gx, gy, gz = self.voxel_to_world(x, 0, 0) 582 | grid += (gx, gy, gz) 583 | gx, gy, gz = self.voxel_to_world(x, 0, self.depth) 584 | grid += (gx, gy, gz) 585 | #builds the Z_plane 586 | for x in xrange(self.width+1): 587 | gx, gy, gz = self.voxel_to_world(x, 0, self.depth) 588 | grid += (gx, gy, gz) 589 | gx, gy, gz = self.voxel_to_world(x, self.height, self.depth) 590 | grid += (gx, gy, gz) 591 | for y in xrange(self.height+1): 592 | gx, gy, gz = self.voxel_to_world(0, y, self.depth) 593 | grid += (gx, gy, gz) 594 | gx, gy, gz = self.voxel_to_world(self.width, y, self.depth) 595 | grid += (gx, gy, gz) 596 | #builds the X_plane 597 | for y in xrange(self.height+1): 598 | gx, gy, gz = self.voxel_to_world(0, y, 0) 599 | grid += (gx, gy, gz) 600 | gx, gy, gz = self.voxel_to_world(0, y, self.depth) 601 | grid += (gx, gy, gz) 602 | for z in xrange(self.depth+1): 603 | gx, gy, gz = self.voxel_to_world(0, 0, z) 604 | grid += (gx, gy, gz) 605 | gx, gy, gz = self.voxel_to_world(0, self.height, z) 606 | grid += (gx, gy, gz) 607 | return grid 608 | 609 | # Convert voxel space coordinates to world space 610 | def voxel_to_world(self, x, y, z): 611 | x = (x - self.width//2)-0.5 612 | y = (y - self.height//2)-0.5 613 | z = (z - self.depth//2)-0.5 614 | z = -z 615 | return x, y, z 616 | 617 | # Convert world space coordinates to voxel space 618 | def world_to_voxel(self, x, y, z): 619 | x = (x + self.width//2)+0.5 620 | y = (y + self.height//2)+0.5 621 | z = (z - self.depth//2)-0.5 622 | z = -z 623 | return x, y, z 624 | 625 | # Rebuild our cache 626 | def _cache_rebuild(self): 627 | self._cache = [] 628 | for x in range(self.width): 629 | for z in range(self.depth): 630 | for y in range(self.height): 631 | if self._data[x][y][z] != EMPTY: 632 | self._cache.append((x, y, z)) 633 | 634 | # Calculate the actual bounding box of the model in voxel space 635 | # Consider all animation frames 636 | def get_bounding_box(self): 637 | minx = 999 638 | miny = 999 639 | minz = 999 640 | maxx = -999 641 | maxy = -999 642 | maxz = -999 643 | for data in self._frames: 644 | for x in range(self.width): 645 | for z in range(self.depth): 646 | for y in range(self.height): 647 | if data[x][y][z] != EMPTY: 648 | if x < minx: 649 | minx = x 650 | if x > maxx: 651 | maxx = x 652 | if y < miny: 653 | miny = y 654 | if y > maxy: 655 | maxy = y 656 | if z < minz: 657 | minz = z 658 | if z > maxz: 659 | maxz = z 660 | width = (maxx-minx)+1 661 | height = (maxy-miny)+1 662 | depth = (maxz-minz)+1 663 | return minx, miny, minz, width, height, depth 664 | 665 | # Resize the voxel space. If no dimensions given, adjust to bounding box. 666 | # We offset all voxels on all axis by the given amount. 667 | # Resize all animation frames 668 | def resize(self, width = None, height = None, depth = None, shift = 0): 669 | # Reset undo buffer 670 | self._undo.clear() 671 | # No dimensions, use bounding box 672 | mx, my, mz, cwidth, cheight, cdepth = self.get_bounding_box() 673 | if not width: 674 | width, height, depth = cwidth, cheight, cdepth 675 | for i, frame in enumerate(self._frames): 676 | # Create new data structure of the required size 677 | data = [[[0 for _ in xrange(depth)] 678 | for _ in xrange(height)] 679 | for _ in xrange(width)] 680 | # Adjust ranges 681 | movewidth = min(width, cwidth) 682 | moveheight = min(height, cheight) 683 | movedepth = min(depth, cdepth) 684 | # Calculate translation 685 | dx = (0-mx)+shift 686 | dy = (0-my)+shift 687 | dz = (0-mz)+shift 688 | # Copy data over at new location 689 | for x in xrange(mx, mx+movewidth): 690 | for y in xrange(my, my+moveheight): 691 | for z in xrange(mz, mz+movedepth): 692 | data[x+dx][y+dy][z+dz] = frame[x][y][z] 693 | self._frames[i] = data 694 | self._data = self._frames[self._current_frame] 695 | # Set new dimensions 696 | self._width = width 697 | self._height = height 698 | self._depth = depth 699 | # Rebuild our cache 700 | self._cache_rebuild() 701 | self.changed = True 702 | 703 | # Rotate voxels in voxel space 90 degrees 704 | def rotate_about_axis(self, axis): 705 | # Reset undo buffer 706 | self._undo.clear() 707 | 708 | if axis == self.Y_AXIS: 709 | width = self.depth # note swap 710 | height = self.height 711 | depth = self.width 712 | elif axis == self.X_AXIS: 713 | width = self.width 714 | height = self.depth 715 | depth = self.height 716 | elif axis == self.Z_AXIS: 717 | width = self.height 718 | height = self.width 719 | depth = self.depth 720 | 721 | for i, frame in enumerate(self._frames): 722 | 723 | # Create new temporary data structure 724 | data = [[[0 for _ in xrange(depth)] 725 | for _ in xrange(height)] 726 | for _ in xrange(width)] 727 | 728 | # Copy data over at new location 729 | for tx in xrange(0, self.width): 730 | for ty in xrange(0, self.height): 731 | for tz in xrange(0, self.depth): 732 | if axis == self.Y_AXIS: 733 | dx = (-tz)-1 734 | dy = ty 735 | dz = tx 736 | elif axis == self.X_AXIS: 737 | dx = tx 738 | dy = (-tz)-1 739 | dz = ty 740 | elif axis == self.Z_AXIS: 741 | dx = ty 742 | dy = (-tx)-1 743 | dz = tz 744 | data[dx][dy][dz] = frame[tx][ty][tz] 745 | self._frames[i] = data 746 | 747 | self._width = width 748 | self._height = height 749 | self._depth = depth 750 | 751 | self._data = self._frames[self._current_frame] 752 | # Rebuild our cache 753 | self._cache_rebuild() 754 | self.changed = True 755 | 756 | # Translate the voxel data. 757 | def translate(self, x, y, z, undo = True): 758 | # Sanity 759 | if x == 0 and y == 0 and z == 0: 760 | return 761 | 762 | # Add to undo 763 | if undo: 764 | self._undo.add(UndoItem(Undo.TRANSLATE, 765 | (-x, -y, -z), (x, y, z))) 766 | 767 | # Create new temporary data structure 768 | data = [[[0 for _ in xrange(self.depth)] 769 | for _ in xrange(self.height)] 770 | for _ in xrange(self.width)] 771 | # Copy data over at new location 772 | for tx in xrange(0, self.width): 773 | for ty in xrange(0, self.height): 774 | for tz in xrange(0, self.depth): 775 | dx = (tx+x) % self.width 776 | dy = (ty+y) % self.height 777 | dz = (tz+z) % self.depth 778 | data[dx][dy][dz] = self._data[tx][ty][tz] 779 | self._data = data 780 | self._frames[self._current_frame] = self._data 781 | # Rebuild our cache 782 | self._cache_rebuild() 783 | self.changed = True 784 | 785 | # Undo previous operation 786 | def undo(self): 787 | op = self._undo.undo() 788 | # Voxel edit 789 | if op and op.operation == Undo.SET_VOXEL: 790 | data = op.olddata 791 | self.set(data[0], data[1], data[2], data[3], False) 792 | # Translation 793 | elif op and op.operation == Undo.TRANSLATE: 794 | data = op.olddata 795 | self.translate(data[0], data[1], data[2], False) 796 | 797 | # Redo an undone operation 798 | def redo(self): 799 | op = self._undo.redo() 800 | # Voxel edit 801 | if op and op.operation == Undo.SET_VOXEL: 802 | data = op.newdata 803 | self.set(data[0], data[1], data[2], data[3], False) 804 | # Translation 805 | elif op and op.operation == Undo.TRANSLATE: 806 | data = op.newdata 807 | self.translate(data[0], data[1], data[2], False) 808 | 809 | # Enable/Disable undo buffer 810 | def disable_undo(self): 811 | self._undo.enabled = False 812 | def enable_undo(self): 813 | self._undo.enabled = True 814 | --------------------------------------------------------------------------------