Major overhaul of papercraft-unfold (not finished yet)

This commit is contained in:
Mario Voigt 2020-09-09 02:18:27 +02:00
parent a724a3dc12
commit 7f099ab621
43 changed files with 2774 additions and 102 deletions

Binary file not shown.

View File

@ -0,0 +1,41 @@
# libadmesh.la - a libtool library file
# Generated by libtool (GNU libtool) 2.4.6 Debian-2.4.6-14
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# The name that we can dlopen(3).
dlname='libadmesh.so.1'
# Names of this library.
library_names='libadmesh.so.1.0.0 libadmesh.so.1 libadmesh.so'
# The name of the static archive.
old_library=''
# Linker flags that cannot go in dependency_libs.
inherited_linker_flags=''
# Libraries that this one depends upon.
dependency_libs=' -lm'
# Names of additional weak libraries provided by this library
weak_library_names=''
# Version information for libadmesh.
current=1
age=0
revision=0
# Is this an already installed library?
installed=no
# Should we warn about portability when linking against -modules?
shouldnotlink=no
# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='/usr/local/lib'

View File

@ -0,0 +1,41 @@
# libadmesh.la - a libtool library file
# Generated by libtool (GNU libtool) 2.4.6 Debian-2.4.6-14
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# The name that we can dlopen(3).
dlname='libadmesh.so.1'
# Names of this library.
library_names='libadmesh.so.1.0.0 libadmesh.so.1 libadmesh.so'
# The name of the static archive.
old_library=''
# Linker flags that cannot go in dependency_libs.
inherited_linker_flags=''
# Libraries that this one depends upon.
dependency_libs=' -lm'
# Names of additional weak libraries provided by this library
weak_library_names=''
# Version information for libadmesh.
current=1
age=0
revision=0
# Is this an already installed library?
installed=yes
# Should we warn about portability when linking against -modules?
shouldnotlink=no
# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='/usr/local/lib'

View File

@ -0,0 +1,210 @@
#! /bin/bash
# admesh - temporary wrapper script for .libs/admesh
# Generated by libtool (GNU libtool) 2.4.6 Debian-2.4.6-14
#
# The admesh program cannot be directly executed until all the libtool
# libraries that it depends on are installed.
#
# This wrapper script should never be moved out of the build directory.
# If it is, it will not operate correctly.
# Sed substitution that helps us do robust quoting. It backslashifies
# metacharacters that are still active within double-quoted strings.
sed_quote_subst='s|\([`"$\\]\)|\\\1|g'
# Be Bourne compatible
if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then
emulate sh
NULLCMD=:
# Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which
# is contrary to our usage. Disable this feature.
alias -g '${1+"$@"}'='"$@"'
setopt NO_GLOB_SUBST
else
case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac
fi
BIN_SH=xpg4; export BIN_SH # for Tru64
DUALCASE=1; export DUALCASE # for MKS sh
# The HP-UX ksh and POSIX shell print the target directory to stdout
# if CDPATH is set.
(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
relink_command=""
# This environment variable determines our operation mode.
if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then
# install mode needs the following variables:
generated_by_libtool_version='2.4.6'
notinst_deplibs=' libadmesh.la'
else
# When we are sourced in execute mode, $file and $ECHO are already set.
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
file="$0"
# A function that is used when there is no print builtin or printf.
func_fallback_echo ()
{
eval 'cat <<_LTECHO_EOF
$1
_LTECHO_EOF'
}
ECHO="printf %s\\n"
fi
# Very basic option parsing. These options are (a) specific to
# the libtool wrapper, (b) are identical between the wrapper
# /script/ and the wrapper /executable/ that is used only on
# windows platforms, and (c) all begin with the string --lt-
# (application programs are unlikely to have options that match
# this pattern).
#
# There are only two supported options: --lt-debug and
# --lt-dump-script. There is, deliberately, no --lt-help.
#
# The first argument to this parsing function should be the
# script's ./libtool value, followed by no.
lt_option_debug=
func_parse_lt_options ()
{
lt_script_arg0=$0
shift
for lt_opt
do
case "$lt_opt" in
--lt-debug) lt_option_debug=1 ;;
--lt-dump-script)
lt_dump_D=`$ECHO "X$lt_script_arg0" | /usr/bin/sed -e 's/^X//' -e 's%/[^/]*$%%'`
test "X$lt_dump_D" = "X$lt_script_arg0" && lt_dump_D=.
lt_dump_F=`$ECHO "X$lt_script_arg0" | /usr/bin/sed -e 's/^X//' -e 's%^.*/%%'`
cat "$lt_dump_D/$lt_dump_F"
exit 0
;;
--lt-*)
$ECHO "Unrecognized --lt- option: '$lt_opt'" 1>&2
exit 1
;;
esac
done
# Print the debug banner immediately:
if test -n "$lt_option_debug"; then
echo "admesh:admesh:$LINENO: libtool wrapper (GNU libtool) 2.4.6 Debian-2.4.6-14" 1>&2
fi
}
# Used when --lt-debug. Prints its arguments to stdout
# (redirection is the responsibility of the caller)
func_lt_dump_args ()
{
lt_dump_args_N=1;
for lt_arg
do
$ECHO "admesh:admesh:$LINENO: newargv[$lt_dump_args_N]: $lt_arg"
lt_dump_args_N=`expr $lt_dump_args_N + 1`
done
}
# Core function for launching the target application
func_exec_program_core ()
{
if test -n "$lt_option_debug"; then
$ECHO "admesh:admesh:$LINENO: newargv[0]: $progdir/$program" 1>&2
func_lt_dump_args ${1+"$@"} 1>&2
fi
exec "$progdir/$program" ${1+"$@"}
$ECHO "$0: cannot exec $program $*" 1>&2
exit 1
}
# A function to encapsulate launching the target application
# Strips options in the --lt-* namespace from $@ and
# launches target application with the remaining arguments.
func_exec_program ()
{
case " $* " in
*\ --lt-*)
for lt_wr_arg
do
case $lt_wr_arg in
--lt-*) ;;
*) set x "$@" "$lt_wr_arg"; shift;;
esac
shift
done ;;
esac
func_exec_program_core ${1+"$@"}
}
# Parse options
func_parse_lt_options "$0" ${1+"$@"}
# Find the directory that this script lives in.
thisdir=`$ECHO "$file" | /usr/bin/sed 's%/[^/]*$%%'`
test "x$thisdir" = "x$file" && thisdir=.
# Follow symbolic links until we get to the real thisdir.
file=`ls -ld "$file" | /usr/bin/sed -n 's/.*-> //p'`
while test -n "$file"; do
destdir=`$ECHO "$file" | /usr/bin/sed 's%/[^/]*$%%'`
# If there was a directory component, then change thisdir.
if test "x$destdir" != "x$file"; then
case "$destdir" in
[\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;;
*) thisdir="$thisdir/$destdir" ;;
esac
fi
file=`$ECHO "$file" | /usr/bin/sed 's%^.*/%%'`
file=`ls -ld "$thisdir/$file" | /usr/bin/sed -n 's/.*-> //p'`
done
# Usually 'no', except on cygwin/mingw when embedded into
# the cwrapper.
WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no
if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then
# special case for '.'
if test "$thisdir" = "."; then
thisdir=`pwd`
fi
# remove .libs from thisdir
case "$thisdir" in
*[\\/].libs ) thisdir=`$ECHO "$thisdir" | /usr/bin/sed 's%[\\/][^\\/]*$%%'` ;;
.libs ) thisdir=. ;;
esac
fi
# Try to get the absolute directory name.
absdir=`cd "$thisdir" && pwd`
test -n "$absdir" && thisdir="$absdir"
program='admesh'
progdir="$thisdir/.libs"
if test -f "$progdir/$program"; then
# Add our own library path to LD_LIBRARY_PATH
LD_LIBRARY_PATH="/tmp/admesh/.libs:$LD_LIBRARY_PATH"
# Some systems cannot cope with colon-terminated LD_LIBRARY_PATH
# The second colon is a workaround for a bug in BeOS R4 sed
LD_LIBRARY_PATH=`$ECHO "$LD_LIBRARY_PATH" | /usr/bin/sed 's/::*$//'`
export LD_LIBRARY_PATH
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
# Run the actual program with our arguments.
func_exec_program ${1+"$@"}
fi
else
# The program doesn't exist.
$ECHO "$0: error: '$progdir/$program' does not exist" 1>&2
$ECHO "This script is just a wrapper for $program." 1>&2
$ECHO "See the libtool documentation for more information." 1>&2
exit 1
fi
fi

Binary file not shown.

View File

