├── 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 |
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 |
--------------------------------------------------------------------------------