@ -0,0 +1,201 @@
/* ADMesh -- process triangulated solid meshes
* Copyright (C) 1995, 1996 Anthony D. Martin <amartin@engr.csulb.edu>
* Copyright (C) 2013, 2014 several contributors, see AUTHORS
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Questions, comments, suggestions, etc to
* https://github.com/admesh/admesh/issues
*/
#ifndef __admesh_stl__
#define __admesh_stl__
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
#define STL_MAX(A,B) ((A)>(B)? (A):(B))
#define STL_MIN(A,B) ((A)<(B)? (A):(B))
#define ABS(X) ((X) < 0 ? -(X) : (X))
#define LABEL_SIZE 80
#define NUM_FACET_SIZE 4
#define HEADER_SIZE 84
#define STL_MIN_FILE_SIZE 284
#define ASCII_LINES_PER_FACET 7
#define SIZEOF_EDGE_SORT 24
typedef struct {
float x;
float y;
float z;
} stl_vertex;
typedef struct {
float x;
float y;
float z;
} stl_normal;
typedef char stl_extra[2];
typedef struct {
stl_normal normal;
stl_vertex vertex[3];
stl_extra extra;
} stl_facet;
#define SIZEOF_STL_FACET 50
typedef enum {binary, ascii, inmemory} stl_type;
typedef struct {
stl_vertex p1;
stl_vertex p2;
int facet_number;
} stl_edge;
typedef struct stl_hash_edge {
unsigned key[6];
int facet_number;
int which_edge;
struct stl_hash_edge *next;
} stl_hash_edge;
typedef struct {
int neighbor[3];
char which_vertex_not[3];
} stl_neighbors;
typedef struct {
int vertex[3];
} v_indices_struct;
typedef struct {
char header[81];
stl_type type;
int number_of_facets;
stl_vertex max;
stl_vertex min;
stl_vertex size;
float bounding_diameter;
float shortest_edge;
float volume;
unsigned number_of_blocks;
int connected_edges;
int connected_facets_1_edge;
int connected_facets_2_edge;
int connected_facets_3_edge;
int facets_w_1_bad_edge;
int facets_w_2_bad_edge;
int facets_w_3_bad_edge;
int original_num_facets;
int edges_fixed;
int degenerate_facets;
int facets_removed;
int facets_added;
int facets_reversed;
int backwards_edges;
int normals_fixed;
int number_of_parts;
int malloced;
int freed;
int facets_malloced;
int collisions;
int shared_vertices;
int shared_malloced;
} stl_stats;
typedef struct {
FILE *fp;
stl_facet *facet_start;
stl_edge *edge_start;
stl_hash_edge **heads;
stl_hash_edge *tail;
int M;
stl_neighbors *neighbors_start;
v_indices_struct *v_indices;
stl_vertex *v_shared;
stl_stats stats;
char error;
} stl_file;
extern void stl_open(stl_file *stl, char *file);
extern void stl_close(stl_file *stl);
extern void stl_stats_out(stl_file *stl, FILE *file, char *input_file);
extern void stl_print_edges(stl_file *stl, FILE *file);
extern void stl_print_neighbors(stl_file *stl, char *file);
extern void stl_put_little_int(FILE *fp, int value_in);
extern void stl_put_little_float(FILE *fp, float value_in);
extern void stl_write_ascii(stl_file *stl, const char *file, const char *label);
extern void stl_write_binary(stl_file *stl, const char *file, const char *label);
extern void stl_write_binary_block(stl_file *stl, FILE *fp);
extern void stl_check_facets_exact(stl_file *stl);
extern void stl_check_facets_nearby(stl_file *stl, float tolerance);
extern void stl_remove_unconnected_facets(stl_file *stl);
extern void stl_write_vertex(stl_file *stl, int facet, int vertex);
extern void stl_write_facet(stl_file *stl, char *label, int facet);
extern void stl_write_edge(stl_file *stl, char *label, stl_hash_edge edge);
extern void stl_write_neighbor(stl_file *stl, int facet);
extern void stl_write_quad_object(stl_file *stl, char *file);
extern void stl_verify_neighbors(stl_file *stl);
extern void stl_fill_holes(stl_file *stl);
extern void stl_fix_normal_directions(stl_file *stl);
extern void stl_fix_normal_values(stl_file *stl);
extern void stl_reverse_all_facets(stl_file *stl);
extern void stl_translate(stl_file *stl, float x, float y, float z);
extern void stl_translate_relative(stl_file *stl, float x, float y, float z);
extern void stl_scale_versor(stl_file *stl, float versor[3]);
extern void stl_scale(stl_file *stl, float factor);
extern void stl_rotate_x(stl_file *stl, float angle);
extern void stl_rotate_y(stl_file *stl, float angle);
extern void stl_rotate_z(stl_file *stl, float angle);
extern void stl_mirror_xy(stl_file *stl);
extern void stl_mirror_yz(stl_file *stl);
extern void stl_mirror_xz(stl_file *stl);
extern void stl_open_merge(stl_file *stl, char *file);
extern void stl_invalidate_shared_vertices(stl_file *stl);
extern void stl_generate_shared_vertices(stl_file *stl);
extern void stl_write_obj(stl_file *stl, char *file);
extern void stl_write_off(stl_file *stl, char *file);
extern void stl_write_dxf(stl_file *stl, char *file, char *label);
extern void stl_write_vrml(stl_file *stl, char *file);
extern void stl_calculate_normal(float normal[], stl_facet *facet);
extern void stl_normalize_vector(float v[]);
extern void stl_calculate_volume(stl_file *stl);
extern void stl_repair(stl_file *stl, int fixall_flag, int exact_flag, int tolerance_flag, float tolerance, int increment_flag, float increment, int nearby_flag, int iterations, int remove_unconnected_flag, int fill_holes_flag, int normal_directions_flag, int normal_values_flag, int reverse_all_flag, int verbose_flag);
extern void stl_initialize(stl_file *stl);
extern void stl_count_facets(stl_file *stl, char *file);
extern void stl_allocate(stl_file *stl);
extern void stl_read(stl_file *stl, int first_facet, int first);
extern void stl_facet_stats(stl_file *stl, stl_facet facet, int first);
extern void stl_reallocate(stl_file *stl);
extern void stl_add_facet(stl_file *stl, stl_facet *new_facet);
extern void stl_get_size(stl_file *stl);
extern void stl_clear_error(stl_file *stl);
extern int stl_get_error(stl_file *stl);
extern void stl_exit_on_error(stl_file *stl);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,41 @@
# libadmesh.la - a libtool library file
# Generated by libtool (GNU libtool) 2.4.6
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# The name that we can dlopen(3).
dlname='../bin/libadmesh-1.dll'
# Names of this library.
library_names='libadmesh.dll.a'
# The name of the static archive.
old_library=''
# Linker flags that cannot go in dependency_libs.
inherited_linker_flags=''
# Libraries that this one depends upon.
dependency_libs=''
# Names of additional weak libraries provided by this library
weak_library_names=''
# Version information for libadmesh.
current=1
age=0
revision=0
# Is this an already installed library?
installed=yes
# Should we warn about portability when linking against -modules?
shouldnotlink=no
# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='/usr/x86_64-w64-mingw32/sys-root/mingw/lib'

View File

@ -0,0 +1,11 @@
prefix=/usr/x86_64-w64-mingw32/sys-root/mingw
exec_prefix=/usr/x86_64-w64-mingw32/sys-root/mingw
libdir=/usr/x86_64-w64-mingw32/sys-root/mingw/lib
includedir=/usr/x86_64-w64-mingw32/sys-root/mingw/include
Name: libadmesh
Description: Library for woring with admesh
Version: 0.98.3
Libs: -L${libdir} -ladmesh
Libs.private:
Cflags: -I${includedir}

Binary file not shown.

View File

@ -0,0 +1,10 @@
newmtl cubemtl
Ns 10
Ni 1.0
d 1.0
Tf 1 1 1
illum 2
Ka 0.5 0.5 0.
Kd 0.9 0.9 0.9
Ks 0.0 0.0 0.0
map_Kd cube.png

View File

@ -0,0 +1,34 @@
mtllib cube.mtl
usemtl cubemtl
v -0.5 -0.5 -0.5
v -0.5 -0.5 0.5
v -0.5 0.5 -0.5
v -0.5 0.5 0.5
v 0.5 -0.5 -0.5
v 0.5 -0.5 0.5
v 0.5 0.5 -0.5
v 0.5 0.5 0.5
vt 0.0 0.0
vt 0.0 1.0
vt 1.0 0.0
vt 1.0 1.0
vn 1.0 0.0 0.0
vn 0.0 1.0 0.0
vn 0.0 0.0 1.0
vn -1.0 0.0 0.0
vn 0.0 -1.0 0.0
vn 0.0 0.0 -1.0
f 1/1/4 2/3/4 4/4/4 3/2/4
f 2/1/3 6/3/3 8/4/3 4/2/3
f 6/1/1 5/3/1 7/4/1 8/2/1
f 5/1/6 1/3/6 3/4/6 7/2/6
f 4/1/2 8/3/2 7/4/2 3/2/2
f 2/1/5 1/3/5 5/4/5 6/2/5
usemtl fuckyou

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -0,0 +1,22 @@
varying vec3 fNormal;
varying vec4 fFrontColor;
vec3 ambientLight = vec3(0.2,0.2,0.2);
vec3 directionnalLight = normalize(vec3(10,5,7));
vec3 directionnalLightFactor = vec3(0.5,0.5,0.5);
uniform sampler2D tex;
void main() {
vec3 ambientFactor = ambientLight;
vec3 lambertFactor = max(vec3(0.0,0.0,0.0), dot(directionnalLight, fNormal) * directionnalLightFactor);
vec4 noTexColor = vec4(ambientFactor + lambertFactor, 1.0);
vec4 color = texture2D(tex, gl_TexCoord[0].st);
vec4 fragColor = noTexColor * color;
gl_FragColor = fragColor * fFrontColor;
}

View File

@ -0,0 +1,12 @@
varying vec3 fNormal;
varying vec4 fTexCoord;
varying vec4 fFrontColor;
void main() {
fNormal = gl_Normal;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
fFrontColor = gl_Color;
}

View File

@ -0,0 +1,27 @@
from .geometry import Vector
import OpenGL.GLU as glu
class Camera:
"""Simple 3D camera
"""
def __init__(self, position = Vector(1.0,0.0,0.0), target = Vector(), up = Vector(0.0,1.0,0.0)):
"""Creates a simple camera
:param position: center of the camera
:param target: point where the camera is looking
:param up: up vector of the camera
"""
self.position = position
self.target = target
self.up = up
def look(self):
"""Sets the model view matrix of OpenGL
Simply calls the gluLookAt function
"""
glu.gluLookAt(
self.position.x, self.position.y, self.position.z,
self.target.x, self.target.y, self.target.z,
self.up.x, self.up.y, self.up.z)

View File

@ -0,0 +1,112 @@
from .geometry import Vector
import pygame
import OpenGL.GL as gl
import math
class Controls:
"""Abstract class for controls
"""
def __init__(self):
pass
def apply(self):
"""Apply the controls modification to the model view matrix
"""
pass
def update(self, time = 10):
"""Update according to the user's inputs
"""
pass
class TrackBallControls(Controls):
"""Trackball controls
Simple trackball controls"""
def __init__(self):
"""Creates a TrackBallControls
The trackball is centered at the origin
"""
super().__init__()
self.vertex = Vector()
self.theta = 0
def apply(self):
"""Apply the rotation of the current trackball
"""
gl.glRotatef(self.theta * 180 / math.pi, self.vertex.x, self.vertex.y, self.vertex.z)
def update(self, time = 10):
"""Checks the keyboard inputs and update the angle
"""
if not pygame.mouse.get_pressed()[0]:
return
coeff = 0.001
move = pygame.mouse.get_rel()
dV = Vector(move[1] * time * coeff, move[0] * time * coeff, 0)
dTheta = dV.norm2()
if abs(dTheta) < 0.00001:
return
dV.normalize()
cosT2 = math.cos(self.theta / 2)
sinT2 = math.sin(self.theta / 2)
cosDT2 = math.cos(dTheta / 2)
sinDT2 = math.sin(dTheta / 2)
A = cosT2 * sinDT2 * dV + cosDT2 * sinT2 * self.vertex + sinDT2 * sinT2 * Vector.cross_product(dV, self.vertex)
self.theta = 2 * math.acos(cosT2 * cosDT2 - sinT2 * sinDT2 * Vector.dot(dV, self.vertex))
self.vertex = A
self.vertex.normalize()
class OrbitControls(Controls):
"""Simple OrbitControls
Similar to TrackBallControls but the up vector is preserved"""
def __init__(self):
"""Creates an OrbitControls with null angles
"""
super().__init__()
self.phi = 0
self.theta = 0
self.scale_log = 0
def apply(self):
scale = math.exp(self.scale_log)
gl.glScalef(scale, scale, scale)
gl.glRotatef(self.theta * 180 / math.pi, 1.0, 0.0, 0.0)
gl.glRotatef(self.phi * 180 / math.pi, 0.0, 1.0, 0.0)
def apply_event(self, event):
"""Manages the wheel event
:param event: a pyevent
"""
if event.type == pygame.MOUSEBUTTONDOWN:
# Wheel up
if event.button == 4:
self.scale_log += 0.1
# Wheel down
elif event.button == 5:
self.scale_log -= 0.1
def update(self, time = 10):
if not pygame.mouse.get_pressed()[0]:
return
move = pygame.mouse.get_rel()
self.theta += move[1] * 0.01
self.phi += move[0] * 0.01
self.theta = max(min(self.theta, math.pi / 2), -math.pi / 2)

View File

@ -0,0 +1,108 @@
import math
class Vector:
""" 3D Vector
Simple class that represents a 3D vector
"""
def __init__(self, x = 0.0, y = 0.0, z = 0.0):
"""
Creates a vector from it's coordinates
"""
self.x = x
self.y = y
self.z = z
def from_array(self, arr):
"""
Creates a vector from an array
"""
self.x = float(arr[0]) if len(arr) > 0 else None
self.y = float(arr[1]) if len(arr) > 1 else None
self.z = float(arr[2]) if len(arr) > 2 else None
return self
def __add__(self, other):
"""
Sums two vectors
"""
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(self, other):
"""
Subs two vectors
"""
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(self, other):
"""
Computes the product between a vector and a number
"""
return Vector(self.x * other, self.y * other, self.z * other)
def __truediv__(self, number):
self.x /= number
self.y /= number
self.z /= number
return self
def __rmul__(self, other):
"""
Computes the product between a vector and a number
"""
return self.__mul__(other)
def norm2(self):
"""
Computes the square of the norm of a vector
"""
return self.x * self.x + self.y * self.y + self.z * self.z
def norm(self):
"""
Compute the norm of a vector
"""
return math.sqrt(self.norm2())
def normalize(self):
"""
Divides each coordinate of the vector by its norm
"""
norm = self.norm()
if abs(norm) > 0.0001:
self.x /= norm
self.y /= norm
self.z /= norm
def cross_product(v1, v2):
"""
Computes the cross product between the two vectors
"""
return Vector(
v1.y * v2.z - v1.z * v2.y,
v1.z * v2.x - v1.x * v2.z,
v1.x * v2.y - v1.y * v2.x)
def from_points(v1, v2):
"""
Creates a vector from two points
"""
return Vector(
v2.x - v1.x,
v2.y - v1.y,
v2.z - v1.z)
def __str__(self):
"""
Prints the coordinates of the vector between partheses
"""
return '(' + ", ".join([str(self.x), str(self.y), str(self.z)]) + ")"
def dot(self, other):
"""
Computes the dot product of two vectors
"""
return self.x * other.x + self.y * other.y + self.z * other.z

View File

@ -0,0 +1,334 @@
from math import sqrt
from ..geometry import Vector
from .mesh import Material, MeshPart
Vertex = Vector
TexCoord = Vertex
Normal = Vertex
Color = Vertex
class FaceVertex:
"""Contains the information a vertex needs in a face
In contains the index of the vertex, the index of the texture coordinate
and the index of the normal. It is None if it is not available.
:param vertex: index of the vertex
:param tex_coord: index of the texture coordinate
:param normal: index of the normal
:param color: index of the color
"""
def __init__(self, vertex = None, tex_coord = None, normal = None, color = None):
"""Initializes a FaceVertex from its indices
"""
self.vertex = vertex
self.tex_coord = tex_coord
self.normal = normal
self.color = color
def from_array(self, arr):
"""Initializes a FaceVertex from an array
:param arr: can be an array of strings, the first value will be the
vertex index, the second will be the texture coordinate index, the
third will be the normal index, and the fourth will be the color index.
"""
self.vertex = int(arr[0]) if len(arr) > 0 else None
try:
self.tex_coord = int(arr[1]) if len(arr) > 1 else None
except:
self.tex_coord = None
try:
self.normal = int(arr[2]) if len(arr) > 2 else None
except:
self.normal = None
try:
self.color = int(arr[3]) if len(arr) > 3 else None
except:
self.color = None
return self
class Face:
"""Represents a face with 3 vertices
Faces with more than 3 vertices are not supported in this class. You should
split your face first and then create the number needed of instances of
this class.
"""
def __init__(self, a = None, b = None, c = None, material = None):
"""Initializes a Face with its three FaceVertex and its Material
:param a: first FaceVertex element
:param b: second FaceVertex element
:param c: third FaceVertex element
:param material: the material to use with this face
"""
self.a = a
self.b = b
self.c = c
self.material = material
# Expects array of array
def from_array(self, arr):
"""Initializes a Face with an array
:param arr: should be an array of array of objects. Each array will
represent a FaceVertex
"""
self.a = FaceVertex().from_array(arr[0])
self.b = FaceVertex().from_array(arr[1])
self.c = FaceVertex().from_array(arr[2])
return self
class ModelParser:
"""Represents a 3D model
"""
def __init__(self, up_conversion = None):
"""Initializes the model
:param up_conversion: couple of characters, can be y z or z y
"""
self.up_conversion = up_conversion
self.vertices = []
self.colors = []
self.normals = []
self.tex_coords = []
self.parts = []
self.materials = []
self.current_part = None
self.path = None
def init_textures(self):
"""Initializes the textures of the parts of the model
Basically, calls glGenTexture on each texture
"""
for part in self.parts:
part.init_texture()
def add_vertex(self, vertex):
"""Adds a vertex to the current model
Will also update its bounding box, and convert the up vector if
up_conversion was specified.
:param vertex: vertex to add to the model
"""
# Apply up_conversion to the vertex
new_vertex = vertex
if self.up_conversion is not None:
if self.up_conversion[0] == 'y' and self.up_conversion[1] == 'z':
new_vertex = Vector(vertex.y, vertex.z, vertex.x)
elif self.up_conversion[0] == 'z' and self.up_conversion[1] == 'y':
new_vertex = Vector(vertex.z, vertex.x, vertex.y)
self.vertices.append(new_vertex)
def add_tex_coord(self, tex_coord):
"""Adds a texture coordinate element to the current model
:param tex_coord: tex_coord to add to the model
"""
self.tex_coords.append(tex_coord)
def add_normal(self, normal):
"""Adds a normal element to the current model
:param normal: normal to add to the model
"""
self.normals.append(normal)
def add_color(self, color):
"""Adds a color element to the current model
:param color: color to add to the model
"""
self.colors.append(color)
def add_face(self, face):
"""Adds a face to the current model
If the face has a different material than the current material, it will
create a new mesh part and update the current material.
:param face: face to add to the model
"""
if self.current_part is None or (face.material != self.current_part.material and face.material is not None):
self.current_part = MeshPart(self)
self.current_part.material = face.material if face.material is not None else Material.DEFAULT_MATERIAL
self.parts.append(self.current_part)
self.current_part.add_face(face)
def parse_file(self, path, chunk_size = 512):
"""Sets the path of the model and parse bytes by chunk
:param path: path to the file to parse
:param chunk_size: the file will be read chunk by chunk, each chunk
having chunk_size bytes
"""
self.path = path
byte_counter = 0
with open(path, 'rb') as f:
while True:
bytes = f.read(chunk_size)
if bytes == b'':
return
self.parse_bytes(bytes, byte_counter)
byte_counter += chunk_size
def draw(self):
"""Draws each part of the model with OpenGL
"""
import OpenGL.GL as gl
for part in self.parts:
part.draw()
def generate_vbos(self):
"""Generates the VBOs of each part of the model
"""
for part in self.parts:
part.generate_vbos()
def generate_vertex_normals(self):
"""Generate the normals for each vertex of the model
A normal will be the average normal of the adjacent faces of a vertex.
"""
self.normals = [Normal() for i in self.vertices]
for part in self.parts:
for face in part.faces:
v1 = Vertex.from_points(self.vertices[face.a.vertex], self.vertices[face.b.vertex])
v2 = Vertex.from_points(self.vertices[face.a.vertex], self.vertices[face.c.vertex])
v1.normalize()
v2.normalize()
cross = Vertex.cross_product(v1, v2)
self.normals[face.a.vertex] += cross
self.normals[face.b.vertex] += cross
self.normals[face.c.vertex] += cross
for normal in self.normals:
normal.normalize()
for part in self.parts:
for face in part.faces:
face.a.normal = face.a.vertex
face.b.normal = face.b.vertex
face.c.normal = face.c.vertex
def generate_face_normals(self):
"""Generate the normals for each face of the model
A normal will be the normal of the face
"""
# Build array of faces
faces = sum(map(lambda x: x.faces, self.parts), [])
self.normals = [Normal()] * len(faces)
for (index, face) in enumerate(faces):
v1 = Vertex.from_points(self.vertices[face.a.vertex], self.vertices[face.b.vertex])
v2 = Vertex.from_points(self.vertices[face.a.vertex], self.vertices[face.c.vertex])
cross = Vertex.cross_product(v1, v2)
cross.normalize()
self.normals[index] = cross
face.a.normal = index
face.b.normal = index
face.c.normal = index
def get_material_index(self, material):
"""Finds the index of the given material
:param material: Material you want the index of
"""
return [i for (i,m) in enumerate(self.materials) if m.name == material.name][0]
class TextModelParser(ModelParser):
def parse_file(self, path):
"""Sets the path of the model and parse each line
:param path: path to the text file to parse
"""
self.path = path
with open(path) as f:
for line in f.readlines():
line = line.rstrip()
if line != '':
self.parse_line(line)
class BoundingBox:
"""Represents a bounding box of a 3D model
"""
def __init__(self):
"""Initializes the coordinates of the bounding box
"""
self.min_x = +float('inf')
self.min_y = +float('inf')
self.min_z = +float('inf')
self.max_x = -float('inf')
self.max_y = -float('inf')
self.max_z = -float('inf')
def add(self, vector):
"""Adds a vector to a bounding box
If the vector is outside the bounding box, the bounding box will be
enlarged, otherwise, nothing will happen.
:param vector: the vector that will enlarge the bounding box
"""
self.min_x = min(self.min_x, vector.x)
self.min_y = min(self.min_y, vector.y)
self.min_z = min(self.min_z, vector.z)
self.max_x = max(self.max_x, vector.x)
self.max_y = max(self.max_y, vector.y)
self.max_z = max(self.max_z, vector.z)
def __str__(self):
"""Returns a string that represents the bounding box
"""
return "[{},{}],[{},{}],[{},{}]".format(
self.min_x,
self.min_y,
self.min_z,
self.max_x,
self.max_y,
self.max_z)
def get_center(self):
"""Returns the center of the bounding box
"""
return Vertex(
(self.min_x + self.max_x) / 2,
(self.min_y + self.max_y) / 2,
(self.min_z + self.max_z) / 2)
def get_scale(self):
"""Returns the maximum edge of the bounding box
"""
return max(
abs(self.max_x - self.min_x),
abs(self.max_y - self.min_y),
abs(self.max_z - self.min_z))
class Exporter:
"""Represents an object that can export a model into a certain format
"""
def __init__(self, model):
"""Creates a exporter for the model
:param model: model to export
"""
self.model = model

View File

@ -0,0 +1,4 @@
from os.path import dirname, basename, isfile
import glob
modules = glob.glob(dirname(__file__)+"/*.py")
__all__ = [ basename(f)[:-3] for f in modules if isfile(f)]

View File

@ -0,0 +1,208 @@
from ..basemodel import TextModelParser, Exporter, Vertex, TexCoord, Normal, FaceVertex, Face
from ..mesh import Material, MeshPart
from functools import reduce
import os.path
import sys
def is_obj(filename):
"""Checks that the file is a .obj file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.obj'
class OBJParser(TextModelParser):
"""Parser that parses a .obj file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.current_material = None
self.mtl = None
self.vertex_offset = 0
def parse_line(self, string):
"""Parses a line of .obj file
:param string: the line to parse
"""
if string == '':
return
split = string.split()
first = split[0]
split = split[1:]
if first == 'usemtl' and self.mtl is not None:
self.current_material = self.mtl[split[0]]
elif first == 'mtllib':
path = os.path.join(os.path.dirname(self.path), ' '.join(split[:]))
if os.path.isfile(path):
self.mtl = MTLParser(self)
self.mtl.parse_file(path)
else:
print('Warning : ' + path + ' not found ', file=sys.stderr)
elif first == 'v':
self.add_vertex(Vertex().from_array(split))
elif first == 'vn':
self.add_normal(Normal().from_array(split))
elif first == 'vt':
self.add_tex_coord(TexCoord().from_array(split))
elif first == 'f':
splits = list(map(lambda x: x.split('/'), split))
for i in range(len(splits)):
for j in range(len(splits[i])):
if splits[i][j] != '':
splits[i][j] = int(splits[i][j])
if splits[i][j] > 0:
splits[i][j] -= 1
else:
splits[i][j] = len(self.vertices) + splits[i][j]
# if Face3
if len(split) == 3:
face = Face().from_array(splits)
face.material = self.current_material
self.add_face(face)
# Face4 are well supported with the next stuff
# elif len(split) == 4:
# face = Face().from_array(splits[:3])
# face.material = self.current_material
# self.add_face(face)
# face = Face().from_array([splits[0], splits[2], splits[3]])
# face.material = self.current_material
# self.add_face(face)
else:
# Bweeee
# First, lets compute all the FaceVertex for each vertex
face_vertices = []
for face_vertex in splits[:]:
face_vertices.append(FaceVertex(*face_vertex))
# Then, we build the faces 0 i i+1 for each 1 <= i < len - 1
for i in range(1, len(face_vertices) - 1):
# Create face with barycenter, i and i + 1
face = Face(face_vertices[0], face_vertices[i], face_vertices[i+1])
face.material = self.current_material
self.add_face(face)
class MTLParser:
"""Parser that parses a .mtl material file
"""
def __init__(self, parent):
"""Creates a MTLParser bound to the OBJParser
:param parent: the OBJParser this MTLParser refers to
"""
self.parent = parent
self.current_mtl = None
def parse_line(self, string):
"""Parses a line of .mtl file
:param string: line to parse
"""
if string == '':
return
split = string.split()
first = split[0]
split = split[1:]
if first == 'newmtl':
self.current_mtl = Material(' '.join(split[:]))
self.parent.materials.append(self.current_mtl)
elif first == 'Ka':
self.current_mtl.Ka = Vertex().from_array(split)
elif first == 'Kd':
self.current_mtl.Kd = Vertex().from_array(split)
elif first == 'Ks':
self.current_mtl.Ks = Vertex().from_array(split)
elif first == 'map_Kd':
self.current_mtl.relative_path_to_texture = ' '.join(split)
self.current_mtl.absolute_path_to_texture = os.path.join(os.path.dirname(self.parent.path), ' '.join(split))
def parse_file(self, path):
with open(path) as f:
for line in f.readlines():
line = line.rstrip()
self.parse_line(line)
def __getitem__(self, key):
for material in self.parent.materials:
if material.name == key:
return material
class OBJExporter(Exporter):
"""Exporter to .obj format
"""
def __init__(self, model):
"""Creates an exporter from the model
:param model: Model to export
"""
super().__init__(model)
def __str__(self):
"""Exports the model
"""
current_material = ''
string = ""
for vertex in self.model.vertices:
string += "v " + ' '.join([str(vertex.x), str(vertex.y), str(vertex.z)]) + "\n"
string += "\n"
if len(self.model.tex_coords) > 0:
for tex_coord in self.model.tex_coords:
string += "vt " + ' '.join([str(tex_coord.x), str(tex_coord.y)]) + "\n"
string += "\n"
if len(self.model.normals) > 0:
for normal in self.model.normals:
string += "vn " + ' '.join([str(normal.x), str(normal.y), str(normal.z)]) + "\n"
string += "\n"
faces = sum(map(lambda x: x.faces, self.model.parts), [])
for face in faces:
if face.material is not None and face.material.name != current_material:
current_material = face.material.name
string += "usemtl " + current_material + "\n"
string += "f "
arr = []
for v in [face.a, face.b, face.c]:
sub_arr = []
sub_arr.append(str(v.vertex + 1))
if v.normal is None:
if v.tex_coord is not None:
sub_arr.append('')
sub_arr.append(str(v.tex_coord + 1))
elif v.tex_coord is not None:
sub_arr.append(str(v.tex_coord + 1))
if v.normal is not None:
sub_arr.append(str(v.normal + 1))
arr.append('/'.join(sub_arr))
string += ' '.join(arr) + '\n'
return string

View File

@ -0,0 +1,65 @@
from ..basemodel import TextModelParser, Exporter, Vertex, TexCoord, Normal, FaceVertex, Face
from ..mesh import Material, MeshPart
def is_off(filename):
"""Checks that the file is a .off file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.off'
class OFFParser(TextModelParser):
"""Parser that parses a .off file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.vertex_number = None
self.face_number = None
self.edge_number = None
def parse_line(self, string):
"""Parses a line of .off file
:param string: the line to parse
"""
split = string.split()
if string == '' or string == 'OFF':
pass
elif self.vertex_number is None:
# The first will be the header
self.vertex_number = int(split[0])
self.face_number = int(split[1])
self.edge_number = int(split[2])
elif len(self.vertices) < self.vertex_number:
self.add_vertex(Vertex().from_array(split))
else:
self.add_face(Face(FaceVertex(int(split[1])), FaceVertex(int(split[2])), FaceVertex(int(split[3]))))
class OFFExporter(Exporter):
"""Exporter to .off format
"""
def __init__(self, model):
"""Creates an exporter from the model
:param model: Model to export
"""
super().__init__(model)
def __str__(self):
"""Exports the model
"""
faces = sum(map(lambda x: x.faces, self.model.parts), [])
string = "OFF\n{} {} {}".format(len(self.model.vertices), len(faces), 0) + '\n'
for vertex in self.model.vertices:
string += ' '.join([str(vertex.x), str(vertex.y), str(vertex.z)]) + '\n'
for face in faces:
string += '3 ' + ' '.join([str(face.a.vertex), str(face.b.vertex), str(face.c.vertex)]) + '\n'
return string

View File

@ -0,0 +1,494 @@
import os
import sys
import struct
from ..basemodel import ModelParser, TextModelParser, Exporter, Vertex, Face, Color, FaceVertex, TexCoord, Material
class UnkownTypeError(Exception):
def __init__(self, message):
self.message = message
def is_ply(filename):
"""Checks that the file is a .ply file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.ply'
# List won't work with this function
def _ply_type_size(type):
"""Returns the size of a ply property
:param type: a string that is in a ply element
"""
if type == 'char' or type == 'uchar':
return 1
elif type == 'short' or type == 'ushort':
return 2
elif type == 'int' or type == 'uint':
return 4
elif type == 'float':
return 4
elif type == 'double':
return 8
else:
raise UnkownTypeError('Type ' + type + ' is unknown')
def ply_type_size(type):
"""Returns the list containing the sizes of the elements
:param type: a string that is in a ply element
"""
split = type.split()
if len(split) == 1:
return [_ply_type_size(type)]
else:
if split[0] != 'list':
print('You have multiple types but it\'s not a list...', file=sys.stderr)
sys.exit(-1)
else:
return list(map(lambda a: _ply_type_size(a), split[1:]))
def bytes_to_element(type, bytes, byteorder = 'little'):
"""Returns a python object parsed from bytes
:param type: the type of the object to parse
:param bytes: the bytes to read
:param byteorder: little or big endian
"""
if type == 'char':
return ord(struct.unpack('<b', bytes)[0])
if type == 'uchar':
return ord(struct.unpack('<c', bytes)[0])
elif type == 'short':
return struct.unpack('<h', bytes)[0]
elif type == 'ushort':
return struct.unpack('<H', bytes)[0]
elif type == 'int':
return struct.unpack('<i', bytes)[0]
elif type == 'uint':
return struct.unpack('<I', bytes)[0]
elif type == 'float':
return struct.unpack('<f', bytes)[0]
elif type == 'double':
return struct.unpack('<d', bytes)[0]
else:
raise UnkownTypeError('Type ' + type + ' is unknown')
class PLYParser(ModelParser):
"""Parser that parses a .ply file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.counter = 0
self.elements = []
self.inner_parser = PLYHeaderParser(self)
self.beginning_of_line = ''
self.header_finished = False
def parse_bytes(self, bytes, byte_counter):
"""Parses bytes of a .ply file
"""
if self.header_finished:
self.inner_parser.parse_bytes(self.beginning_of_line + bytes, byte_counter - len(self.beginning_of_line))
self.beginning_of_line = b''
return
# Build lines for header and use PLYHeaderParser
current_line = self.beginning_of_line
for (i, c) in enumerate(bytes):
char = chr(c)
if char == '\n':
self.inner_parser.parse_line(current_line)
if current_line == 'end_header':
self.header_finished = True
self.beginning_of_line = bytes[i+1:]
return
current_line = ''
else:
current_line += chr(c)
self.beginning_of_line = current_line
class PLYHeaderParser:
"""Parser that parses the header of a .ply file
"""
def __init__(self, parent):
self.current_element = None
self.parent = parent
self.content_parser = None
def parse_line(self, string):
split = string.split()
if string == 'ply':
return
elif split[0] == 'format':
if split[2] != '1.0':
print('Only format 1.0 is supported', file=sys.stderr)
sys.exit(-1)
if split[1] == 'ascii':
self.content_parser = PLY_ASCII_ContentParser(self.parent)
elif split[1] == 'binary_little_endian':
self.content_parser = PLYLittleEndianContentParser(self.parent)
elif split[1] == 'binary_big_endian':
self.content_parser = PLYBigEndianContentParser(self.parent)
else:
print('Only ascii, binary_little_endian and binary_big_endian are supported', \
file=sys.stderr)
sys.exit(-1)
elif split[0] == 'element':
self.current_element = PLYElement(split[1], int(split[2]))
self.parent.elements.append(self.current_element)
elif split[0] == 'property':
self.current_element.add_property(split[-1], ' '.join(split[1:-1]))
elif split[0] == 'end_header':
self.parent.inner_parser = self.content_parser
elif split[0] == 'comment' and split[1] == 'TextureFile':
material = Material('mat' + str(len(self.parent.materials)))
self.parent.materials.append(material)
material.relative_path_to_texture = split[2]
material.absolute_path_to_texture = os.path.join(os.path.dirname(self.parent.path), split[2])
class PLYElement:
def __init__(self, name, number):
self.name = name
self.number = number
self.properties = []
def add_property(self, name, type):
self.properties.append((name, type))
class PLY_ASCII_ContentParser:
def __init__(self, parent):
self.parent = parent
self.element_index = 0
self.counter = 0
self.current_element = None
self.beginning_of_line = ''
def parse_bytes(self, bytes, byte_counter):
current_line = self.beginning_of_line
for (i, c) in enumerate(bytes):
char = chr(c)
if char == '\n':
self.parse_line(current_line)
current_line = ''
else:
current_line += chr(c)
self.beginning_of_line = current_line
def parse_line(self, string):
if string == '':
return
if self.current_element is None:
self.current_element = self.parent.elements[0]
split = string.split()
color = None
if self.current_element.name == 'vertex':
vertex = Vertex()
red = None
blue = None
green = None
alpha = None
offset = 0
for property in self.current_element.properties:
if property[0] == 'x':
vertex.x = float(split[offset])
elif property[0] == 'y':
vertex.y = float(split[offset])
elif property[0] == 'z':
vertex.z = float(split[offset])
elif property[0] == 'red':
red = float(split[offset]) / 255
elif property[0] == 'green':
green = float(split[offset]) / 255
elif property[0] == 'blue':
blue = float(split[offset]) / 255
elif property[0] == 'alpha':
alpha = float(split[offset]) / 255
offset += 1
self.parent.add_vertex(vertex)
if red is not None:
color = Color(red, blue, green)
self.parent.add_color(color)
elif self.current_element.name == 'face':
faceVertexArray = []
current_material = None
# Analyse element
offset = 0
for property in self.current_element.properties:
if property[0] == 'vertex_indices':
for i in range(int(split[offset])):
faceVertexArray.append(FaceVertex(int(split[i+offset+1])))
offset += int(split[0]) + 1
elif property[0] == 'texcoord':
offset += 1
for i in range(3):
# Create corresponding tex_coords
tex_coord = TexCoord().from_array(split[offset:offset+2])
offset += 2
self.parent.add_tex_coord(tex_coord)
faceVertexArray[i].tex_coord = len(self.parent.tex_coords) - 1
elif property[0] == 'texnumber':
current_material = self.parent.materials[int(split[offset])]
offset += 1
face = Face(*faceVertexArray)
face.material = current_material
self.parent.add_face(face)
self.counter += 1
if self.counter == self.current_element.number:
self.next_element()
def next_element(self):
self.element_index += 1
if self.element_index < len(self.parent.elements):
self.current_element = self.parent.elements[self.element_index]
class PLYLittleEndianContentParser:
def __init__(self, parent):
self.parent = parent
self.previous_bytes = b''
self.element_index = 0
self.counter = 0
self.current_element = None
self.started = False
# Serves for debugging purposes
# self.current_byte = 0
def parse_bytes(self, bytes, byte_counter):
if not self.started:
# self.current_byte = byte_counter
self.started = True
if self.current_element is None:
self.current_element = self.parent.elements[0]
bytes = self.previous_bytes + bytes
current_byte_index = 0
while True:
property_values = []
beginning_byte_index = current_byte_index
for property in self.current_element.properties:
size = ply_type_size(property[1])
if current_byte_index + size[0] > len(bytes):
self.previous_bytes = bytes[beginning_byte_index:]
# self.current_byte -= len(self.previous_bytes)
return
if len(size) == 1:
size = size[0]
current_property_bytes = bytes[current_byte_index:current_byte_index+size]
property_values.append(bytes_to_element(property[1], current_property_bytes))
current_byte_index += size
# self.current_byte += size
elif len(size) == 2:
types = property[1].split()[1:]
current_property_bytes = bytes[current_byte_index:current_byte_index+size[0]]
number_of_elements = bytes_to_element(types[0], current_property_bytes)
current_byte_index += size[0]
# self.current_byte += size[0]
property_values.append([])
# Parse list
for i in range(number_of_elements):
if current_byte_index + size[1] > len(bytes):
self.previous_bytes = bytes[beginning_byte_index:]
# self.current_byte -= len(self.previous_bytes)
return
current_property_bytes = bytes[current_byte_index:current_byte_index+size[1]]
property_values[-1].append(bytes_to_element(types[1], current_property_bytes))
current_byte_index += size[1]
# self.current_byte += size[1]
else:
print('I have not idea what this means', file=sys.stderr)
# Add element
if self.current_element.name == 'vertex':
vertex = Vertex()
red = None
green = None
blue = None
alpha = None
offset = 0
for property in self.current_element.properties:
if property[0] == 'x':
vertex.x = property_values[offset]
elif property[0] == 'y':
vertex.y = property_values[offset]
elif property[0] == 'z':
vertex.z = property_values[offset]
elif property[0] == 'red':
red = property_values[offset] / 255
elif property[0] == 'green':
green = property_values[offset] / 255
elif property[0] == 'blue':
blue = property_values[offset] / 255
elif property[0] == 'alpha':
alpha = property_values[offset] / 255
offset += 1
self.parent.add_vertex(vertex)
if red is not None:
self.parent.add_color(Color(red, blue, green))
elif self.current_element.name == 'face':
vertex_indices = []
tex_coords = []
material = None
for (i, property) in enumerate(self.current_element.properties):
if property[0] == 'vertex_indices':
vertex_indices.append(property_values[i][0])
vertex_indices.append(property_values[i][1])
vertex_indices.append(property_values[i][2])
elif property[0] == 'texcoord':
# Create texture coords
for j in range(0,6,2):
tex_coord = TexCoord(*property_values[i][j:j+2])
tex_coords.append(tex_coord)
elif property[0] == 'texnumber':
material = self.parent.materials[property_values[i]]
for tex_coord in tex_coords:
self.parent.add_tex_coord(tex_coord)
face = Face(*list(map(lambda x: FaceVertex(x), vertex_indices)))
counter = 3
if len(tex_coords) > 0:
for face_vertex in [face.a, face.b, face.c]:
face_vertex.tex_coord = len(self.parent.tex_coords) - counter
counter -= 1
if material is None and len(self.parent.materials) == 1:
material = self.parent.materials[0]
face.material = material
self.parent.add_face(face)
self.counter += 1
if self.counter == self.current_element.number:
self.next_element()
def next_element(self):
self.counter = 0
self.element_index += 1
if self.element_index < len(self.parent.elements):
self.current_element = self.parent.elements[self.element_index]
class PLYBigEndianContentParser(PLYLittleEndianContentParser):
def __init__(self, parent):
super().__init__(self, parent)
def parse_bytes(self, bytes):
# Reverse bytes, and then
super().parse_bytes(self, bytes)
class PLYExporter(Exporter):
def __init__(self, model):
super().__init__(model)
def __str__(self):
faces = sum([part.faces for part in self.model.parts], [])
# Header
string = "ply\nformat ascii 1.0\ncomment Automatically gnerated by model-converter\n"
for material in self.model.materials:
string += "comment TextureFile " + (material.relative_path_to_texture or 'None') + "\n"
# Types : vertices
string += "element vertex " + str(len(self.model.vertices)) +"\n"
string += "property float x\nproperty float y\nproperty float z\n"
# Types : faces
string += "element face " + str(len(faces)) + "\n"
string += "property list uchar int vertex_indices\n"
if len(self.model.tex_coords) > 0:
string += "property list uchar float texcoord\n"
string += "property int texnumber\n"
# End header
string += "end_header\n"
# Content of the model
for vertex in self.model.vertices:
string += str(vertex.x) + " " + str(vertex.y) + " " + str(vertex.z) + "\n"
for face in faces:
string += "3 " + str(face.a.vertex) + " " + str(face.b.vertex) + " " + str(face.c.vertex)
if len(self.model.tex_coords) > 0:
string += " 6 " \
+ str(self.model.tex_coords[face.a.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.a.tex_coord].y) + " " \
+ str(self.model.tex_coords[face.b.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.b.tex_coord].y) + " " \
+ str(self.model.tex_coords[face.c.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.c.tex_coord].y) + " " \
+ str(self.model.get_material_index(face.material))
string += "\n"
return string

View File

@ -0,0 +1,116 @@
from ..basemodel import TextModelParser, Exporter, Vertex, FaceVertex, Face
from ..mesh import MeshPart
import os.path
def is_stl(filename):
"""Checks that the file is a .stl file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.stl'
class STLParser(TextModelParser):
"""Parser that parses a .stl file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.parsing_solid = False
self.parsing_face = False
self.parsing_loop = False
self.current_face = None
self.face_vertices = None
def parse_line(self, string):
"""Parses a line of .stl file
:param string: the line to parse
"""
if string == '':
return
split = string.split()
if split[0] == 'solid':
self.parsing_solid = True
return
if split[0] == 'endsolid':
self.parsing_solid = False
return
if self.parsing_solid:
if split[0] == 'facet' and split[1] == 'normal':
self.parsing_face = True
self.face_vertices = [FaceVertex(), FaceVertex(), FaceVertex()]
self.current_face = Face(*self.face_vertices)
return
if self.parsing_face:
if split[0] == 'outer' and split[1] == 'loop':
self.parsing_loop = True
return
if split[0] == 'endloop':
self.parsing_loop = False
return
if self.parsing_loop:
if split[0] == 'vertex':
current_vertex = Vertex().from_array(split[1:])
self.add_vertex(current_vertex)
self.face_vertices[0].vertex = len(self.vertices) - 1
self.face_vertices.pop(0)
return
if split[0] == 'endfacet':
self.parsing_face = False
self.add_face(self.current_face)
self.current_face = None
self.face_vertices = None
class STLExporter(Exporter):
"""Exporter to .stl format
"""
def __init__(self, model):
"""Creates an exporter from the model
:param model: Model to export
"""
super().__init__(model)
super().__init__(model)
def __str__(self):
"""Exports the model
"""
string = 'solid {}\n'.format(os.path.basename(self.model.path[:-4]))
self.model.generate_face_normals()
faces = sum(map(lambda x: x.faces, self.model.parts), [])
for face in faces:
n = self.model.normals[face.a.normal]
v1 = self.model.vertices[face.a.vertex]
v2 = self.model.vertices[face.b.vertex]
v3 = self.model.vertices[face.c.vertex]
string += "facet normal {} {} {}\n".format(n.x, n.y, n.z)
string += "\touter loop\n"
string += "\t\tvertex {} {} {}\n".format(v1.x, v1.y, v1.z)
string += "\t\tvertex {} {} {}\n".format(v2.x, v2.y, v2.z)
string += "\t\tvertex {} {} {}\n".format(v3.x, v3.y, v3.z)
string += "\tendloop\n"
string += "endfacet\n"
string += 'endsolid {}'.format(os.path.basename(self.model.path[:-4]))
return string

View File

@ -0,0 +1,230 @@
class Material:
"""Represents a material
It contains its constants and its texturess. It is also usable with OpenGL
"""
def __init__(self, name):
""" Creates an empty material
:param name: name of the material:
"""
self.name = name
self.Ka = None
self.Kd = None
self.Ks = None
self.relative_path_to_texture = None
self.absolute_path_to_texture = None
self.im = None
self.id = None
def init_texture(self):
""" Initializes the OpenGL texture of the current material
To be simple, calls glGenTextures and stores the given id
"""
import OpenGL.GL as gl
# Already initialized
if self.id is not None:
return
# If no map_Kd, nothing to do
if self.im is None:
if self.absolute_path_to_texture is None:
return
try:
import PIL.Image
self.im = PIL.Image.open(self.absolute_path_to_texture)
except ImportError:
return
try:
ix, iy, image = self.im.size[0], self.im.size[1], self.im.tobytes("raw", "RGBA", 0, -1)
except:
ix, iy, image = self.im.size[0], self.im.size[1], self.im.tobytes("raw", "RGBX", 0, -1)
self.id = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.id)
gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT,1)
gl.glTexImage2D(
gl.GL_TEXTURE_2D, 0, 3, ix, iy, 0,
gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, image
)
def bind(self):
"""Binds the material to OpenGL
"""
from OpenGL import GL as gl
gl.glEnable(gl.GL_TEXTURE_2D)
gl.glTexParameterf(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
gl.glTexParameterf(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.id)
def unbind(self):
"""Disables the GL_TEXTURE_2D flag of OpenGL
"""
from OpenGL import GL as gl
gl.glDisable(gl.GL_TEXTURE_2D)
Material.DEFAULT_MATERIAL=Material('')
"""Material that is used when no material is specified
"""
Material.DEFAULT_MATERIAL.Ka = 1.0
Material.DEFAULT_MATERIAL.Kd = 0.0
Material.DEFAULT_MATERIAL.Ks = 0.0
try:
import PIL.Image
Material.DEFAULT_MATERIAL.im = PIL.Image.new("RGBA", (1,1), "white")
except ImportError:
pass
class MeshPart:
"""A part of a 3D model that is bound to a single material
"""
def __init__(self, parent):
"""Creates a mesh part
:param parent: the global model with all the information
"""
self.parent = parent
self.material = None
self.vertex_vbo = None
self.tex_coord_vbo = None
self.normal_vbo = None
self.color_vbo = None
self.faces = []
def init_texture(self):
"""Initializes the material of the current parent
"""
if self.material is not None:
self.material.init_texture()
def add_face(self, face):
"""Adds a face to this MeshPart
:param face: face to add
"""
self.faces.append(face)
def generate_vbos(self):
"""Generates the vbo for this MeshPart
Creates the arrays that are necessary for smooth rendering
"""
from OpenGL.arrays import vbo
from numpy import array
# Build VBO
v = []
n = []
t = []
c = []
for face in self.faces:
v1 = self.parent.vertices[face.a.vertex]
v2 = self.parent.vertices[face.b.vertex]
v3 = self.parent.vertices[face.c.vertex]
v += [[v1.x, v1.y, v1.z], [v2.x, v2.y, v2.z], [v3.x, v3.y, v3.z]]
if face.a.normal is not None:
n1 = self.parent.normals[face.a.normal]
n2 = self.parent.normals[face.b.normal]
n3 = self.parent.normals[face.c.normal]
n += [[n1.x, n1.y, n1.z], [n2.x, n2.y, n2.z], [n3.x, n3.y, n3.z]]
if face.a.tex_coord is not None:
t1 = self.parent.tex_coords[face.a.tex_coord]
t2 = self.parent.tex_coords[face.b.tex_coord]
t3 = self.parent.tex_coords[face.c.tex_coord]
t += [[t1.x, t1.y], [t2.x, t2.y], [t3.x, t3.y]]
if len(self.parent.colors) > 0: # face.a.color is not None:
c1 = self.parent.colors[face.a.vertex]
c2 = self.parent.colors[face.b.vertex]
c3 = self.parent.colors[face.c.vertex]
c += [[c1.x, c1.y, c1.z], [c2.x, c2.y, c2.z], [c3.x, c3.y, c3.z]]
self.vertex_vbo = vbo.VBO(array(v, 'f'))
if len(n) > 0:
self.normal_vbo = vbo.VBO(array(n, 'f'))
if len(t) > 0:
self.tex_coord_vbo = vbo.VBO(array(t, 'f'))
if len(c) > 0:
self.color_vbo = vbo.VBO(array(c, 'f'))
def draw(self):
"""Draws the current MeshPart
Binds the material, and draws the model
"""
if self.material is not None:
self.material.bind()
if self.vertex_vbo is not None:
self.draw_from_vbos()
else:
self.draw_from_arrays()
if self.material is not None:
self.material.unbind()
def draw_from_vbos(self):
"""Simply calls the OpenGL drawArrays function
Sets the correct vertex arrays and draws the part
"""
import OpenGL.GL as gl
self.vertex_vbo.bind()
gl.glEnableClientState(gl.GL_VERTEX_ARRAY);
gl.glVertexPointerf(self.vertex_vbo)
self.vertex_vbo.unbind()
if self.normal_vbo is not None:
self.normal_vbo.bind()
gl.glEnableClientState(gl.GL_NORMAL_ARRAY)
gl.glNormalPointerf(self.normal_vbo)
self.normal_vbo.unbind()
if self.tex_coord_vbo is not None:
if self.material is not None:
self.material.bind()
self.tex_coord_vbo.bind()
gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
gl.glTexCoordPointerf(self.tex_coord_vbo)
self.tex_coord_vbo.unbind()
if self.color_vbo is not None:
self.color_vbo.bind()
gl.glEnableClientState(gl.GL_COLOR_ARRAY)
gl.glColorPointerf(self.color_vbo)
self.color_vbo.unbind()
gl.glDrawArrays(gl.GL_TRIANGLES, 0, len(self.vertex_vbo.data) * 9)
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
gl.glDisableClientState(gl.GL_NORMAL_ARRAY)
gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
gl.glDisableClientState(gl.GL_COLOR_ARRAY)
def draw_from_arrays(self):
pass

View File

@ -0,0 +1,99 @@
import os
from importlib import import_module
from . import formats
from .formats import *
from .basemodel import ModelParser, Exporter
from types import ModuleType
supported_formats = []
class ModelType:
"""Represents a type of coding of 3D object, and the module enabling
parsing and exporting
"""
def __init__(self, typename, inner_module):
"""Creates a ModelType
:param typename: the name of the 3D format
:param inner_module: the module that will parse and export the format
"""
self.typename = typename
self.inner_module = inner_module
def test_type(self, file):
"""Tests if a file has the correct type
:param file: path to the file to test
"""
return getattr(self.inner_module, 'is_' + self.typename)(file)
def create_parser(self, *args, **kwargs):
"""Creates a parser of the current type
"""
return getattr(self.inner_module, self.typename.upper() + 'Parser')(*args, **kwargs)
def create_exporter(self, *args, **kwargs):
"""Creates an exporter of the current type
"""
return getattr(self.inner_module, self.typename.upper() + 'Exporter')(*args, **kwargs)
def find_type(filename, supported_formats):
"""Find the correct type from a filename
:param filename: path to the file
:param supported_formats: list of formats that we have modules for
"""
for type in supported_formats:
if type.test_type(filename):
return type
for name in formats.__dict__:
if isinstance(formats.__dict__[name], ModuleType) and name != 'glob':
type = ModelType(name, formats.__dict__[name])
supported_formats.append(type)
def load_model(path, up_conversion = None):
"""Loads a model from a path
:param path: path to the file to load
:param up_conversion: conversion of up vectors
"""
parser = None
type = find_type(path, supported_formats)
if type is None:
raise Exception("File format not supported \"" + str(type) + "\"")
parser = type.create_parser(up_conversion)
parser.parse_file(path)
return parser
def export_model(model, path):
"""Exports a model to a path
:param model: model to export
:param path: path to save the model
"""
exporter = None
type = find_type(path, supported_formats)
if type is None:
raise Exception('File format is not supported')
exporter = type.create_exporter(model)
return exporter
def convert(input, output, up_conversion = None):
"""Converts a model
:param input: path of the input model
:param output: path to the output
:param up_conversion: convert the up vector
"""
model = load_model(input, up_conversion)
exporter = export_model(model, output)
return str(exporter)

View File

@ -0,0 +1,53 @@
import os
dir_path = os.path.dirname(os.path.realpath(__file__))
import OpenGL.GL as gl
import OpenGL.GL.shaders as sh
default_vertex_path = dir_path + '/../assets/shaders/shader.vert'
default_fragment_path = dir_path + '/../assets/shaders/shader.frag'
class Shader:
"""Shader
Loads, compile and binds the shader that are in the assets/shaders
directory
"""
def __init__(self, vertex_path = default_vertex_path, fragment_path = default_fragment_path):
"""Creates a shader object, and compile it
:param vertex_path: path to the vertex shader
:param fragment_path: path to the fragment shader
"""
with open(vertex_path) as f:
self.vertex_src = f.read()
with open(fragment_path) as f:
self.fragment_src = f.read()
self.compile_shaders()
self.compile_program()
def compile_shaders(self):
""" Compiles the shader
"""
self.vertex_shader = sh.compileShader(self.vertex_src, gl.GL_VERTEX_SHADER)
self.fragment_shader = sh.compileShader(self.fragment_src, gl.GL_FRAGMENT_SHADER)
def compile_program(self):
"""Compile the shader program
The shaders must be compiled
"""
self.program = sh.compileProgram(self.vertex_shader, self.fragment_shader)
def bind(self):
"""Bind the current shader to the OpenGL context
"""
gl.glUseProgram(self.program)
def unbind(self):
"""Reset OpenGL shader to 0
"""
gl.glUseProgram(0)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Papercraft Unfold</name>
<id>fablabchemnitz.de.papercraft_unfold</id>
<param name="tab" type="notebook">
<page name="general" gui-text="Input / General">
<param name="inputfile" type="path" gui-text="Input File" gui-description="The model to unfold" filetypes="amf,gcode,jscad,js,json,obj,off,ply,scad,stl"
mode="file">/your/beautiful/3dmodel/file</param>
<param name="resizetoimport" type="bool" gui-text="Resize the canvas to the imported drawing's bounding box">true</param>
<param name="extraborder" type="float" precision="3" gui-text="Add extra border around fitted canvas">0.0</param>
<param name="extraborder_units" type="optiongroup" appearance="combo" gui-text="Border offset units">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
</param>
<param name="show_fstl" type="bool" gui-text="Show converted (and fixed) STL in fstl Viewer">true</param>
</page>
<page name="meshfixing" gui-text="Mesh Fixing">
<param name="fixmesh" type="bool" gui-text="Try to repair meshes before flattening using ADMmesh (recommended)">true</param>
<separator/>
<param name="exact" type="bool" gui-text="Only check for perfectly matched edges">true</param>
<param name="nearby" type="bool" gui-text="Find and connect nearby facets. Correct bad facets">true</param>
<param name="tolerance" type="float" min="0.0" max="10000.0" precision="4" gui-text="Initial tolerance to use for nearby check">0.0</param>
<param name="iterations" type="int" min="1" max="1000" gui-text="Number of iterations for nearby check">1</param>
<param name="increment" type="float" min="0.0" max="10000.0" precision="4" gui-text="Amount to increment tolerance after iteration">0.0</param>
<param name="remove_unconnected" type="bool" gui-text="Remove facets that have 0 neighbors">true</param>
<param name="fill_holes" type="bool" gui-text="Add facets to fill holes">true</param>
<param name="normal_directions" type="bool" gui-text="Check and fix direction of normals (ie cw, ccw)">true</param>
<param name="reverse_all" type="bool" gui-text="Reverse the directions of all facets and normals">true</param>
<param name="normal_values" type="bool" gui-text="Check and fix normal values">true</param>
</page>
</param>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Import/Export/Transfer"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">papercraft_unfold.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,255 @@
#!/usr/bin/env python3
import sys
import os
import inkex
import tempfile
import subprocess
from subprocess import Popen, PIPE
from lxml import etree
#specific imports for model-converter-python
import functools as fc
import d3.model.tools as mt
from d3.model.basemodel import Vector
"""
Extension for InkScape 1.0
Import any DWG or DXF file using ODA File Converter, sk1 UniConvertor, ezdxf and more tools.
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Date: 08.09.2020
Last patch: 08.09.2020
License: GNU GPL v3
#################################################################
This tool converts a lot of different formats into STL Format. The STL then gets unfolded (flattened) to make a papercraft model. You can select between different engines. The used tools are
- converters
- openjscad
- model-converter-python
- repairers
- ADMesh
- unfolders
- osresearch/papercraft
#################################################################
openjscad [-v] <file> [-of <format>] [-o <output>]
<file> : input file (Supported types: .jscad, .js, .scad, .stl, .amf, .obj, .gcode, .svg, .json)
<output>: output file (Supported types: .jscad, .stl, .amf, .dxf, .svg, .json)
<format>: 'jscad', 'stla' (STL ASCII, default), 'stlb' (STL Binary), 'amf', 'dxf', 'svg', 'json'
#################################################################
ADMesh version 0.99.0dev
Copyright (C) 1995, 1996 Anthony D. Martin
Usage: /usr/share/inkscape/extensions/mightyscape-1.X/extensions/fablabchemnitz/papercraft/.libs/admesh [OPTION]... file
--x-rotate=angle Rotate CCW about x-axis by angle degrees
--y-rotate=angle Rotate CCW about y-axis by angle degrees
--z-rotate=angle Rotate CCW about z-axis by angle degrees
--xy-mirror Mirror about the xy plane
--yz-mirror Mirror about the yz plane
--xz-mirror Mirror about the xz plane
--scale=factor Scale the file by factor (multiply by factor)
--scale-xyz=x,y,z Scale the file by a non uniform factor
--translate=x,y,z Translate the file to x, y, and z
--merge=name Merge file called name with input file
-e, --exact Only check for perfectly matched edges
-n, --nearby Find and connect nearby facets. Correct bad facets
-t, --tolerance=tol Initial tolerance to use for nearby check = tol
-i, --iterations=i Number of iterations for nearby check = i
-m, --increment=inc Amount to increment tolerance after iteration=inc
-u, --remove-unconnected Remove facets that have 0 neighbors
-f, --fill-holes Add facets to fill holes
-d, --normal-directions Check and fix direction of normals(ie cw, ccw)
--reverse-all Reverse the directions of all facets and normals
-v, --normal-values Check and fix normal values
-c, --no-check Don't do any check on input file
-b, --write-binary-stl=name Output a binary STL file called name
-a, --write-ascii-stl=name Output an ascii STL file called name
--write-off=name Output a Geomview OFF format file called name
--write-dxf=name Output a DXF format file called name
--write-vrml=name Output a VRML format file called name
--help Display this help and exit
--version Output version information and exit
The functions are executed in the same order as the options shown here.
So check here to find what happens if, for example, --translate and --merge
options are specified together. The order of the options specified on the
command line is not important.
#################################################################
Module licenses
- papercraft (https://github.com/osresearch/papercraft) - GPL v2 License
- openjscad (https://github.com/jscad/OpenJSCAD.org) - MIT License and other, not bundled. Install it separately
- model-converter (https://github.com/tforgione/model-converter-python) - MIT License
- admesh (https://github.com/admesh/admesh) - GPL License
#TODO
- admesh Parameter als extra Tab in InkScape
- fix canvas resize
- Windows Executables
- Doku Seite mit HowTo Compile / install openjscad + admesh
"""
class Unfold(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
self.arg_parser.add_argument("--tab")
self.arg_parser.add_argument("--inputfile")
self.arg_parser.add_argument("--resizetoimport", type=inkex.Boolean, default=True, help="Resize the canvas to the imported drawing's bounding box")
self.arg_parser.add_argument("--extraborder", type=float, default=0.0)
self.arg_parser.add_argument("--extraborder_units")
self.arg_parser.add_argument("--show_fstl", type=inkex.Boolean, default=True, help="Show converted (and fixed) STL in fstl Viewer")
self.arg_parser.add_argument("--fixmesh", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("--exact", type=inkex.Boolean, default=True, help="Only check for perfectly matched edges")
self.arg_parser.add_argument("--nearby", type=inkex.Boolean, default=True, help="Find and connect nearby facets. Correct bad facets")
self.arg_parser.add_argument("--tolerance", type=float, default=0.0, help="Initial tolerance to use for nearby check")
self.arg_parser.add_argument("--iterations", type=int, default=1, help="Number of iterations for nearby check")
self.arg_parser.add_argument("--increment", type=float, default=0.0, help="Amount to increment tolerance after iteration")
self.arg_parser.add_argument("--remove_unconnected", type=inkex.Boolean, default=True, help="Remove facets that have 0 neighbors")
self.arg_parser.add_argument("--fill_holes", type=inkex.Boolean, default=True, help="Add facets to fill holes")
self.arg_parser.add_argument("--normal_directions", type=inkex.Boolean, default=True, help="Check and fix direction of normals (ie cw, ccw)")
self.arg_parser.add_argument("--reverse_all", type=inkex.Boolean, default=True, help="Reverse the directions of all facets and normals")
self.arg_parser.add_argument("--normal_values", type=inkex.Boolean, default=True, help="Check and fix normal values")
def effect(self):
import_formats_model_converter=[".obj", ".off", ".ply"] #we could also handle stl but openscad does the better job
import_formats_openjscad=[".jscad", ".js", ".scad", ".stl", ".amf", ".gcode", ".json"] #we could also handle obj but model converter does the better job
inputfile = self.options.inputfile
if not os.path.exists(inputfile):
inkex.utils.debug("The input file does not exist. Please select a proper file and try again.")
exit(1)
inputfile_format = (os.path.splitext(os.path.basename(inputfile))[1]).lower()
if inputfile_format not in import_formats_model_converter and inputfile_format not in import_formats_openjscad:
inkex.utils.debug("The input file format cannot be converted to a required format for flattening.")
exit(1)
converted_inputfile = os.path.join(tempfile.gettempdir(), os.path.splitext(os.path.basename(inputfile))[0] + ".stl")
if os.path.exists(converted_inputfile):
os.remove(converted_inputfile) #remove previously generated conversion file
if inputfile_format in import_formats_model_converter:
up_conversion = None
with open(converted_inputfile, 'w') as f:
f.write(mt.convert(inputfile, converted_inputfile, up_conversion))
if inputfile_format in import_formats_openjscad:
cmd = "openjscad \"" + inputfile + "\" -of stlb -o \"" + converted_inputfile + "\""
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
p.wait()
if p.returncode != 0 or len(stderr) > 0:
inkex.utils.debug("openjscad conversion failed: %d %s %s" % (p.returncode, stdout, stderr))
#we do this to convert possibly previously generated ASCII STL from model converter to binary STL to ensure always binary STLs
cmd = "openjscad \"" + converted_inputfile + "\" -of stlb -o \"" + converted_inputfile + "\""
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
p.wait()
if p.returncode != 0 or len(stderr) > 0:
inkex.utils.debug("openjscad conversion failed: %d %s %s" % (p.returncode, stdout, stderr))
if not os.path.exists(converted_inputfile):
inkex.utils.debug("Cannot find conversion output. Unable to continue")
exit(1)
# Run ADMesh mesh fixer to overwrite the STL with fixed output (binary output too)
if self.options.fixmesh == True:
if os.name=="nt":
cmd = "admesh\\admesh.exe "
else:
cmd = "./admesh/admesh "
if self.options.exact == True: cmd += "--exact "
if self.options.nearby == True: cmd += "--nearby "
if self.options.tolerance > 0.0: cmd += "--tolerance " + str(self.options.tolerance) + " "
if self.options.iterations > 1: cmd += "--iterations " + str(self.options.iterations) + " "
if self.options.increment > 0.0: cmd += "--increment " + str(self.options.increment) + " "
if self.options.remove_unconnected == True: cmd += "--remove-unconnected "
if self.options.normal_directions == True: cmd += "--normal-directions "
if self.options.fill_holes == True: cmd += "--fill-holes "
if self.options.reverse_all == True: cmd += "--reverse-all "
if self.options.normal_values == True: cmd += "--normal-values "
cmd += "\"" + converted_inputfile + "\" "
cmd += "-b \"" + converted_inputfile + "\""
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
p.wait()
if p.returncode != 0:
inkex.utils.debug("admesh failed: %d %s %s" % (p.returncode, stdout, stderr))
# Run papercraft flattening
converted_flattenfile = os.path.join(tempfile.gettempdir(), os.path.splitext(os.path.basename(inputfile))[0] + ".svg")
if os.path.exists(converted_flattenfile):
os.remove(converted_flattenfile) #remove previously generated conversion file
if os.name=="nt":
cmd = "unfold\\unfold.exe" + " < \"" + converted_inputfile + "\" > \"" + converted_flattenfile + "\""
else:
cmd = "./unfold/unfold" + " < \"" + converted_inputfile + "\" > \"" + converted_flattenfile + "\""
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
p.wait()
if p.returncode != 0:
inkex.utils.debug("osresearch/papercraft unfold failed: %d %s %s" % (p.returncode, stdout, stderr))
# Open converted output in fstl
if self.options.show_fstl == True:
if os.name=="nt":
cmd = "fstl\\fstl.exe \"" + converted_inputfile + "\""
else:
cmd = "./fstl/fstl \"" + converted_inputfile + "\""
p = Popen(cmd, shell=True)
p.wait()
# Write the generated SVG into InkScape's canvas
try:
stream = open(converted_flattenfile, 'r')
except FileNotFoundError as e:
inkex.utils.debug("There was no SVG output generated. Cannot continue")
exit(1)
p = etree.XMLParser(huge_tree=True)
doc = etree.parse(stream, parser=etree.XMLParser(huge_tree=True)).getroot()
stream.close()
doc.set('id', self.svg.get_unique_id('papercraft_unfold'))
self.document.getroot().append(doc)
#adjust viewport and width/height to have the import at the center of the canvas - unstable at the moment.
if self.options.resizetoimport:
elements = []
for child in doc.getchildren():
#if child.tag == inkex.addNS('g','svg'):
elements.append(child)
#build sum of bounding boxes and ignore errors for faulty elements (sum function often fails for that usecase!)
bbox = None
try:
bbox = elements[0].bounding_box() #init with the first bounding box of the tree (and hope that it is not a faulty one)
except Exception as e:
#inkex.utils.debug(str(e))
pass
count = 0
for element in elements:
if count != 0: #skip the first
try:
#bbox.add(element.bounding_box())
bbox += element.bounding_box()
except Exception as e:
#inkex.utils.debug(str(e))
pass
count += 1 #some stupid counter
if bbox is not None:
root = self.svg.getElement('//svg:svg');
offset = self.svg.unittouu(str(self.options.extraborder) + self.options.extraborder_units)
root.set('viewBox', '%f %f %f %f' % (bbox.left - offset, bbox.top - offset, bbox.width + 2 * offset, bbox.height + 2 * offset))
root.set('width', bbox.width + 2 * offset)
root.set('height', bbox.height + 2 * offset)
if __name__ == '__main__':
Unfold().run()

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Papercraft Unfold ASCII</name>
<id>fablabchemnitz.de.papercraft_unfold_ascii</id>
<input>
<extension>.stl</extension>
<mimetype>application/sla</mimetype>
<filetypename>Unfoldable Stereolitography File ASCII (*.stl)</filetypename>
<filetypetooltip>Unfold STL Files</filetypetooltip>
</input>
<script>
<command location="inx" interpreter="python">papercraft_unfold_ascii.py</command>
</script>
</inkscape-extension>

View File

@ -1,39 +0,0 @@
#!/usr/bin/env python3
import sys
import os
import inkex
import subprocess
from subprocess import Popen
from lxml import etree
class Unfold(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
stl_filename = sys.argv[1]
#inkex.utils.debug("stl_filename: "+stl_filename)
if os.name=="nt":
outname = "papercraft_unfold_output.svg"
#remove old file if existent
if os.path.exists(outname):
os.remove(outname)
if os.path.exists("unfold.exe.stackdump"):
os.remove("unfold.exe.stackdump")
#convert the STL to have a binary one and wait until conversion finished before running papercraft
cmd = os.getcwd() + "\\papercraft\\STLConverter.exe" + " \"" + stl_filename + "\""
p = Popen(cmd, shell=True)
#inkex.utils.debug(cmd)
#inkex.utils.debug("os.getcwd(): "+os.getcwd())
#inkex.utils.debug(os.path.splitext(stl_filename)[0])
p.wait()
cmd2 = os.getcwd() + "\\papercraft\\unfold.exe" + " < \"" + os.path.splitext(stl_filename)[0] + "-binary.stl\" > " + outname
#inkex.utils.debug("cmd2: "+cmd2)
p2 = Popen(cmd2, shell=True)
#inkex.utils.debug(p2.communicate())
p2.wait()
if p2.returncode == 0:
#inkex.utils.debug("OK")
doc = etree.parse(os.getcwd() + "\\" + outname)
doc.write(sys.stdout.buffer)
if __name__ == '__main__':
gc = Unfold()

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Papercraft Unfold Binary</name>
<id>fablabchemnitz.de.papercraft_unfold_binary</id>
<input>
<extension>.stl</extension>
<mimetype>application/sla</mimetype>
<filetypename>Unfoldable Stereolitography File Binary (*.stl)</filetypename>
<filetypetooltip>Unfold STL Files</filetypetooltip>
</input>
<script>
<command location="inx" interpreter="python">papercraft_unfold_binary.py</command>
</script>
</inkscape-extension>

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
import sys
import os
import inkex
import subprocess
from subprocess import Popen
from lxml import etree
class Unfold(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
stl_filename = sys.argv[1]
#inkex.utils.debug("stl_filename: "+stl_filename)
if os.name=="nt":
outname = "papercraft_unfold_output.svg"
#remove old file if existent
if os.path.exists(outname):
os.remove(outname)
if os.path.exists("unfold.exe.stackdump"):
os.remove("unfold.exe.stackdump")
#inkex.utils.debug("os.getcwd(): "+os.getcwd())
#inkex.utils.debug(os.path.splitext(stl_filename)[0])
cmd = os.getcwd() + "\\papercraft\\unfold.exe" + " < \"" + stl_filename + "\" > " + outname
#inkex.utils.debug("cmd: "+cmd)
p = Popen(cmd, shell=True)
#inkex.utils.debug(p.communicate())
p.wait()
if p.returncode == 0:
#inkex.utils.debug("OK")
doc = etree.parse(os.getcwd() + "\\" + outname)
#inkex.utils.debug(etree.tostring(doc))
doc.write(sys.stdout.buffer)
if __name__ == '__main__':
gc = Unfold()

Binary file not shown.