Some cleanings

This commit is contained in:
Mario Voigt 2020-09-13 03:24:10 +02:00
parent 6c9508acf8
commit c2b48e521a
2328 changed files with 0 additions and 577913 deletions

View File

@ -1,41 +0,0 @@
# 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

@ -1,41 +0,0 @@
# 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

@ -1,210 +0,0 @@
#! /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

View File

@ -1,201 +0,0 @@
/* 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

@ -1,41 +0,0 @@
# 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

@ -1,11 +0,0 @@
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}

View File

@ -1,10 +0,0 @@
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

@ -1,34 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 149 KiB

View File

@ -1,22 +0,0 @@
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

@ -1,12 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,112 +0,0 @@
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

@ -1,108 +0,0 @@
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

@ -1,334 +0,0 @@
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

@ -1,4 +0,0 @@
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

@ -1,208 +0,0 @@
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

@ -1,65 +0,0 @@
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

@ -1,494 +0,0 @@
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

@ -1,116 +0,0 @@
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

@ -1,230 +0,0 @@
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

@ -1,99 +0,0 @@
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

@ -1,53 +0,0 @@
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)

View File

@ -1,15 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../astring/bin/astring" "$@"
ret=$?
else
node "$basedir/../astring/bin/astring" "$@"
ret=$?
fi
exit $ret

View File

@ -1,17 +0,0 @@
@ECHO off
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
"%_prog%" "%dp0%\..\astring\bin\astring" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b

View File

@ -1,18 +0,0 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
& "$basedir/node$exe" "$basedir/../astring/bin/astring" $args
$ret=$LASTEXITCODE
} else {
& "node$exe" "$basedir/../astring/bin/astring" $args
$ret=$LASTEXITCODE
}
exit $ret

View File

@ -1,15 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../esprima/bin/esparse.js" "$@"
ret=$?
else
node "$basedir/../esprima/bin/esparse.js" "$@"
ret=$?
fi
exit $ret

View File

@ -1,17 +0,0 @@
@ECHO off
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
"%_prog%" "%dp0%\..\esprima\bin\esparse.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b

View File

@ -1,18 +0,0 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
& "$basedir/node$exe" "$basedir/../esprima/bin/esparse.js" $args
$ret=$LASTEXITCODE
} else {
& "node$exe" "$basedir/../esprima/bin/esparse.js" $args
$ret=$LASTEXITCODE
}
exit $ret

View File

@ -1,15 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../esprima/bin/esvalidate.js" "$@"
ret=$?
else
node "$basedir/../esprima/bin/esvalidate.js" "$@"
ret=$?
fi
exit $ret

View File

@ -1,17 +0,0 @@
@ECHO off
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
"%_prog%" "%dp0%\..\esprima\bin\esvalidate.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b

View File

@ -1,18 +0,0 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
& "$basedir/node$exe" "$basedir/../esprima/bin/esvalidate.js" $args
$ret=$LASTEXITCODE
} else {
& "node$exe" "$basedir/../esprima/bin/esvalidate.js" $args
$ret=$LASTEXITCODE
}
exit $ret

View File

@ -1,15 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../@jscad/openjscad/src/cli/cli.js" "$@"
ret=$?
else
node "$basedir/../@jscad/openjscad/src/cli/cli.js" "$@"
ret=$?
fi
exit $ret

View File

@ -1,17 +0,0 @@
@ECHO off
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
"%_prog%" "%dp0%\..\@jscad\openjscad\src\cli\cli.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b

View File

@ -1,18 +0,0 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
& "$basedir/node$exe" "$basedir/../@jscad/openjscad/src/cli/cli.js" $args
$ret=$LASTEXITCODE
} else {
& "node$exe" "$basedir/../@jscad/openjscad/src/cli/cli.js" $args
$ret=$LASTEXITCODE
}
exit $ret

View File

@ -1,28 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
<a name="0.0.4"></a>
## [0.0.4](https://github.com/jscad/io/compare/@jscad/amf-deserializer@0.0.3...@jscad/amf-deserializer@0.0.4) (2017-11-04)
**Note:** Version bump only for package @jscad/amf-deserializer
<a name="0.0.3"></a>
## [0.0.3](https://github.com/jscad/io/compare/@jscad/amf-deserializer@0.0.2...@jscad/amf-deserializer@0.0.3) (2017-10-10)
**Note:** Version bump only for package @jscad/amf-deserializer
<a name="0.0.2"></a>
## 0.0.2 (2017-10-10)
**Note:** Version bump only for package @jscad/amf-deserializer

View File

@ -1,50 +0,0 @@
## @jscad/amf-deserializer
> amf deserializer for the jscad project
[![npm version](https://badge.fury.io/js/%40jscad%2Famf-deserializer.svg)](https://badge.fury.io/js/%40jscad%2Famf-deserializer)
[![Build Status](https://travis-ci.org/jscad/io.svg)](https://travis-ci.org/jscad/amf-deserializer)
## Overview
This deserializer converts raw amf data to jscad code (that can be evaluated to CSG/CAG).
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [Contribute](#contribute)
- [License](#license)
## Installation
```
npm install @jscad/amf-deserializer
```
## Usage
```javascript
const amfDeSerializer = require('@jscad/amf-deserializer')
const rawData = fs.readFileSync('PATH/TO/file.amf')
const csgData = amfDeSerializer(rawData)
```
## Contribute
For questions about the API, please contact the [User Group](https://plus.google.com/communities/114958480887231067224)
PRs accepted.
Small Note: If editing this README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
## License
[The MIT License (MIT)](./LICENSE)
(unless specified otherwise)

View File

@ -1,728 +0,0 @@
/*
## License
Copyright (c) 2016 Z3 Development https://github.com/z3dev
Copyright (c) 2013-2016 by Rene K. Mueller <spiritdude@gmail.com>
Copyright (c) 2016 by Z3D Development
All code released under MIT license
History:
2016/06/27: 0.5.1: rewrote using SAX XML parser, enhanced for multiple objects, materials, units by Z3Dev
2013/04/11: 0.018: added alpha support to AMF export
*/
// //////////////////////////////////////////
//
// AMF is a language for describing three-dimensional graphics in XML
// See http://www.astm.org/Standards/ISOASTM52915.htm
// See http://amf.wikispaces.com/
//
// //////////////////////////////////////////
const sax = require('sax')
const inchMM = (1 / 0.039370) // used for scaling AMF (inch) to CAG coordinates(MM)
// processing controls
sax.SAXParser.prototype.amfLast = null // last object found
sax.SAXParser.prototype.amfDefinition = 0 // definitions beinging created
// 0-AMF,1-object,2-material,3-texture,4-constellation,5-metadata
// high level elements / definitions
sax.SAXParser.prototype.amfObjects = [] // list of objects
sax.SAXParser.prototype.amfMaterials = [] // list of materials
sax.SAXParser.prototype.amfTextures = [] // list of textures
sax.SAXParser.prototype.amfConstels = [] // list of constellations
sax.SAXParser.prototype.amfMetadata = [] // list of metadata
sax.SAXParser.prototype.amfObj = null // amf in object form
function amfAmf (element) {
// default SVG with no viewport
var obj = {type: 'amf', unit: 'mm', scale: 1.0}
if ('UNIT' in element) { obj.unit = element.UNIT.toLowerCase() }
// set scaling
switch (obj.unit.toLowerCase()) {
case 'inch':
obj.scale = inchMM
break
case 'foot':
obj.scale = inchMM * 12.0
break
case 'meter':
obj.scale = 1000.0
break
case 'micron':
obj.scale = 0.001
break
case 'millimeter':
default:
break
}
obj.objects = []
return obj
}
sax.SAXParser.prototype.amfObject = function (element) {
var obj = {type: 'object', id: 'JSCAD' + (this.amfObjects.length)} // default ID
if ('ID' in element) { obj.id = element.ID }
obj.objects = []
return obj
}
function amfMesh (element) {
var obj = {type: 'mesh'}
obj.objects = []
return obj
}
// Note: TBD Vertices can have a color, which is used to interpolate a face color (from the 3 vertices)
function amfVertices (element) {
var obj = {type: 'vertices'}
obj.objects = []
return obj
}
function amfCoordinates (element) {
var obj = {type: 'coordinates'}
obj.objects = []
return obj
}
function amfNormal (element) {
var obj = {type: 'normal'}
obj.objects = []
return obj
}
function amfX (element) {
return {type: 'x', value: '0'}
}
function amfY (element) {
return {type: 'y', value: '0'}
}
function amfZ (element) {
return {type: 'z', value: '0'}
}
function amfVolume (element) {
var obj = {type: 'volume'}
if ('MATERIALID' in element) { obj.materialid = element.MATERIALID }
obj.objects = []
return obj
}
function amfTriangle (element) {
var obj = {type: 'triangle'}
obj.objects = []
return obj
}
function amfV1 (element) {
return {type: 'v1', value: '0'}
}
function amfV2 (element) {
return {type: 'v2', value: '0'}
}
function amfV3 (element) {
return {type: 'v3', value: '0'}
}
function amfVertex (element) {
var obj = {type: 'vertex'}
obj.objects = []
return obj
}
function amfEdge (element) {
var obj = {type: 'edge'}
obj.objects = []
return obj
}
function amfMetadata (element) {
var obj = {type: 'metadata'}
if ('TYPE' in element) { obj.mtype = element.TYPE }
if ('ID' in element) { obj.id = element.ID }
return obj
}
function amfMaterial (element) {
var obj = {type: 'material'}
if ('ID' in element) { obj.id = element.ID }
obj.objects = []
return obj
}
function amfColor (element) {
var obj = {type: 'color'}
obj.objects = []
return obj
}
function amfR (element) {
return {type: 'r', value: '1'}
}
function amfG (element) {
return {type: 'g', value: '1'}
}
function amfB (element) {
return {type: 'b', value: '1'}
}
function amfA (element) {
return {type: 'a', value: '1'}
}
function amfMap (element) {
var obj = {type: 'map'}
if ('GTEXID' in element) { obj.gtexid = element.GTEXID }
if ('BTEXID' in element) { obj.btexid = element.BTEXID }
if ('RTEXID' in element) { obj.rtexid = element.RTEXID }
obj.objects = []
return obj
}
function amfU1 (element) {
return {type: 'u1', value: '0'}
}
function amfU2 (element) {
return {type: 'u2', value: '0'}
}
function amfU3 (element) {
return {type: 'u3', value: '0'}
}
function createAmfParser (src, pxPmm) {
// create a parser for the XML
var parser = sax.parser(false, {trim: true, lowercase: false, position: true})
parser.onerror = function (e) {
console.log('error: line ' + e.line + ', column ' + e.column + ', bad character [' + e.c + ']')
}
parser.onopentag = function (node) {
// console.log('opentag: '+node.name+' at line '+this.line+' position '+this.column);
// for (x in node.attributes) {
// console.log(' '+x+'='+node.attributes[x]);
// }
// case 'VTEX1':
// case 'VTEX2':
// case 'VTEX3':
var obj = null
switch (node.name) {
// top level elements
case 'AMF':
obj = amfAmf(node.attributes)
break
case 'OBJECT':
obj = this.amfObject(node.attributes)
if (this.amfDefinition === 0) this.amfDefinition = 1 // OBJECT processing
break
case 'MESH':
obj = amfMesh(node.attributes)
break
case 'VERTICES':
obj = amfVertices(node.attributes)
break
case 'VERTEX':
obj = amfVertex(node.attributes)
break
case 'EDGE':
obj = amfEdge(node.attributes)
break
case 'VOLUME':
obj = amfVolume(node.attributes)
break
case 'MATERIAL':
obj = amfMaterial(node.attributes)
if (this.amfDefinition === 0) this.amfDefinition = 2 // MATERIAL processing
break
case 'COMPOSITE':
break
case 'TEXTURE':
if (this.amfDefinition === 0) this.amfDefinition = 3 // TEXTURE processing
break
case 'CONSTELLATION':
if (this.amfDefinition === 0) this.amfDefinition = 4 // CONSTELLATION processing
break
case 'METADATA':
obj = amfMetadata(node.attributes)
if (this.amfDefinition === 0) this.amfDefinition = 5 // METADATA processing
break
// coordinate elements
case 'COORDINATES':
obj = amfCoordinates(node.attributes)
break
case 'NORMAL':
obj = amfNormal(node.attributes)
break
case 'X':
case 'NX':
obj = amfX(node.attributes)
break
case 'Y':
case 'NY':
obj = amfY(node.attributes)
break
case 'Z':
case 'NZ':
obj = amfZ(node.attributes)
break
// triangle elements
case 'TRIANGLE':
obj = amfTriangle(node.attributes)
break
case 'V1':
case 'VTEX1':
obj = amfV1(node.attributes)
break
case 'V2':
case 'VTEX2':
obj = amfV2(node.attributes)
break
case 'V3':
case 'VTEX3':
obj = amfV3(node.attributes)
break
// color elements
case 'COLOR':
obj = amfColor(node.attributes)
break
case 'R':
obj = amfR(node.attributes)
break
case 'G':
obj = amfG(node.attributes)
break
case 'B':
obj = amfB(node.attributes)
break
case 'A':
obj = amfA(node.attributes)
break
// map elements
case 'MAP':
case 'TEXMAP':
obj = amfMap(node.attributes)
break
case 'U1':
case 'UTEX1':
case 'WTEX1':
obj = amfU1(node.attributes)
break
case 'U2':
case 'UTEX2':
case 'WTEX2':
obj = amfU2(node.attributes)
break
case 'U3':
case 'UTEX3':
case 'WTEX3':
obj = amfU3(node.attributes)
break
default:
// console.log('opentag: '+node.name+' at line '+this.line+' position '+this.column);
break
}
if (obj !== null) {
// console.log('definitinon '+this.amfDefinition);
switch (this.amfDefinition) {
case 0: // definition of AMF
if ('objects' in obj) {
// console.log('push object ['+obj.type+']');
this.amfObjects.push(obj)
}
break
case 1: // definition of OBJECT
if (this.amfObjects.length > 0) {
var group = this.amfObjects.pop()
// add the object to the active group if necessary
if ('objects' in group) {
// console.log('object '+group.type+' adding ['+obj.type+']');
// console.log(JSON.stringify(obj));
group.objects.push(obj)
}
this.amfObjects.push(group)
// and push this object as a group object if necessary
if ('objects' in obj) {
// console.log('object group ['+obj.type+']');
this.amfObjects.push(obj)
}
}
break
case 2: // definition of MATERIAL
if (obj.type === 'material') {
// console.log('push material ['+obj.type+']');
this.amfMaterials.push(obj)
} else {
if (this.amfMaterials.length > 0) {
let group = this.amfMaterials.pop()
// add the object to the active group if necessary
if ('objects' in group) {
// console.log('material '+group.type+' adding ['+obj.type+']');
// console.log(JSON.stringify(obj));
group.objects.push(obj)
}
this.amfMaterials.push(group)
// and push this object as a group object if necessary
if ('objects' in obj) {
// console.log('push material ['+obj.type+']');
this.amfMaterials.push(obj)
}
}
}
break
case 3: // definition of TEXTURE
break
case 4: // definition of CONSTELLATION
break
case 5: // definition of METADATA
break
default:
console.log('ERROR: invalid AMF definition')
break
}
this.amfLast = obj // retain this object in order to add values
}
}
parser.onclosetag = function (node) {
// console.log('onclosetag: '+this.amfDefinition);
switch (node) {
// list those which have objects
case 'AMF':
case 'OBJECT':
case 'MESH':
case 'VERTICES':
case 'VERTEX':
case 'EDGE':
case 'COORDINATES':
case 'NORMAL':
case 'VOLUME':
case 'TRIANGLE':
case 'MATERIAL':
case 'COLOR':
case 'MAP':
case 'TEXMAP':
break
case 'TEXTURE':
if (this.amfDefinition === 3) { this.amfDefinition = 0 } // resume processing
return
case 'CONSTELLATION':
if (this.amfDefinition === 4) { this.amfDefinition = 0 } // resume processing
return
case 'METADATA':
if (this.amfDefinition === 5) { this.amfDefinition = 0 } // resume processing
return
default:
// console.log('closetag: '+node);
return
}
var obj = null
switch (this.amfDefinition) {
case 0: // definition of AMF
case 1: // definition of OBJECT
if (this.amfObjects.length > 0) {
obj = this.amfObjects.pop()
// console.log('pop object ['+obj.type+']');
if (obj.type === 'object') {
this.amfDefinition = 0 // AMF processing
}
}
// check for completeness
if (this.amfObjects.length === 0) {
this.amfObj = obj
}
break
case 2: // definition of MATERIAL
if (this.amfMaterials.length > 0) {
obj = this.amfMaterials.pop()
// console.log('pop material ['+obj.type+']');
if (obj.type === 'material') {
this.amfMaterials.push(obj) // keep a list of materials
this.amfDefinition = 0 // AMF processing
}
}
break
case 3: // definition of TEXTURE
this.amfDefinition = 0 // AMF processing
break
case 4: // definition of CONSTELLATION
this.amfDefinition = 0 // AMF processing
break
case 5: // definition of METADATA
this.amfDefinition = 0 // AMF processing
break
default:
break
}
}
parser.ontext = function (value) {
if (value !== null) {
if (this.amfLast && this.amfDefinition !== 0) {
this.amfLast.value = value
// console.log(JSON.stringify(this.amfLast));
}
}
}
parser.onend = function () {
// console.log('AMF parsing completed');
}
// start the parser
parser.write(src).close()
return parser
}
//
// convert the internal repreentation into JSCAD code
//
function codify (amf, data) {
if (amf.type !== 'amf' || (!amf.objects)) throw new Error('AMF malformed')
let code = ''
// hack due to lack of this in array map()
var objects = amf.objects
var materials = data.amfMaterials
var lastmaterial = null
function findMaterial (id) {
if (lastmaterial && lastmaterial.id === id) return lastmaterial
for (let i = 0; i < materials.length; i++) {
if (materials[i].id && materials[i].id === id) {
lastmaterial = materials[i]
return lastmaterial
}
}
return null
}
function getValue (objects, type) {
for (let i = 0; i < objects.length; i++) {
if (objects[i].type === type) return objects[i].value
}
return null
}
function getColor (objects) {
for (let i = 0; i < objects.length; i++) {
var obj = objects[i]
if (obj.type === 'color') {
var r = parseFloat(getValue(obj.objects, 'r'))
var g = parseFloat(getValue(obj.objects, 'g'))
var b = parseFloat(getValue(obj.objects, 'b'))
var a = parseFloat(getValue(obj.objects, 'a'))
if (Number.isNaN(r)) r = 1.0 // AMF default color
if (Number.isNaN(g)) g = 1.0
if (Number.isNaN(b)) b = 1.0
if (Number.isNaN(a)) a = 1.0
return [r, g, b, a]
}
}
return null
}
function findColorByMaterial (id) {
var m = findMaterial(id)
if (m) {
return getColor(m.objects)
}
return null
}
// convert high level definitions
function createDefinition (obj, didx) {
// console.log(materials.length);
switch (obj.type) {
case 'object':
createObject(obj, didx)
break
case 'metadata':
break
case 'material':
break
default:
console.log('Warning: unknown definition: ' + obj.type)
break
}
}
// convert all objects to CSG based code
function createObject (obj, oidx) {
var vertices = [] // [x,y,z]
var faces = [] // [v1,v2,v3]
var colors = [] // [r,g,b,a]
function addCoord (coord, cidx) {
if (coord.type === 'coordinates') {
var x = parseFloat(getValue(coord.objects, 'x'))
var y = parseFloat(getValue(coord.objects, 'y'))
var z = parseFloat(getValue(coord.objects, 'z'))
// console.log('['+x+','+y+','+z+']');
vertices.push([x, y, z])
}
// normal is possible
}
function addVertex (vertex, vidx) {
// console.log(vertex.type);
if (vertex.type === 'vertex') {
vertex.objects.map(addCoord)
}
// edge is possible
}
function addTriangle (tri, tidx) {
if (tri.type === 'triangle') {
var v1 = parseInt(getValue(tri.objects, 'v1'))
var v2 = parseInt(getValue(tri.objects, 'v2'))
var v3 = parseInt(getValue(tri.objects, 'v3'))
// console.log('['+v1+','+v2+','+v3+']');
faces.push([v1, v2, v3]) // HINT: reverse order for polyhedron()
var c = getColor(tri.objects)
if (c) {
colors.push(c)
} else {
colors.push(tricolor)
}
}
}
var tricolor = null // for found colors
function addPart (part, pidx) {
// console.log(part.type);
switch (part.type) {
case 'vertices':
part.objects.map(addVertex, data)
break
case 'volume':
tricolor = getColor(part.objects)
if (part.materialid) {
// convert material to color
tricolor = findColorByMaterial(part.materialid)
}
part.objects.map(addTriangle, data)
break
default:
break
}
}
function addMesh (mesh, midx) {
// console.log(mesh.type);
if (mesh.type === 'mesh') {
mesh.objects.map(addPart, data)
}
}
if (obj.objects.length > 0) {
obj.objects.map(addMesh, data)
var fcount = faces.length
var vcount = vertices.length
code += '// Object ' + obj.id + '\n'
code += '// faces : ' + fcount + '\n'
code += '// vertices: ' + vcount + '\n'
code += 'function createObject' + obj.id + '() {\n'
code += ' var polys = [];\n'
// convert the results into function calls
for (var i = 0; i < fcount; i++) {
code += ' polys.push(\n'
code += ' PP([\n'
for (var j = 0; j < faces[i].length; j++) {
if (faces[i][j] < 0 || faces[i][j] >= vcount) {
// if (err.length === '') err += 'bad index for vertice (out of range)'
continue
}
if (j) code += ',\n'
code += ' VV(' + vertices[faces[i][j]] + ')'
}
code += '])'
if (colors[i]) {
var c = colors[i]
code += '.setColor([' + c[0] + ',' + c[1] + ',' + c[2] + ',' + c[3] + '])'
}
code += ');\n'
}
code += ' return CSG.fromPolygons(polys);\n'
code += '}\n'
}
}
// start everthing
code = '// Objects : ' + objects.length + '\n'
code += '// Materials: ' + materials.length + '\n'
code += '\n'
code += '// helper functions\n'
if (amf.scale !== 1.0) {
code += 'var SCALE = ' + amf.scale + '; // scaling units (' + amf.unit + ')\n'
code += 'var VV = function(x,y,z) { return new CSG.Vertex(new CSG.Vector3D(x*SCALE,y*SCALE,z*SCALE)); };\n'
} else {
code += 'var VV = function(x,y,z) { return new CSG.Vertex(new CSG.Vector3D(x,y,z)); };\n'
}
code += 'var PP = function(a) { return new CSG.Polygon(a); };\n'
code += '\n'
code += 'function main() {\n'
code += ' var csgs = [];\n'
for (let i = 0; i < objects.length; i++) {
var obj = objects[i]
if (obj.type === 'object') {
code += ' csgs.push(createObject' + obj.id + '());\n'
}
}
code += ' return union(csgs);\n'
code += '}\n'
code += '\n'
objects.map(createDefinition, data)
return code
}
//
// deserialize the given AMF source and return a JSCAD script
//
// fn (optional) original filename of AMF source
// options (optional) anonymous object with:
// pxPmm: pixels per milimeter for calcuations
// FIXME: add openjscad version in a cleaner manner ?
function deserialize (src, fn, options) {
fn = fn || 'amf'
const defaults = {version: '0.0.0'}
options = Object.assign({}, defaults, options)
const {version} = options
// parse the AMF source
const parser = createAmfParser(src)
// convert the internal objects to JSCAD code
var code = ''
code += '//\n'
code += '// producer: OpenJSCAD.org ' + version + ' AMF Importer\n'
code += '// date: ' + (new Date()) + '\n'
code += '// source: ' + fn + '\n'
code += '//\n'
if (parser.amfObj !== null) {
// console.log(JSON.stringify(parser.amfObj))
// console.log(JSON.stringify(parser.amfMaterials))
code += codify(parser.amfObj, parser)
} else {
console.log('Warning: AMF parsing failed')
}
return code
}
module.exports = {
deserialize
}

View File

@ -1,71 +0,0 @@
{
"_from": "@jscad/amf-deserializer@^0.0.4",
"_id": "@jscad/amf-deserializer@0.0.4",
"_inBundle": false,
"_integrity": "sha1-BB3uKQrNpBEPs7m/hxoepUIDTCs=",
"_location": "/@jscad/amf-deserializer",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "@jscad/amf-deserializer@^0.0.4",
"name": "@jscad/amf-deserializer",
"escapedName": "@jscad%2famf-deserializer",
"scope": "@jscad",
"rawSpec": "^0.0.4",
"saveSpec": null,
"fetchSpec": "^0.0.4"
},
"_requiredBy": [
"/@jscad/io"
],
"_resolved": "https://registry.npmjs.org/@jscad/amf-deserializer/-/amf-deserializer-0.0.4.tgz",
"_shasum": "041dee290acda4110fb3b9bf871a1ea542034c2b",
"_spec": "@jscad/amf-deserializer@^0.0.4",
"_where": "C:\\Users\\tomate\\node_modules\\@jscad\\io",
"bugs": {
"url": "https://github.com/jscad/io/issues"
},
"bundleDependencies": false,
"contributors": [
{
"name": "Rene K. Mueller",
"url": "http://renekmueller.com"
},
{
"name": "z3dev",
"url": "http://www.z3d.jp"
},
{
"name": "Mark 'kaosat-dev' Moissette",
"url": "http://kaosat.net"
}
],
"dependencies": {
"sax": "^1.2.1"
},
"deprecated": false,
"description": "Amf deserializer for jscad project",
"homepage": "https://github.com/jscad/io#readme",
"keywords": [
"openjscad",
"jscad",
"csg",
"deserializer",
"amf"
],
"license": "MIT",
"main": "index.js",
"name": "@jscad/amf-deserializer",
"repository": {
"type": "git",
"url": "git+https://github.com/jscad/io.git"
},
"scripts": {
"release-major": "git checkout master && npm version major && git commit -a -m 'chore(dist): built dist/'; git push origin master --tags ",
"release-minor": "git checkout master && npm version minor && git commit -a -m 'chore(dist): built dist/'; git push origin master --tags ",
"release-patch": "git checkout master && npm version patch && git commit -a -m 'chore(dist): built dist/'; git push origin master --tags ",
"test": "ava './test.js' --verbose --timeout 10000"
},
"version": "0.0.4"
}

View File

@ -1,4 +0,0 @@
const test = require('ava')
const deserializer = require('./index.js')
test.todo('add some actual tests later')

View File

@ -1,36 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
<a name="0.0.5"></a>
## [0.0.5](https://github.com/jscad/io/compare/@jscad/amf-serializer@0.0.4...@jscad/amf-serializer@0.0.5) (2017-11-04)
**Note:** Version bump only for package @jscad/amf-serializer
<a name="0.0.4"></a>
## [0.0.4](https://github.com/jscad/io/compare/@jscad/amf-serializer@0.0.3...@jscad/amf-serializer@0.0.4) (2017-10-30)
**Note:** Version bump only for package @jscad/amf-serializer
<a name="0.0.3"></a>
## [0.0.3](https://github.com/jscad/io/compare/@jscad/amf-serializer@0.0.2...@jscad/amf-serializer@0.0.3) (2017-10-10)
**Note:** Version bump only for package @jscad/amf-serializer
<a name="0.0.2"></a>
## 0.0.2 (2017-10-10)
**Note:** Version bump only for package @jscad/amf-serializer

View File

@ -1,54 +0,0 @@
## @jscad/amf-serializer
> amf serializer for the jscad project (from CSG)
[![npm version](https://badge.fury.io/js/%40jscad%2Famf-serializer.svg)](https://badge.fury.io/js/%40jscad%2Famf-serializer)
[![Build Status](https://travis-ci.org/jscad/io.svg)](https://travis-ci.org/jscad/amf-serializer)
## Overview
This serializer outputs a 'blobable' array of data (from a CSG object)
ie an array that can either be passed directly to a Blob (`new Blob(blobable)`)
or converted to a Node.js buffer.
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [Contribute](#contribute)
- [License](#license)
## Installation
```
npm install @jscad/amf-serializer
```
## Usage
```javascript
const amfSerializer = require('@jscad/amf-serializer')
const rawData = amfSerializer(CSGObject)
//in browser (with browserify etc)
const blob = new Blob(rawData)
```
## Contribute
For questions about the API, please contact the [User Group](https://plus.google.com/communities/114958480887231067224)
PRs accepted.
Small Note: If editing this README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
## License
[The MIT License (MIT)](./LICENSE)
(unless specified otherwise)

View File

@ -1,70 +0,0 @@
const { ensureManifoldness } = require('@jscad/io-utils')
const mimeType = 'application/amf+xml'
function serialize (CSG, m) {
CSG = ensureManifoldness(CSG)
var result = '<?xml version="1.0" encoding="UTF-8"?>\n<amf' + (m && m.unit ? ' unit="+m.unit"' : '') + '>\n'
for (var k in m) {
result += '<metadata type="' + k + '">' + m[k] + '</metadata>\n'
}
result += '<object id="0">\n<mesh>\n<vertices>\n'
CSG.polygons.map(function (p) { // first we dump all vertices of all polygons
for (var i = 0; i < p.vertices.length; i++) {
result += CSGVertextoAMFString(p.vertices[i])
}
})
result += '</vertices>\n'
var n = 0
CSG.polygons.map(function (p) { // then we dump all polygons
result += '<volume>\n'
if (p.vertices.length < 3) {
return
}
var color = null
if (p.shared && p.shared.color) {
color = p.shared.color
} else if (p.color) {
color = p.color
}
if (color != null) {
if (color.length < 4) color.push(1.0)
result += '<color><r>' + color[0] + '</r><g>' + color[1] + '</g><b>' + color[2] + '</b><a>' + color[3] + '</a></color>'
}
for (var i = 0; i < p.vertices.length - 2; i++) { // making sure they are all triangles (triangular polygons)
result += '<triangle>'
result += '<v1>' + (n) + '</v1>'
result += '<v2>' + (n + i + 1) + '</v2>'
result += '<v3>' + (n + i + 2) + '</v3>'
result += '</triangle>\n'
}
n += p.vertices.length
result += '</volume>\n'
})
result += '</mesh>\n</object>\n'
result += '</amf>\n'
return [result]
}
function CSGVectortoAMFString (v) {
return '<x>' + v._x + '</x><y>' + v._y + '</y><z>' + v._z + '</z>'
}
function CSGVertextoAMFString (vertex) {
return '<vertex><coordinates>' + CSGVectortoAMFString(vertex.pos) + '</coordinates></vertex>\n'
}
/*
CSG.Vector3D.prototype.toAMFString = function () {
return '<x>' + this._x + '</x><y>' + this._y + '</y><z>' + this._z + '</z>'
}
CSG.Vertex.prototype.toAMFString = function () {
return '<vertex><coordinates>' + this.pos.toAMFString() + '</coordinates></vertex>\n'
} */
module.exports = {
serialize,
mimeType
}

View File

@ -1,76 +0,0 @@
{
"_from": "@jscad/amf-serializer@^0.0.5",
"_id": "@jscad/amf-serializer@0.0.5",
"_inBundle": false,
"_integrity": "sha1-3A+Hx7LG15/F9rZnfwxoag0/Bg4=",
"_location": "/@jscad/amf-serializer",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "@jscad/amf-serializer@^0.0.5",
"name": "@jscad/amf-serializer",
"escapedName": "@jscad%2famf-serializer",
"scope": "@jscad",
"rawSpec": "^0.0.5",
"saveSpec": null,
"fetchSpec": "^0.0.5"
},
"_requiredBy": [
"/@jscad/io"
],
"_resolved": "https://registry.npmjs.org/@jscad/amf-serializer/-/amf-serializer-0.0.5.tgz",
"_shasum": "dc0f87c7b2c6d79fc5f6b6677f0c686a0d3f060e",
"_spec": "@jscad/amf-serializer@^0.0.5",
"_where": "C:\\Users\\tomate\\node_modules\\@jscad\\io",
"bugs": {
"url": "https://github.com/jscad/io/issues"
},
"bundleDependencies": false,
"contributors": [
{
"name": "Rene K. Mueller",
"url": "http://renekmueller.com"
},
{
"name": "z3dev",
"url": "http://www.z3d.jp"
},
{
"name": "Mark 'kaosat-dev' Moissette",
"url": "http://kaosat.net"
}
],
"dependencies": {
"@jscad/io-utils": "^0.1.2",
"sax": "^1.2.1",
"xmldom": "^0.1.27"
},
"deprecated": false,
"description": "Amf serializer for jscad project",
"devDependencies": {
"@jscad/csg": "0.3.6"
},
"homepage": "https://github.com/jscad/io#readme",
"keywords": [
"openjscad",
"jscad",
"csg",
"serializer",
"amf"
],
"license": "MIT",
"main": "index.js",
"name": "@jscad/amf-serializer",
"repository": {
"type": "git",
"url": "git+https://github.com/jscad/io.git"
},
"scripts": {
"release-major": "git checkout master && npm version major && git commit -a -m 'chore(dist): built dist/'; git push origin master --tags ",
"release-minor": "git checkout master && npm version minor && git commit -a -m 'chore(dist): built dist/'; git push origin master --tags ",
"release-patch": "git checkout master && npm version patch && git commit -a -m 'chore(dist): built dist/'; git push origin master --tags ",
"test": "ava './test.js' --verbose --timeout 10000"
},
"version": "0.0.5"
}

View File

@ -1,11 +0,0 @@
const test = require('ava')
const {CSG} = require('@jscad/csg')
const serializer = require('./index.js')
test('serialize csg to amf', function (t) {
const input = new CSG.cube()
const expected = [ '<?xml version="1.0" encoding="UTF-8"?>\n<amf>\n<object id="0">\n<mesh>\n<vertices>\n<vertex><coordinates><x>-1</x><y>-1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>-1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>-1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>-1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>-1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>-1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>-1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>-1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>-1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>-1</y><z>-1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>-1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>-1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>1</x><y>1</y><z>1</z></coordinates></vertex>\n<vertex><coordinates><x>-1</x><y>1</y><z>1</z></coordinates></vertex>\n</vertices>\n<volume>\n<triangle><v1>0</v1><v2>1</v2><v3>2</v3></triangle>\n<triangle><v1>0</v1><v2>2</v2><v3>3</v3></triangle>\n</volume>\n<volume>\n<triangle><v1>4</v1><v2>5</v2><v3>6</v3></triangle>\n<triangle><v1>4</v1><v2>6</v2><v3>7</v3></triangle>\n</volume>\n<volume>\n<triangle><v1>8</v1><v2>9</v2><v3>10</v3></triangle>\n<triangle><v1>8</v1><v2>10</v2><v3>11</v3></triangle>\n</volume>\n<volume>\n<triangle><v1>12</v1><v2>13</v2><v3>14</v3></triangle>\n<triangle><v1>12</v1><v2>14</v2><v3>15</v3></triangle>\n</volume>\n<volume>\n<triangle><v1>16</v1><v2>17</v2><v3>18</v3></triangle>\n<triangle><v1>16</v1><v2>18</v2><v3>19</v3></triangle>\n</volume>\n<volume>\n<triangle><v1>20</v1><v2>21</v2><v3>22</v3></triangle>\n<triangle><v1>20</v1><v2>22</v2><v3>23</v3></triangle>\n</volume>\n</mesh>\n</object>\n</amf>\n' ]
const observed = serializer.serialize(input)
t.deepEqual(observed, expected)
})

View File

@ -1,142 +0,0 @@
<a name="0.3.6"></a>
## [0.3.6](https://github.com/jscad/csg.js/compare/v0.3.5...v0.3.6) (2017-11-03)
<a name="0.3.5"></a>
## [0.3.5](https://github.com/jscad/csg.js/compare/v0.3.4...v0.3.5) (2017-11-03)
### Bug Fixes
* **fixTJunctions:** fixes issues with fixTJunctions ([#63](https://github.com/jscad/csg.js/issues/63)) ([78c5102](https://github.com/jscad/csg.js/commit/78c5102))
<a name="0.3.4"></a>
## [0.3.4](https://github.com/jscad/csg.js/compare/v0.3.3...v0.3.4) (2017-11-01)
### Bug Fixes
* **ConnectorsList:** add back missing ConnectorsList ([#62](https://github.com/jscad/csg.js/issues/62)) ([cfc1c7e](https://github.com/jscad/csg.js/commit/cfc1c7e))
<a name="0.3.3"></a>
## [0.3.3](https://github.com/jscad/csg.js/compare/v0.3.2...v0.3.3) (2017-11-01)
<a name="0.3.2"></a>
## [0.3.2](https://github.com/jscad/csg.js/compare/v0.3.1...v0.3.2) (2017-10-28)
### Bug Fixes
* **path2D:** added missing innerToCAG method to path2D proto & added tests & docs ([#52](https://github.com/jscad/csg.js/issues/52)) ([4a5e37e](https://github.com/jscad/csg.js/commit/4a5e37e))
<a name="0.3.1"></a>
## [0.3.1](https://github.com/jscad/csg.js/compare/v0.3.0...v0.3.1) (2017-06-11)
<a name="0.3.0"></a>
# [0.3.0](https://github.com/jscad/csg.js/compare/v0.2.4...v0.3.0) (2017-05-30)
### Features
* **CAG.toPoints:** CAG Enhancement for toPoints() ([d023243](https://github.com/jscad/csg.js/commit/d023243))
<a name="0.2.4"></a>
## [0.2.4](https://github.com/jscad/csg.js/compare/v0.2.3...v0.2.4) (2017-05-20)
### Bug Fixes
* **CAG:** reverted back CAG to default to canonicalized = false ([68a0558](https://github.com/jscad/csg.js/commit/68a0558))
<a name="0.2.3"></a>
## [0.2.3](https://github.com/jscad/csg.js/compare/v0.2.2...v0.2.3) (2017-05-19)
<a name="0.2.2"></a>
## [0.2.2](https://github.com/jscad/csg.js/compare/v0.2.1...v0.2.2) (2017-05-19)
<a name="0.2.1"></a>
## [0.2.1](https://github.com/jscad/csg.js/compare/v0.2.0...v0.2.1) (2017-04-27)
<a name="0.2.0"></a>
# [0.2.0](https://github.com/jscad/csg.js/compare/v0.1.4...v0.2.0) (2017-04-27)
### Bug Fixes
* **asserts:** fixed issue with asserts in latest ava ([1210f66](https://github.com/jscad/csg.js/commit/1210f66))
<a name="0.1.4"></a>
## [0.1.4](https://github.com/jscad/csg.js/compare/v0.1.3...v0.1.4) (2017-01-27)
### Bug Fixes
* **README:** more attempts at fixing README on npm ... ([a5ae096](https://github.com/jscad/csg.js/commit/a5ae096))
<a name="0.1.3"></a>
## [0.1.3](https://github.com/jscad/csg.js/compare/v0.1.2...v0.1.3) (2017-01-27)
<a name="0.1.2"></a>
## [0.1.2](https://github.com/jscad/csg.js/compare/v0.1.1...v0.1.2) (2017-01-27)
### Bug Fixes
* **typo:** fixed typo in package name ([4f2aec6](https://github.com/jscad/csg.js/commit/4f2aec6))
<a name="0.1.1"></a>
## [0.1.1](https://github.com/jscad/csg.js/compare/v0.1.0...v0.1.1) (2017-01-27)
### Bug Fixes
* **babelrc:** added missing babelrc ([94f9684](https://github.com/jscad/csg.js/commit/94f9684))
* **package:** removed part of package relevant to 'builds' ([7bf4815](https://github.com/jscad/csg.js/commit/7bf4815))
<a name="0.1.0"></a>
# [0.1.0](https://github.com/jscad/csg.js/compare/af6453c...v0.1.0) (2017-01-16)
### Bug Fixes
* bools fail if cylinder resolution not integer. Solution: parse all resolution as int ([af6453c](https://github.com/jscad/csg.js/commit/af6453c))
* **package:** fixed package name ([cbba148](https://github.com/jscad/csg.js/commit/cbba148))
### Features
* **csg.js:** updated csg.js based on recent changes in OpenjSCAD.org ([db1d133](https://github.com/jscad/csg.js/commit/db1d133))

View File

@ -1,73 +0,0 @@
## csg.js
## Constructive Solid Geometry Library - Contributing Guide
This library is part of the JSCAD Organization, and is maintained by a group of volunteers. We welcome and encourage anyone to pitch in but please take a moment to read the following guidelines.
* If you want to submit a bug report please make sure to follow the [Reporting Issues](https://github.com/jscad/csg.js/wiki/Reporting-Issues) guide. Bug reports are accepted as [Issues](https://github.com/jscad/csg.js/issues/) via GitHub.
* If you want to submit a change or a patch, please read the contents below on how to make changes. New contributions are accepted as [Pull Requests](https://github.com/jscad/csg.js/pulls/) via GithHub.
* We only accept bug reports and pull requests on **GitHub**.
* If you have a question about how to use CSG.js, then please start a conversation at the [OpenJSCAD.org User Group](https://plus.google.com/communities/114958480887231067224). You might find the answer in the [OpenJSCAD.org User Guide](https://github.com/Spiritdude/OpenJSCAD.org/wiki/User-Guide).
* If you have a change or new feature in mind, please start a conversation with the [Core Developers](https://plus.google.com/communities/114958480887231067224) and start contributing changes.
Thanks!
The JSCAD Organization
## Making Changes
First, we suggest that you fork this GIT repository. This will keep your changes separate from the fast lane. And make pull requests easier.
Once forked, clone your copy of the CSG library.
```
git clone https://github.com/myusername/csg.js.git
cd csg.js
```
**We suggest downloading NPM. This will allow you to generate API documents, and run test suites.**
Once you have NPM, install all the dependencies for development.
```
npm install
```
And, run the test cases to verify the library is functional.
```
npm test
```
And, create the API documentation.
```
npm run build-docs
```
The project is structured as:
- csg.js : library source code
- test/*.js : various test suites
- helpers/*.js : various helper classes for testing
- objects/* : objects created by test suites
- doc/* : generated API documentation
You can now make changes to the library, as well as the test suites.
If you intend to contribute changes back to JSCAD then please be sure to create test cases.
Done? Great! Push the changes back to the fork.
```
git commit changed-file
git add test/new-test-suite.js
git commit test/new-test-suite.js
git push
```
Finally, you can review your changes via GitHub, and create a pull request.
TIPS for successful pull requests:
- Commit often, and comment well
- Follow the [JavaScript Standard Style](https://github.com/feross/standard)
- Create test cases for all changes
- Verify that all tests suites pass
WOW! Thanks for the cool code.

View File

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2017 @jscad
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,67 +0,0 @@
## csg.js
## Constructive Solid Geometry (CSG) Library
[![GitHub version](https://badge.fury.io/gh/jscad%2Fcsg.js.svg)](https://badge.fury.io/gh/jscad%2Fcsg.js)
[![Build Status](https://travis-ci.org/jscad/csg.js.svg)](https://travis-ci.org/jscad/csg.js)
> Solid modelling library (2d & 3d)
## Overview
Constructive Solid Geometry (CSG) is a modelling technique that uses Boolean operations like union and intersection to combine 3D solids. This library implements CSG operations on meshes elegantly and concisely using BSP trees, and is meant to serve as an easily understandable implementation of the algorithm.
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [API](#api)
- [Contribute](#contribute)
- [License](#license)
## Installation
```
npm install @jscad/csg
```
## Usage
- as Node module :
```
const csg = require('@jscad/csg')
```
## API
The API documentation can be found [here](./docs/api.md).
Also see the [OpenJsCad User Guide](https://en.wikibooks.org/wiki/OpenJSCAD_User_Guide).
For questions about the API, please contact the [User Group](https://plus.google.com/communities/114958480887231067224)
## Contribute
This library is part of the JSCAD Organization, and is maintained by a group of volunteers. We welcome and encourage anyone to pitch in but please take a moment to read the following guidelines.
* If you want to submit a bug report please make sure to follow the [Reporting Issues](https://github.com/jscad/csg.js/wiki/Reporting-Issues) guide. Bug reports are accepted as [Issues](https://github.com/jscad/csg.js/issues/) via GitHub.
* If you want to submit a change or a patch, please see the [Contributing guidelines](https://github.com/jscad/csg.js/blob/master/CONTRIBUTING.md). New contributions are accepted as [Pull Requests](https://github.com/jscad/csg.js/pulls/) via GithHub.
* We only accept bug reports and pull requests on **GitHub**.
* If you have a question about how to use CSG.js, then please start a conversation at the [OpenJSCAD.org User Group](https://plus.google.com/communities/114958480887231067224). You might find the answer in the [OpenJSCAD.org User Guide](https://github.com/Spiritdude/OpenJSCAD.org/wiki/User-Guide).
* If you have a change or new feature in mind, please start a conversation with the [Core Developers](https://plus.google.com/communities/114958480887231067224) and start contributing changes.
Small Note: If editing this README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
## Copyrights
Some copyrights apply. Copyright (c) 2012 Joost Nieuwenhuijse (joost@newhouse.nl), under the MIT license. Copyright (c) 2011 Evan Wallace (http://madebyevan.com/)
## License
[The MIT License (MIT)](https://github.com/jscad/csg.js/blob/master/LICENSE)
(unless specified otherwise)

View File

@ -1,189 +0,0 @@
/*
## License
Copyright (c) 2014 bebbi (elghatta@gmail.com)
Copyright (c) 2013 Eduard Bespalov (edwbes@gmail.com)
Copyright (c) 2012 Joost Nieuwenhuijse (joost@newhouse.nl)
Copyright (c) 2011 Evan Wallace (http://evanw.github.com/csg.js/)
Copyright (c) 2012 Alexandre Girard (https://github.com/alx)
All code released under MIT license
## Overview
For an overview of the CSG process see the original csg.js code:
http://evanw.github.com/csg.js/
CSG operations through BSP trees suffer from one problem: heavy fragmentation
of polygons. If two CSG solids of n polygons are unified, the resulting solid may have
in the order of n*n polygons, because each polygon is split by the planes of all other
polygons. After a few operations the number of polygons explodes.
This version of CSG.js solves the problem in 3 ways:
1. Every polygon split is recorded in a tree (CSG.PolygonTreeNode). This is a separate
tree, not to be confused with the CSG tree. If a polygon is split into two parts but in
the end both fragments have not been discarded by the CSG operation, we can retrieve
the original unsplit polygon from the tree, instead of the two fragments.
This does not completely solve the issue though: if a polygon is split multiple times
the number of fragments depends on the order of subsequent splits, and we might still
end up with unncessary splits:
Suppose a polygon is first split into A and B, and then into A1, B1, A2, B2. Suppose B2 is
discarded. We will end up with 2 polygons: A and B1. Depending on the actual split boundaries
we could still have joined A and B1 into one polygon. Therefore a second approach is used as well:
2. After CSG operations all coplanar polygon fragments are joined by a retesselating
operation. See CSG.reTesselated(). Retesselation is done through a
linear sweep over the polygon surface. The sweep line passes over the y coordinates
of all vertices in the polygon. Polygons are split at each sweep line, and the fragments
are joined horizontally and vertically into larger polygons (making sure that we
will end up with convex polygons).
This still doesn't solve the problem completely: due to floating point imprecisions
we may end up with small gaps between polygons, and polygons may not be exactly coplanar
anymore, and as a result the retesselation algorithm may fail to join those polygons.
Therefore:
3. A canonicalization algorithm is implemented: it looks for vertices that have
approximately the same coordinates (with a certain tolerance, say 1e-5) and replaces
them with the same vertex. If polygons share a vertex they will actually point to the
same CSG.Vertex instance. The same is done for polygon planes. See CSG.canonicalized().
Performance improvements to the original CSG.js:
Replaced the flip() and invert() methods by flipped() and inverted() which don't
modify the source object. This allows to get rid of all clone() calls, so that
multiple polygons can refer to the same CSG.Plane instance etc.
The original union() used an extra invert(), clipTo(), invert() sequence just to remove the
coplanar front faces from b; this is now combined in a single b.clipTo(a, true) call.
Detection whether a polygon is in front or in back of a plane: for each polygon
we are caching the coordinates of the bounding sphere. If the bounding sphere is
in front or in back of the plane we don't have to check the individual vertices
anymore.
Other additions to the original CSG.js:
CSG.Vector class has been renamed into CSG.Vector3D
Classes for 3D lines, 2D vectors, 2D lines, and methods to find the intersection of
a line and a plane etc.
Transformations: CSG.transform(), CSG.translate(), CSG.rotate(), CSG.scale()
Expanding or contracting a solid: CSG.expand() and CSG.contract(). Creates nice
smooth corners.
The vertex normal has been removed since it complicates retesselation. It's not needed
for solid CAD anyway.
*/
const {addTransformationMethodsToPrototype, addCenteringToPrototype} = require('./src/mutators')
let CSG = require('./src/CSG')
let CAG = require('./src/CAG')
// FIXME: how many are actual usefull to be exposed as API ?? looks like a code smell
const { _CSGDEBUG,
defaultResolution2D,
defaultResolution3D,
EPS,
angleEPS,
areaEPS,
all,
top,
bottom,
left,
right,
front,
back,
staticTag,
getTag} = require('./src/constants')
CSG._CSGDEBUG = _CSGDEBUG
CSG.defaultResolution2D = defaultResolution2D
CSG.defaultResolution3D = defaultResolution3D
CSG.EPS = EPS
CSG.angleEPS = angleEPS
CSG.areaEPS = areaEPS
CSG.all = all
CSG.top = top
CSG.bottom = bottom
CSG.left = left
CSG.right = right
CSG.front = front
CSG.back = back
CSG.staticTag = staticTag
CSG.getTag = getTag
// eek ! all this is kept for backwards compatibility...for now
CSG.Vector2D = require('./src/math/Vector2')
CSG.Vector3D = require('./src/math/Vector3')
CSG.Vertex = require('./src/math/Vertex3')
CAG.Vertex = require('./src/math/Vertex2')
CSG.Plane = require('./src/math/Plane')
CSG.Polygon = require('./src/math/Polygon3')
CSG.Polygon2D = require('./src/math/Polygon2')
CSG.Line2D = require('./src/math/Line2')
CSG.Line3D = require('./src/math/Line3')
CSG.Path2D = require('./src/math/Path2')
CSG.OrthoNormalBasis = require('./src/math/OrthoNormalBasis')
CSG.Matrix4x4 = require('./src/math/Matrix4')
CAG.Side = require('./src/math/Side')
CSG.Connector = require('./src/connectors').Connector
CSG.ConnectorList = require('./src/connectors').ConnectorList
CSG.Properties = require('./src/Properties')
const {circle, ellipse, rectangle, roundedRectangle} = require('./src/primitives2d')
const {sphere, cube, roundedCube, cylinder, roundedCylinder, cylinderElliptic, polyhedron} = require('./src/primitives3d')
CSG.sphere = sphere
CSG.cube = cube
CSG.roundedCube = roundedCube
CSG.cylinder = cylinder
CSG.roundedCylinder = roundedCylinder
CSG.cylinderElliptic = cylinderElliptic
CSG.polyhedron = polyhedron
CAG.circle = circle
CAG.ellipse = ellipse
CAG.rectangle = rectangle
CAG.roundedRectangle = roundedRectangle
//
const {fromCompactBinary, fromObject, fromSlices} = require('./src/CSGFactories')
CSG.fromCompactBinary = fromCompactBinary
CSG.fromObject = fromObject
CSG.fromSlices = fromSlices
CSG.toPointCloud = require('./src/debugHelpers').toPointCloud
const CAGMakers = require('./src/CAGFactories')
CAG.fromObject = CAGMakers.fromObject
CAG.fromPointsNoCheck = CAGMakers.fromPointsNoCheck
CAG.fromPath2 = CAGMakers.fromPath2
// ////////////////////////////////////
addTransformationMethodsToPrototype(CSG.prototype)
addTransformationMethodsToPrototype(CSG.Vector2D.prototype)
addTransformationMethodsToPrototype(CSG.Vector3D.prototype)
addTransformationMethodsToPrototype(CSG.Vertex.prototype)
addTransformationMethodsToPrototype(CSG.Plane.prototype)
addTransformationMethodsToPrototype(CSG.Polygon.prototype)
addTransformationMethodsToPrototype(CSG.Line2D.prototype)
addTransformationMethodsToPrototype(CSG.Line3D.prototype)
addTransformationMethodsToPrototype(CSG.Path2D.prototype)
addTransformationMethodsToPrototype(CSG.OrthoNormalBasis.prototype)
addTransformationMethodsToPrototype(CSG.Connector.prototype)
addTransformationMethodsToPrototype(CAG.prototype)
addTransformationMethodsToPrototype(CAG.Side.prototype)
addTransformationMethodsToPrototype(CAG.Vertex.prototype)
addCenteringToPrototype(CSG.prototype, ['x', 'y', 'z'])
addCenteringToPrototype(CAG.prototype, ['x', 'y'])
module.exports = {CSG, CAG}

File diff suppressed because it is too large Load Diff

View File

@ -1,108 +0,0 @@
{
"_from": "@jscad/csg@0.3.6",
"_id": "@jscad/csg@0.3.6",
"_inBundle": false,
"_integrity": "sha512-GoUXhTwO0L+Cxba8VfFFLGm+ECd0xZKoIAPpeUYsDiCVsIZ6XK+3GLgBgn5waeumQNh+H6WsHjMMRKXBrDBreA==",
"_location": "/@jscad/csg",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "@jscad/csg@0.3.6",
"name": "@jscad/csg",
"escapedName": "@jscad%2fcsg",
"scope": "@jscad",
"rawSpec": "0.3.6",
"saveSpec": null,
"fetchSpec": "0.3.6"
},
"_requiredBy": [
"/@jscad/json-deserializer",
"/@jscad/json-serializer",
"/@jscad/openjscad",
"/@jscad/stl-deserializer",
"/@jscad/svg-deserializer",
"/@jscad/svg-serializer"
],
"_resolved": "https://registry.npmjs.org/@jscad/csg/-/csg-0.3.6.tgz",
"_shasum": "4fc13667c4130b3010328d7587fe5d9328712ced",
"_spec": "@jscad/csg@0.3.6",
"_where": "C:\\Users\\tomate\\node_modules\\@jscad\\openjscad",
"ava": {
"require": [
"babel-register"
]
},
"bugs": {
"url": "https://github.com/jscad/csg.js/issues"
},
"bundleDependencies": false,
"contributors": [
{
"name": "Alexandre Girard",
"url": "https://github.com/alx"
},
{
"name": "Evan Wallace",
"url": "http://evanw.github.com/csg.js/"
},
{
"name": "Joost Nieuwenhuijse",
"email": "joost@newhouse.nl"
},
{
"name": "Eduard Bespalov",
"url": "http://evanw.github.com/csg.js/"
},
{
"name": "bebbi",
"email": "elghatta@gmail.com"
},
{
"name": "Spiritdude Rene K Mueller",
"url": "http://renekmueller.com"
},
{
"name": "Jeff Gay",
"url": "http://www.z3d.jp"
}
],
"dependencies": {},
"deprecated": false,
"description": "Constructive Solid Geometry (CSG) Library",
"devDependencies": {
"ava": "^0.23.0",
"conventional-changelog-cli": "^1.3.4",
"jsdoc": "^3.4.3",
"jsdoc-to-markdown": "^3.0.0",
"nyc": "^10.3.2"
},
"homepage": "https://github.com/jscad/csg.js#readme",
"keywords": [
"csg",
"parametric",
"modeling",
"openjscad",
"jscad"
],
"license": "MIT",
"main": "csg.js",
"name": "@jscad/csg",
"repository": {
"type": "git",
"url": "git+https://github.com/jscad/csg.js.git"
},
"scripts": {
"build-docs": "jsdoc -c jsdoc.json",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"docs": "jsdoc2md --files 'src/**/*.js' > docs/api.md",
"postversion": "git push origin master && git push origin master --tags",
"preversion": "npm test",
"release-major": "git checkout master && git pull origin master && npm version major",
"release-minor": "git checkout master && git pull origin master && npm version minor",
"release-patch": "git checkout master && git pull origin master && npm version patch",
"test": "nyc ava 'test' --concurrency 3 --verbose --timeout 40000",
"version": "npm run changelog && npm run docs && git add -A "
},
"version": "0.3.6"
}

View File

@ -1,816 +0,0 @@
const {EPS, angleEPS, areaEPS, defaultResolution3D} = require('./constants')
const {Connector} = require('./connectors')
const OrthoNormalBasis = require('./math/OrthoNormalBasis')
const Vertex2D = require('./math/Vertex2')
const Vertex3D = require('./math/Vertex3')
const Vector2D = require('./math/Vector2')
const Vector3D = require('./math/Vector3')
const Polygon = require('./math/Polygon3')
const Path2D = require('./math/Path2')
const Side = require('./math/Side')
const {linesIntersect} = require('./math/lineUtils')
const {parseOptionAs3DVector, parseOptionAsBool, parseOptionAsFloat, parseOptionAsInt} = require('./optionParsers')
const FuzzyCAGFactory = require('./FuzzyFactory2d')
/**
* Class CAG
* Holds a solid area geometry like CSG but 2D.
* Each area consists of a number of sides.
* Each side is a line between 2 points.
* @constructor
*/
let CAG = function () {
this.sides = []
this.isCanonicalized = false
}
/** Construct a CAG from a list of `Side` instances.
* @param {Side[]} sides - list of sides
* @returns {CAG} new CAG object
*/
CAG.fromSides = function (sides) {
let cag = new CAG()
cag.sides = sides
return cag
}
// Converts a CSG to a The CSG must consist of polygons with only z coordinates +1 and -1
// as constructed by _toCSGWall(-1, 1). This is so we can use the 3D union(), intersect() etc
CAG.fromFakeCSG = function (csg) {
let sides = csg.polygons.map(function (p) {
return Side._fromFakePolygon(p)
})
.filter(function (s) {
return s !== null
})
return CAG.fromSides(sides)
}
/** Construct a CAG from a list of points (a polygon).
* The rotation direction of the points is not relevant.
* The points can define a convex or a concave polygon.
* The polygon must not self intersect.
* @param {points[]} points - list of points in 2D space
* @returns {CAG} new CAG object
*/
CAG.fromPoints = function (points) {
let numpoints = points.length
if (numpoints < 3) throw new Error('CAG shape needs at least 3 points')
let sides = []
let prevpoint = new Vector2D(points[numpoints - 1])
let prevvertex = new Vertex2D(prevpoint)
points.map(function (p) {
let point = new Vector2D(p)
let vertex = new Vertex2D(point)
let side = new Side(prevvertex, vertex)
sides.push(side)
prevvertex = vertex
})
let result = CAG.fromSides(sides)
if (result.isSelfIntersecting()) {
throw new Error('Polygon is self intersecting!')
}
let area = result.area()
if (Math.abs(area) < areaEPS) {
throw new Error('Degenerate polygon!')
}
if (area < 0) {
result = result.flipped()
}
result = result.canonicalized()
return result
}
const CAGFromCAGFuzzyFactory = function (factory, sourcecag) {
let _this = factory
let newsides = sourcecag.sides.map(function (side) {
return _this.getSide(side)
})
// remove bad sides (mostly a user input issue)
.filter(function (side) {
return side.length() > EPS
})
return CAG.fromSides(newsides)
}
CAG.prototype = {
toString: function () {
let result = 'CAG (' + this.sides.length + ' sides):\n'
this.sides.map(function (side) {
result += ' ' + side.toString() + '\n'
})
return result
},
_toCSGWall: function (z0, z1) {
const CSG = require('./CSG') // FIXME: circular dependencies CAG=>CSG=>CAG
let polygons = this.sides.map(function (side) {
return side.toPolygon3D(z0, z1)
})
return CSG.fromPolygons(polygons)
},
_toVector3DPairs: function (m) {
// transform m
let pairs = this.sides.map(function (side) {
let p0 = side.vertex0.pos
let p1 = side.vertex1.pos
return [Vector3D.Create(p0.x, p0.y, 0),
Vector3D.Create(p1.x, p1.y, 0)]
})
if (typeof m !== 'undefined') {
pairs = pairs.map(function (pair) {
return pair.map(function (v) {
return v.transform(m)
})
})
}
return pairs
},
/*
* transform a cag into the polygons of a corresponding 3d plane, positioned per options
* Accepts a connector for plane positioning, or optionally
* single translation, axisVector, normalVector arguments
* (toConnector has precedence over single arguments if provided)
*/
_toPlanePolygons: function (options) {
const CSG = require('./CSG') // FIXME: circular dependencies CAG=>CSG=>CAG
let flipped = options.flipped || false
// reference connector for transformation
let origin = [0, 0, 0]
let defaultAxis = [0, 0, 1]
let defaultNormal = [0, 1, 0]
let thisConnector = new Connector(origin, defaultAxis, defaultNormal)
// translated connector per options
let translation = options.translation || origin
let axisVector = options.axisVector || defaultAxis
let normalVector = options.normalVector || defaultNormal
// will override above if options has toConnector
let toConnector = options.toConnector ||
new Connector(translation, axisVector, normalVector)
// resulting transform
let m = thisConnector.getTransformationTo(toConnector, false, 0)
// create plane as a (partial non-closed) CSG in XY plane
let bounds = this.getBounds()
bounds[0] = bounds[0].minus(new Vector2D(1, 1))
bounds[1] = bounds[1].plus(new Vector2D(1, 1))
let csgshell = this._toCSGWall(-1, 1)
let csgplane = CSG.fromPolygons([new Polygon([
new Vertex3D(new Vector3D(bounds[0].x, bounds[0].y, 0)),
new Vertex3D(new Vector3D(bounds[1].x, bounds[0].y, 0)),
new Vertex3D(new Vector3D(bounds[1].x, bounds[1].y, 0)),
new Vertex3D(new Vector3D(bounds[0].x, bounds[1].y, 0))
])])
if (flipped) {
csgplane = csgplane.invert()
}
// intersectSub -> prevent premature retesselate/canonicalize
csgplane = csgplane.intersectSub(csgshell)
// only keep the polygons in the z plane:
let polys = csgplane.polygons.filter(function (polygon) {
return Math.abs(polygon.plane.normal.z) > 0.99
})
// finally, position the plane per passed transformations
return polys.map(function (poly) {
return poly.transform(m)
})
},
/*
* given 2 connectors, this returns all polygons of a "wall" between 2
* copies of this cag, positioned in 3d space as "bottom" and
* "top" plane per connectors toConnector1, and toConnector2, respectively
*/
_toWallPolygons: function (options) {
// normals are going to be correct as long as toConn2.point - toConn1.point
// points into cag normal direction (check in caller)
// arguments: options.toConnector1, options.toConnector2, options.cag
// walls go from toConnector1 to toConnector2
// optionally, target cag to point to - cag needs to have same number of sides as this!
let origin = [0, 0, 0]
let defaultAxis = [0, 0, 1]
let defaultNormal = [0, 1, 0]
let thisConnector = new Connector(origin, defaultAxis, defaultNormal)
// arguments:
let toConnector1 = options.toConnector1
// let toConnector2 = new Connector([0, 0, -30], defaultAxis, defaultNormal);
let toConnector2 = options.toConnector2
if (!(toConnector1 instanceof Connector && toConnector2 instanceof Connector)) {
throw new Error('could not parse Connector arguments toConnector1 or toConnector2')
}
if (options.cag) {
if (options.cag.sides.length !== this.sides.length) {
throw new Error('target cag needs same sides count as start cag')
}
}
// target cag is same as this unless specified
let toCag = options.cag || this
let m1 = thisConnector.getTransformationTo(toConnector1, false, 0)
let m2 = thisConnector.getTransformationTo(toConnector2, false, 0)
let vps1 = this._toVector3DPairs(m1)
let vps2 = toCag._toVector3DPairs(m2)
let polygons = []
vps1.forEach(function (vp1, i) {
polygons.push(new Polygon([
new Vertex3D(vps2[i][1]), new Vertex3D(vps2[i][0]), new Vertex3D(vp1[0])]))
polygons.push(new Polygon([
new Vertex3D(vps2[i][1]), new Vertex3D(vp1[0]), new Vertex3D(vp1[1])]))
})
return polygons
},
/**
* Convert to a list of points.
* @return {points[]} list of points in 2D space
*/
toPoints: function () {
let points = this.sides.map(function (side) {
let v0 = side.vertex0
// let v1 = side.vertex1
return v0.pos
})
// due to the logic of CAG.fromPoints()
// move the first point to the last
if (points.length > 0) {
points.push(points.shift())
}
return points
},
union: function (cag) {
let cags
if (cag instanceof Array) {
cags = cag
} else {
cags = [cag]
}
let r = this._toCSGWall(-1, 1)
r = r.union(
cags.map(function (cag) {
return cag._toCSGWall(-1, 1).reTesselated()
}), false, false)
return CAG.fromFakeCSG(r).canonicalized()
},
subtract: function (cag) {
let cags
if (cag instanceof Array) {
cags = cag
} else {
cags = [cag]
}
let r = this._toCSGWall(-1, 1)
cags.map(function (cag) {
r = r.subtractSub(cag._toCSGWall(-1, 1), false, false)
})
r = r.reTesselated()
r = r.canonicalized()
r = CAG.fromFakeCSG(r)
r = r.canonicalized()
return r
},
intersect: function (cag) {
let cags
if (cag instanceof Array) {
cags = cag
} else {
cags = [cag]
}
let r = this._toCSGWall(-1, 1)
cags.map(function (cag) {
r = r.intersectSub(cag._toCSGWall(-1, 1), false, false)
})
r = r.reTesselated()
r = r.canonicalized()
r = CAG.fromFakeCSG(r)
r = r.canonicalized()
return r
},
transform: function (matrix4x4) {
let ismirror = matrix4x4.isMirroring()
let newsides = this.sides.map(function (side) {
return side.transform(matrix4x4)
})
let result = CAG.fromSides(newsides)
if (ismirror) {
result = result.flipped()
}
return result
},
// see http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/ :
// Area of the polygon. For a counter clockwise rotating polygon the area is positive, otherwise negative
// Note(bebbi): this looks wrong. See polygon getArea()
area: function () {
let polygonArea = 0
this.sides.map(function (side) {
polygonArea += side.vertex0.pos.cross(side.vertex1.pos)
})
polygonArea *= 0.5
return polygonArea
},
flipped: function () {
let newsides = this.sides.map(function (side) {
return side.flipped()
})
newsides.reverse()
return CAG.fromSides(newsides)
},
getBounds: function () {
let minpoint
if (this.sides.length === 0) {
minpoint = new Vector2D(0, 0)
} else {
minpoint = this.sides[0].vertex0.pos
}
let maxpoint = minpoint
this.sides.map(function (side) {
minpoint = minpoint.min(side.vertex0.pos)
minpoint = minpoint.min(side.vertex1.pos)
maxpoint = maxpoint.max(side.vertex0.pos)
maxpoint = maxpoint.max(side.vertex1.pos)
})
return [minpoint, maxpoint]
},
isSelfIntersecting: function (debug) {
let numsides = this.sides.length
for (let i = 0; i < numsides; i++) {
let side0 = this.sides[i]
for (let ii = i + 1; ii < numsides; ii++) {
let side1 = this.sides[ii]
if (linesIntersect(side0.vertex0.pos, side0.vertex1.pos, side1.vertex0.pos, side1.vertex1.pos)) {
if (debug) { console.log('side ' + i + ': ' + side0); console.log('side ' + ii + ': ' + side1) }
return true
}
}
}
return false
},
expandedShell: function (radius, resolution) {
resolution = resolution || 8
if (resolution < 4) resolution = 4
let cags = []
let pointmap = {}
let cag = this.canonicalized()
cag.sides.map(function (side) {
let d = side.vertex1.pos.minus(side.vertex0.pos)
let dl = d.length()
if (dl > EPS) {
d = d.times(1.0 / dl)
let normal = d.normal().times(radius)
let shellpoints = [
side.vertex1.pos.plus(normal),
side.vertex1.pos.minus(normal),
side.vertex0.pos.minus(normal),
side.vertex0.pos.plus(normal)
]
// let newcag = CAG.fromPointsNoCheck(shellpoints);
let newcag = CAG.fromPoints(shellpoints)
cags.push(newcag)
for (let step = 0; step < 2; step++) {
let p1 = (step === 0) ? side.vertex0.pos : side.vertex1.pos
let p2 = (step === 0) ? side.vertex1.pos : side.vertex0.pos
let tag = p1.x + ' ' + p1.y
if (!(tag in pointmap)) {
pointmap[tag] = []
}
pointmap[tag].push({
'p1': p1,
'p2': p2
})
}
}
})
for (let tag in pointmap) {
let m = pointmap[tag]
let angle1, angle2
let pcenter = m[0].p1
if (m.length === 2) {
let end1 = m[0].p2
let end2 = m[1].p2
angle1 = end1.minus(pcenter).angleDegrees()
angle2 = end2.minus(pcenter).angleDegrees()
if (angle2 < angle1) angle2 += 360
if (angle2 >= (angle1 + 360)) angle2 -= 360
if (angle2 < angle1 + 180) {
let t = angle2
angle2 = angle1 + 360
angle1 = t
}
angle1 += 90
angle2 -= 90
} else {
angle1 = 0
angle2 = 360
}
let fullcircle = (angle2 > angle1 + 359.999)
if (fullcircle) {
angle1 = 0
angle2 = 360
}
if (angle2 > (angle1 + angleEPS)) {
let points = []
if (!fullcircle) {
points.push(pcenter)
}
let numsteps = Math.round(resolution * (angle2 - angle1) / 360)
if (numsteps < 1) numsteps = 1
for (let step = 0; step <= numsteps; step++) {
let angle = angle1 + step / numsteps * (angle2 - angle1)
if (step === numsteps) angle = angle2 // prevent rounding errors
let point = pcenter.plus(Vector2D.fromAngleDegrees(angle).times(radius))
if ((!fullcircle) || (step > 0)) {
points.push(point)
}
}
let newcag = CAG.fromPointsNoCheck(points)
cags.push(newcag)
}
}
let result = new CAG()
result = result.union(cags)
return result
},
expand: function (radius, resolution) {
let result = this.union(this.expandedShell(radius, resolution))
return result
},
contract: function (radius, resolution) {
let result = this.subtract(this.expandedShell(radius, resolution))
return result
},
// extrude the CAG in a certain plane.
// Giving just a plane is not enough, multiple different extrusions in the same plane would be possible
// by rotating around the plane's origin. An additional right-hand vector should be specified as well,
// and this is exactly a OrthoNormalBasis.
//
// orthonormalbasis: characterizes the plane in which to extrude
// depth: thickness of the extruded shape. Extrusion is done upwards from the plane
// (unless symmetrical option is set, see below)
// options:
// {symmetrical: true} // extrude symmetrically in two directions about the plane
extrudeInOrthonormalBasis: function (orthonormalbasis, depth, options) {
// first extrude in the regular Z plane:
if (!(orthonormalbasis instanceof OrthoNormalBasis)) {
throw new Error('extrudeInPlane: the first parameter should be a OrthoNormalBasis')
}
let extruded = this.extrude({
offset: [0, 0, depth]
})
if (parseOptionAsBool(options, 'symmetrical', false)) {
extruded = extruded.translate([0, 0, -depth / 2])
}
let matrix = orthonormalbasis.getInverseProjectionMatrix()
extruded = extruded.transform(matrix)
return extruded
},
// Extrude in a standard cartesian plane, specified by two axis identifiers. Each identifier can be
// one of ["X","Y","Z","-X","-Y","-Z"]
// The 2d x axis will map to the first given 3D axis, the 2d y axis will map to the second.
// See OrthoNormalBasis.GetCartesian for details.
// options:
// {symmetrical: true} // extrude symmetrically in two directions about the plane
extrudeInPlane: function (axis1, axis2, depth, options) {
return this.extrudeInOrthonormalBasis(OrthoNormalBasis.GetCartesian(axis1, axis2), depth, options)
},
// extruded=cag.extrude({offset: [0,0,10], twistangle: 360, twiststeps: 100});
// linear extrusion of 2D shape, with optional twist
// The 2d shape is placed in in z=0 plane and extruded into direction <offset> (a Vector3D)
// The final face is rotated <twistangle> degrees. Rotation is done around the origin of the 2d shape (i.e. x=0, y=0)
// twiststeps determines the resolution of the twist (should be >= 1)
// returns a CSG object
extrude: function (options) {
const CSG = require('./CSG') // FIXME: circular dependencies CAG=>CSG=>CAG
if (this.sides.length === 0) {
// empty!
return new CSG()
}
let offsetVector = parseOptionAs3DVector(options, 'offset', [0, 0, 1])
let twistangle = parseOptionAsFloat(options, 'twistangle', 0)
let twiststeps = parseOptionAsInt(options, 'twiststeps', defaultResolution3D)
if (offsetVector.z === 0) {
throw new Error('offset cannot be orthogonal to Z axis')
}
if (twistangle === 0 || twiststeps < 1) {
twiststeps = 1
}
let normalVector = Vector3D.Create(0, 1, 0)
let polygons = []
// bottom and top
polygons = polygons.concat(this._toPlanePolygons({
translation: [0, 0, 0],
normalVector: normalVector,
flipped: !(offsetVector.z < 0)}
))
polygons = polygons.concat(this._toPlanePolygons({
translation: offsetVector,
normalVector: normalVector.rotateZ(twistangle),
flipped: offsetVector.z < 0}))
// walls
for (let i = 0; i < twiststeps; i++) {
let c1 = new Connector(offsetVector.times(i / twiststeps), [0, 0, offsetVector.z],
normalVector.rotateZ(i * twistangle / twiststeps))
let c2 = new Connector(offsetVector.times((i + 1) / twiststeps), [0, 0, offsetVector.z],
normalVector.rotateZ((i + 1) * twistangle / twiststeps))
polygons = polygons.concat(this._toWallPolygons({toConnector1: c1, toConnector2: c2}))
}
return CSG.fromPolygons(polygons)
},
/** Extrude to into a 3D solid by rotating the origin around the Y axis.
* (and turning everything into XY plane)
* @param {Object} options - options for construction
* @param {Number} [options.angle=360] - angle of rotation
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*/
rotateExtrude: function (options) { // FIXME options should be optional
const CSG = require('./CSG') // FIXME: circular dependencies CAG=>CSG=>CAG
let alpha = parseOptionAsFloat(options, 'angle', 360)
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
alpha = alpha > 360 ? alpha % 360 : alpha
let origin = [0, 0, 0]
let axisV = Vector3D.Create(0, 1, 0)
let normalV = [0, 0, 1]
let polygons = []
// planes only needed if alpha > 0
let connS = new Connector(origin, axisV, normalV)
if (alpha > 0 && alpha < 360) {
// we need to rotate negative to satisfy wall function condition of
// building in the direction of axis vector
let connE = new Connector(origin, axisV.rotateZ(-alpha), normalV)
polygons = polygons.concat(
this._toPlanePolygons({toConnector: connS, flipped: true}))
polygons = polygons.concat(
this._toPlanePolygons({toConnector: connE}))
}
let connT1 = connS
let connT2
let step = alpha / resolution
for (let a = step; a <= alpha + EPS; a += step) { // FIXME Should this be angelEPS?
connT2 = new Connector(origin, axisV.rotateZ(-a), normalV)
polygons = polygons.concat(this._toWallPolygons(
{toConnector1: connT1, toConnector2: connT2}))
connT1 = connT2
}
return CSG.fromPolygons(polygons).reTesselated()
},
// check if we are a valid CAG (for debugging)
// NOTE(bebbi) uneven side count doesn't work because rounding with EPS isn't taken into account
check: function () {
let errors = []
if (this.isSelfIntersecting(true)) {
errors.push('Self intersects')
}
let pointcount = {}
this.sides.map(function (side) {
function mappoint (p) {
let tag = p.x + ' ' + p.y
if (!(tag in pointcount)) pointcount[tag] = 0
pointcount[tag] ++
}
mappoint(side.vertex0.pos)
mappoint(side.vertex1.pos)
})
for (let tag in pointcount) {
let count = pointcount[tag]
if (count & 1) {
errors.push('Uneven number of sides (' + count + ') for point ' + tag)
}
}
let area = this.area()
if (area < areaEPS) {
errors.push('Area is ' + area)
}
if (errors.length > 0) {
let ertxt = ''
errors.map(function (err) {
ertxt += err + '\n'
})
throw new Error(ertxt)
}
},
canonicalized: function () {
if (this.isCanonicalized) {
return this
} else {
let factory = new FuzzyCAGFactory()
let result = CAGFromCAGFuzzyFactory(factory, this)
result.isCanonicalized = true
return result
}
},
/** Convert to compact binary form.
* See CAG.fromCompactBinary.
* @return {CompactBinary}
*/
toCompactBinary: function () {
let cag = this.canonicalized()
let numsides = cag.sides.length
let vertexmap = {}
let vertices = []
let numvertices = 0
let sideVertexIndices = new Uint32Array(2 * numsides)
let sidevertexindicesindex = 0
cag.sides.map(function (side) {
[side.vertex0, side.vertex1].map(function (v) {
let vertextag = v.getTag()
let vertexindex
if (!(vertextag in vertexmap)) {
vertexindex = numvertices++
vertexmap[vertextag] = vertexindex
vertices.push(v)
} else {
vertexindex = vertexmap[vertextag]
}
sideVertexIndices[sidevertexindicesindex++] = vertexindex
})
})
let vertexData = new Float64Array(numvertices * 2)
let verticesArrayIndex = 0
vertices.map(function (v) {
let pos = v.pos
vertexData[verticesArrayIndex++] = pos._x
vertexData[verticesArrayIndex++] = pos._y
})
let result = {
'class': 'CAG',
sideVertexIndices: sideVertexIndices,
vertexData: vertexData
}
return result
},
getOutlinePaths: function () {
let cag = this.canonicalized()
let sideTagToSideMap = {}
let startVertexTagToSideTagMap = {}
cag.sides.map(function (side) {
let sidetag = side.getTag()
sideTagToSideMap[sidetag] = side
let startvertextag = side.vertex0.getTag()
if (!(startvertextag in startVertexTagToSideTagMap)) {
startVertexTagToSideTagMap[startvertextag] = []
}
startVertexTagToSideTagMap[startvertextag].push(sidetag)
})
let paths = []
while (true) {
let startsidetag = null
for (let aVertexTag in startVertexTagToSideTagMap) {
let sidesForThisVertex = startVertexTagToSideTagMap[aVertexTag]
startsidetag = sidesForThisVertex[0]
sidesForThisVertex.splice(0, 1)
if (sidesForThisVertex.length === 0) {
delete startVertexTagToSideTagMap[aVertexTag]
}
break
}
if (startsidetag === null) break // we've had all sides
let connectedVertexPoints = []
let sidetag = startsidetag
let thisside = sideTagToSideMap[sidetag]
let startvertextag = thisside.vertex0.getTag()
while (true) {
connectedVertexPoints.push(thisside.vertex0.pos)
let nextvertextag = thisside.vertex1.getTag()
if (nextvertextag === startvertextag) break // we've closed the polygon
if (!(nextvertextag in startVertexTagToSideTagMap)) {
throw new Error('Area is not closed!')
}
let nextpossiblesidetags = startVertexTagToSideTagMap[nextvertextag]
let nextsideindex = -1
if (nextpossiblesidetags.length === 1) {
nextsideindex = 0
} else {
// more than one side starting at the same vertex. This means we have
// two shapes touching at the same corner
let bestangle = null
let thisangle = thisside.direction().angleDegrees()
for (let sideindex = 0; sideindex < nextpossiblesidetags.length; sideindex++) {
let nextpossiblesidetag = nextpossiblesidetags[sideindex]
let possibleside = sideTagToSideMap[nextpossiblesidetag]
let angle = possibleside.direction().angleDegrees()
let angledif = angle - thisangle
if (angledif < -180) angledif += 360
if (angledif >= 180) angledif -= 360
if ((nextsideindex < 0) || (angledif > bestangle)) {
nextsideindex = sideindex
bestangle = angledif
}
}
}
let nextsidetag = nextpossiblesidetags[nextsideindex]
nextpossiblesidetags.splice(nextsideindex, 1)
if (nextpossiblesidetags.length === 0) {
delete startVertexTagToSideTagMap[nextvertextag]
}
thisside = sideTagToSideMap[nextsidetag]
} // inner loop
// due to the logic of CAG.fromPoints()
// move the first point to the last
if (connectedVertexPoints.length > 0) {
connectedVertexPoints.push(connectedVertexPoints.shift())
}
let path = new Path2D(connectedVertexPoints, true)
paths.push(path)
} // outer loop
return paths
},
/*
cag = cag.overCutInsideCorners(cutterradius);
Using a CNC router it's impossible to cut out a true sharp inside corner. The inside corner
will be rounded due to the radius of the cutter. This function compensates for this by creating
an extra cutout at each inner corner so that the actual cut out shape will be at least as large
as needed.
*/
overCutInsideCorners: function (cutterradius) {
let cag = this.canonicalized()
// for each vertex determine the 'incoming' side and 'outgoing' side:
let pointmap = {} // tag => {pos: coord, from: [], to: []}
cag.sides.map(function (side) {
if (!(side.vertex0.getTag() in pointmap)) {
pointmap[side.vertex0.getTag()] = {
pos: side.vertex0.pos,
from: [],
to: []
}
}
pointmap[side.vertex0.getTag()].to.push(side.vertex1.pos)
if (!(side.vertex1.getTag() in pointmap)) {
pointmap[side.vertex1.getTag()] = {
pos: side.vertex1.pos,
from: [],
to: []
}
}
pointmap[side.vertex1.getTag()].from.push(side.vertex0.pos)
})
// overcut all sharp corners:
let cutouts = []
for (let pointtag in pointmap) {
let pointobj = pointmap[pointtag]
if ((pointobj.from.length === 1) && (pointobj.to.length === 1)) {
// ok, 1 incoming side and 1 outgoing side:
let fromcoord = pointobj.from[0]
let pointcoord = pointobj.pos
let tocoord = pointobj.to[0]
let v1 = pointcoord.minus(fromcoord).unit()
let v2 = tocoord.minus(pointcoord).unit()
let crossproduct = v1.cross(v2)
let isInnerCorner = (crossproduct < 0.001)
if (isInnerCorner) {
// yes it's a sharp corner:
let alpha = v2.angleRadians() - v1.angleRadians() + Math.PI
if (alpha < 0) {
alpha += 2 * Math.PI
} else if (alpha >= 2 * Math.PI) {
alpha -= 2 * Math.PI
}
let midvector = v2.minus(v1).unit()
let circlesegmentangle = 30 / 180 * Math.PI // resolution of the circle: segments of 30 degrees
// we need to increase the radius slightly so that our imperfect circle will contain a perfect circle of cutterradius
let radiuscorrected = cutterradius / Math.cos(circlesegmentangle / 2)
let circlecenter = pointcoord.plus(midvector.times(radiuscorrected))
// we don't need to create a full circle; a pie is enough. Find the angles for the pie:
let startangle = alpha + midvector.angleRadians()
let deltaangle = 2 * (Math.PI - alpha)
let numsteps = 2 * Math.ceil(deltaangle / circlesegmentangle / 2) // should be even
// build the pie:
let points = [circlecenter]
for (let i = 0; i <= numsteps; i++) {
let angle = startangle + i / numsteps * deltaangle
let p = Vector2D.fromAngleRadians(angle).times(radiuscorrected).plus(circlecenter)
points.push(p)
}
cutouts.push(CAG.fromPoints(points))
}
}
}
let result = cag.subtract(cutouts)
return result
}
}
module.exports = CAG

View File

@ -1,58 +0,0 @@
const CAG = require('./CAG')
const Side = require('./math/Side')
const Vector2D = require('./math/Vector2')
const Vertex = require('./math/Vertex2')
const Path2 = require('./math/Path2')
/** Reconstruct a CAG from an object with identical property names.
* @param {Object} obj - anonymous object, typically from JSON
* @returns {CAG} new CAG object
*/
const fromObject = function (obj) {
let sides = obj.sides.map(function (s) {
return Side.fromObject(s)
})
let cag = CAG.fromSides(sides)
cag.isCanonicalized = obj.isCanonicalized
return cag
}
/** Construct a CAG from a list of points (a polygon).
* Like fromPoints() but does not check if the result is a valid polygon.
* The points MUST rotate counter clockwise.
* The points can define a convex or a concave polygon.
* The polygon must not self intersect.
* @param {points[]} points - list of points in 2D space
* @returns {CAG} new CAG object
*/
const fromPointsNoCheck = function (points) {
let sides = []
let prevpoint = new Vector2D(points[points.length - 1])
let prevvertex = new Vertex(prevpoint)
points.map(function (p) {
let point = new Vector2D(p)
let vertex = new Vertex(point)
let side = new Side(prevvertex, vertex)
sides.push(side)
prevvertex = vertex
})
return CAG.fromSides(sides)
}
/** Construct a CAG from a 2d-path (a closed sequence of points).
* Like fromPoints() but does not check if the result is a valid polygon.
* @param {path} Path2 - a Path2 path
* @returns {CAG} new CAG object
*/
const fromPath2 = function (path) {
if (!path.isClosed()) throw new Error('The path should be closed!')
return CAG.fromPoints(path.getPoints())
}
module.exports = {
fromObject,
fromPointsNoCheck,
fromPath2
//fromFakeCSG
}

View File

@ -1,969 +0,0 @@
const {fnNumberSort} = require('./utils')
const FuzzyCSGFactory = require('./FuzzyFactory3d')
const Tree = require('./trees')
const {EPS} = require('./constants')
const {reTesselateCoplanarPolygons} = require('./math/polygonUtils')
const Polygon = require('./math/Polygon3')
const Plane = require('./math/Plane')
const Vertex = require('./math/Vertex3')
const Vector2D = require('./math/Vector2')
const Vector3D = require('./math/Vector3')
const Matrix4x4 = require('./math/Matrix4')
const OrthoNormalBasis = require('./math/OrthoNormalBasis')
const CAG = require('./CAG') // FIXME: circular dependency !
const Properties = require('./Properties')
const {Connector} = require('./connectors')
const fixTJunctions = require('./utils/fixTJunctions')
// let {fromPolygons} = require('./CSGMakers') // FIXME: circular dependency !
/** Class CSG
* Holds a binary space partition tree representing a 3D solid. Two solids can
* be combined using the `union()`, `subtract()`, and `intersect()` methods.
* @constructor
*/
let CSG = function () {
this.polygons = []
this.properties = new Properties()
this.isCanonicalized = true
this.isRetesselated = true
}
CSG.prototype = {
/** @return {Polygon[]} The list of polygons. */
toPolygons: function () {
return this.polygons
},
/**
* Return a new CSG solid representing the space in either this solid or
* in the given solids. Neither this solid nor the given solids are modified.
* @param {CSG[]} csg - list of CSG objects
* @returns {CSG} new CSG object
* @example
* let C = A.union(B)
* @example
* +-------+ +-------+
* | | | |
* | A | | |
* | +--+----+ = | +----+
* +----+--+ | +----+ |
* | B | | |
* | | | |
* +-------+ +-------+
*/
union: function (csg) {
let csgs
if (csg instanceof Array) {
csgs = csg.slice(0)
csgs.push(this)
} else {
csgs = [this, csg]
}
let i
// combine csg pairs in a way that forms a balanced binary tree pattern
for (i = 1; i < csgs.length; i += 2) {
csgs.push(csgs[i - 1].unionSub(csgs[i]))
}
return csgs[i - 1].reTesselated().canonicalized()
},
unionSub: function (csg, retesselate, canonicalize) {
if (!this.mayOverlap(csg)) {
return this.unionForNonIntersecting(csg)
} else {
let a = new Tree(this.polygons)
let b = new Tree(csg.polygons)
a.clipTo(b, false)
// b.clipTo(a, true); // ERROR: this doesn't work
b.clipTo(a)
b.invert()
b.clipTo(a)
b.invert()
let newpolygons = a.allPolygons().concat(b.allPolygons())
let result = CSG.fromPolygons(newpolygons)
result.properties = this.properties._merge(csg.properties)
if (retesselate) result = result.reTesselated()
if (canonicalize) result = result.canonicalized()
return result
}
},
// Like union, but when we know that the two solids are not intersecting
// Do not use if you are not completely sure that the solids do not intersect!
unionForNonIntersecting: function (csg) {
let newpolygons = this.polygons.concat(csg.polygons)
let result = CSG.fromPolygons(newpolygons)
result.properties = this.properties._merge(csg.properties)
result.isCanonicalized = this.isCanonicalized && csg.isCanonicalized
result.isRetesselated = this.isRetesselated && csg.isRetesselated
return result
},
/**
* Return a new CSG solid representing space in this solid but
* not in the given solids. Neither this solid nor the given solids are modified.
* @param {CSG[]} csg - list of CSG objects
* @returns {CSG} new CSG object
* @example
* let C = A.subtract(B)
* @example
* +-------+ +-------+
* | | | |
* | A | | |
* | +--+----+ = | +--+
* +----+--+ | +----+
* | B |
* | |
* +-------+
*/
subtract: function (csg) {
let csgs
if (csg instanceof Array) {
csgs = csg
} else {
csgs = [csg]
}
let result = this
for (let i = 0; i < csgs.length; i++) {
let islast = (i === (csgs.length - 1))
result = result.subtractSub(csgs[i], islast, islast)
}
return result
},
subtractSub: function (csg, retesselate, canonicalize) {
let a = new Tree(this.polygons)
let b = new Tree(csg.polygons)
a.invert()
a.clipTo(b)
b.clipTo(a, true)
a.addPolygons(b.allPolygons())
a.invert()
let result = CSG.fromPolygons(a.allPolygons())
result.properties = this.properties._merge(csg.properties)
if (retesselate) result = result.reTesselated()
if (canonicalize) result = result.canonicalized()
return result
},
/**
* Return a new CSG solid representing space in both this solid and
* in the given solids. Neither this solid nor the given solids are modified.
* @param {CSG[]} csg - list of CSG objects
* @returns {CSG} new CSG object
* @example
* let C = A.intersect(B)
* @example
* +-------+
* | |
* | A |
* | +--+----+ = +--+
* +----+--+ | +--+
* | B |
* | |
* +-------+
*/
intersect: function (csg) {
let csgs
if (csg instanceof Array) {
csgs = csg
} else {
csgs = [csg]
}
let result = this
for (let i = 0; i < csgs.length; i++) {
let islast = (i === (csgs.length - 1))
result = result.intersectSub(csgs[i], islast, islast)
}
return result
},
intersectSub: function (csg, retesselate, canonicalize) {
let a = new Tree(this.polygons)
let b = new Tree(csg.polygons)
a.invert()
b.clipTo(a)
b.invert()
a.clipTo(b)
b.clipTo(a)
a.addPolygons(b.allPolygons())
a.invert()
let result = CSG.fromPolygons(a.allPolygons())
result.properties = this.properties._merge(csg.properties)
if (retesselate) result = result.reTesselated()
if (canonicalize) result = result.canonicalized()
return result
},
/**
* Return a new CSG solid with solid and empty space switched.
* This solid is not modified.
* @returns {CSG} new CSG object
* @example
* let B = A.invert()
*/
invert: function () {
let flippedpolygons = this.polygons.map(function (p) {
return p.flipped()
})
return CSG.fromPolygons(flippedpolygons)
// TODO: flip properties?
},
// Affine transformation of CSG object. Returns a new CSG object
transform1: function (matrix4x4) {
let newpolygons = this.polygons.map(function (p) {
return p.transform(matrix4x4)
})
let result = CSG.fromPolygons(newpolygons)
result.properties = this.properties._transform(matrix4x4)
result.isRetesselated = this.isRetesselated
return result
},
/**
* Return a new CSG solid that is transformed using the given Matrix.
* Several matrix transformations can be combined before transforming this solid.
* @param {CSG.Matrix4x4} matrix4x4 - matrix to be applied
* @returns {CSG} new CSG object
* @example
* var m = new CSG.Matrix4x4()
* m = m.multiply(CSG.Matrix4x4.rotationX(40))
* m = m.multiply(CSG.Matrix4x4.translation([-.5, 0, 0]))
* let B = A.transform(m)
*/
transform: function (matrix4x4) {
let ismirror = matrix4x4.isMirroring()
let transformedvertices = {}
let transformedplanes = {}
let newpolygons = this.polygons.map(function (p) {
let newplane
let plane = p.plane
let planetag = plane.getTag()
if (planetag in transformedplanes) {
newplane = transformedplanes[planetag]
} else {
newplane = plane.transform(matrix4x4)
transformedplanes[planetag] = newplane
}
let newvertices = p.vertices.map(function (v) {
let newvertex
let vertextag = v.getTag()
if (vertextag in transformedvertices) {
newvertex = transformedvertices[vertextag]
} else {
newvertex = v.transform(matrix4x4)
transformedvertices[vertextag] = newvertex
}
return newvertex
})
if (ismirror) newvertices.reverse()
return new Polygon(newvertices, p.shared, newplane)
})
let result = CSG.fromPolygons(newpolygons)
result.properties = this.properties._transform(matrix4x4)
result.isRetesselated = this.isRetesselated
result.isCanonicalized = this.isCanonicalized
return result
},
toString: function () {
let result = 'CSG solid:\n'
this.polygons.map(function (p) {
result += p.toString()
})
return result
},
// Expand the solid
// resolution: number of points per 360 degree for the rounded corners
expand: function (radius, resolution) {
let result = this.expandedShell(radius, resolution, true)
result = result.reTesselated()
result.properties = this.properties // keep original properties
return result
},
// Contract the solid
// resolution: number of points per 360 degree for the rounded corners
contract: function (radius, resolution) {
let expandedshell = this.expandedShell(radius, resolution, false)
let result = this.subtract(expandedshell)
result = result.reTesselated()
result.properties = this.properties // keep original properties
return result
},
// cut the solid at a plane, and stretch the cross-section found along plane normal
stretchAtPlane: function (normal, point, length) {
let plane = Plane.fromNormalAndPoint(normal, point)
let onb = new OrthoNormalBasis(plane)
let crosssect = this.sectionCut(onb)
let midpiece = crosssect.extrudeInOrthonormalBasis(onb, length)
let piece1 = this.cutByPlane(plane)
let piece2 = this.cutByPlane(plane.flipped())
let result = piece1.union([midpiece, piece2.translate(plane.normal.times(length))])
return result
},
// Create the expanded shell of the solid:
// All faces are extruded to get a thickness of 2*radius
// Cylinders are constructed around every side
// Spheres are placed on every vertex
// unionWithThis: if true, the resulting solid will be united with 'this' solid;
// the result is a true expansion of the solid
// If false, returns only the shell
expandedShell: function (radius, resolution, unionWithThis) {
// const {sphere} = require('./primitives3d') // FIXME: circular dependency !
let csg = this.reTesselated()
let result
if (unionWithThis) {
result = csg
} else {
result = new CSG()
}
// first extrude all polygons:
csg.polygons.map(function (polygon) {
let extrudevector = polygon.plane.normal.unit().times(2 * radius)
let translatedpolygon = polygon.translate(extrudevector.times(-0.5))
let extrudedface = translatedpolygon.extrude(extrudevector)
result = result.unionSub(extrudedface, false, false)
})
// Make a list of all unique vertex pairs (i.e. all sides of the solid)
// For each vertex pair we collect the following:
// v1: first coordinate
// v2: second coordinate
// planenormals: array of normal vectors of all planes touching this side
let vertexpairs = {} // map of 'vertex pair tag' to {v1, v2, planenormals}
csg.polygons.map(function (polygon) {
let numvertices = polygon.vertices.length
let prevvertex = polygon.vertices[numvertices - 1]
let prevvertextag = prevvertex.getTag()
for (let i = 0; i < numvertices; i++) {
let vertex = polygon.vertices[i]
let vertextag = vertex.getTag()
let vertextagpair
if (vertextag < prevvertextag) {
vertextagpair = vertextag + '-' + prevvertextag
} else {
vertextagpair = prevvertextag + '-' + vertextag
}
let obj
if (vertextagpair in vertexpairs) {
obj = vertexpairs[vertextagpair]
} else {
obj = {
v1: prevvertex,
v2: vertex,
planenormals: []
}
vertexpairs[vertextagpair] = obj
}
obj.planenormals.push(polygon.plane.normal)
prevvertextag = vertextag
prevvertex = vertex
}
})
// now construct a cylinder on every side
// The cylinder is always an approximation of a true cylinder: it will have <resolution> polygons
// around the sides. We will make sure though that the cylinder will have an edge at every
// face that touches this side. This ensures that we will get a smooth fill even
// if two edges are at, say, 10 degrees and the resolution is low.
// Note: the result is not retesselated yet but it really should be!
for (let vertextagpair in vertexpairs) {
let vertexpair = vertexpairs[vertextagpair]
let startpoint = vertexpair.v1.pos
let endpoint = vertexpair.v2.pos
// our x,y and z vectors:
let zbase = endpoint.minus(startpoint).unit()
let xbase = vertexpair.planenormals[0].unit()
let ybase = xbase.cross(zbase)
// make a list of angles that the cylinder should traverse:
let angles = []
// first of all equally spaced around the cylinder:
for (let i = 0; i < resolution; i++) {
angles.push(i * Math.PI * 2 / resolution)
}
// and also at every normal of all touching planes:
for (let i = 0, iMax = vertexpair.planenormals.length; i < iMax; i++) {
let planenormal = vertexpair.planenormals[i]
let si = ybase.dot(planenormal)
let co = xbase.dot(planenormal)
let angle = Math.atan2(si, co)
if (angle < 0) angle += Math.PI * 2
angles.push(angle)
angle = Math.atan2(-si, -co)
if (angle < 0) angle += Math.PI * 2
angles.push(angle)
}
// this will result in some duplicate angles but we will get rid of those later.
// Sort:
angles = angles.sort(fnNumberSort)
// Now construct the cylinder by traversing all angles:
let numangles = angles.length
let prevp1
let prevp2
let startfacevertices = []
let endfacevertices = []
let polygons = []
for (let i = -1; i < numangles; i++) {
let angle = angles[(i < 0) ? (i + numangles) : i]
let si = Math.sin(angle)
let co = Math.cos(angle)
let p = xbase.times(co * radius).plus(ybase.times(si * radius))
let p1 = startpoint.plus(p)
let p2 = endpoint.plus(p)
let skip = false
if (i >= 0) {
if (p1.distanceTo(prevp1) < EPS) {
skip = true
}
}
if (!skip) {
if (i >= 0) {
startfacevertices.push(new Vertex(p1))
endfacevertices.push(new Vertex(p2))
let polygonvertices = [
new Vertex(prevp2),
new Vertex(p2),
new Vertex(p1),
new Vertex(prevp1)
]
let polygon = new Polygon(polygonvertices)
polygons.push(polygon)
}
prevp1 = p1
prevp2 = p2
}
}
endfacevertices.reverse()
polygons.push(new Polygon(startfacevertices))
polygons.push(new Polygon(endfacevertices))
let cylinder = CSG.fromPolygons(polygons)
result = result.unionSub(cylinder, false, false)
}
// make a list of all unique vertices
// For each vertex we also collect the list of normals of the planes touching the vertices
let vertexmap = {}
csg.polygons.map(function (polygon) {
polygon.vertices.map(function (vertex) {
let vertextag = vertex.getTag()
let obj
if (vertextag in vertexmap) {
obj = vertexmap[vertextag]
} else {
obj = {
pos: vertex.pos,
normals: []
}
vertexmap[vertextag] = obj
}
obj.normals.push(polygon.plane.normal)
})
})
// and build spheres at each vertex
// We will try to set the x and z axis to the normals of 2 planes
// This will ensure that our sphere tesselation somewhat matches 2 planes
for (let vertextag in vertexmap) {
let vertexobj = vertexmap[vertextag]
// use the first normal to be the x axis of our sphere:
let xaxis = vertexobj.normals[0].unit()
// and find a suitable z axis. We will use the normal which is most perpendicular to the x axis:
let bestzaxis = null
let bestzaxisorthogonality = 0
for (let i = 1; i < vertexobj.normals.length; i++) {
let normal = vertexobj.normals[i].unit()
let cross = xaxis.cross(normal)
let crosslength = cross.length()
if (crosslength > 0.05) {
if (crosslength > bestzaxisorthogonality) {
bestzaxisorthogonality = crosslength
bestzaxis = normal
}
}
}
if (!bestzaxis) {
bestzaxis = xaxis.randomNonParallelVector()
}
let yaxis = xaxis.cross(bestzaxis).unit()
let zaxis = yaxis.cross(xaxis)
let _sphere = CSG.sphere({
center: vertexobj.pos,
radius: radius,
resolution: resolution,
axes: [xaxis, yaxis, zaxis]
})
result = result.unionSub(_sphere, false, false)
}
return result
},
canonicalized: function () {
if (this.isCanonicalized) {
return this
} else {
let factory = new FuzzyCSGFactory()
let result = CSGFromCSGFuzzyFactory(factory, this)
result.isCanonicalized = true
result.isRetesselated = this.isRetesselated
result.properties = this.properties // keep original properties
return result
}
},
reTesselated: function () {
if (this.isRetesselated) {
return this
} else {
let csg = this
let polygonsPerPlane = {}
let isCanonicalized = csg.isCanonicalized
let fuzzyfactory = new FuzzyCSGFactory()
csg.polygons.map(function (polygon) {
let plane = polygon.plane
let shared = polygon.shared
if (!isCanonicalized) {
// in order to identify to polygons having the same plane, we need to canonicalize the planes
// We don't have to do a full canonizalization (including vertices), to save time only do the planes and the shared data:
plane = fuzzyfactory.getPlane(plane)
shared = fuzzyfactory.getPolygonShared(shared)
}
let tag = plane.getTag() + '/' + shared.getTag()
if (!(tag in polygonsPerPlane)) {
polygonsPerPlane[tag] = [polygon]
} else {
polygonsPerPlane[tag].push(polygon)
}
})
let destpolygons = []
for (let planetag in polygonsPerPlane) {
let sourcepolygons = polygonsPerPlane[planetag]
if (sourcepolygons.length < 2) {
destpolygons = destpolygons.concat(sourcepolygons)
} else {
let retesselayedpolygons = []
reTesselateCoplanarPolygons(sourcepolygons, retesselayedpolygons)
destpolygons = destpolygons.concat(retesselayedpolygons)
}
}
let result = CSG.fromPolygons(destpolygons)
result.isRetesselated = true
// result = result.canonicalized();
result.properties = this.properties // keep original properties
return result
}
},
/**
* Returns an array of Vector3D, providing minimum coordinates and maximum coordinates
* of this solid.
* @returns {Vector3D[]}
* @example
* let bounds = A.getBounds()
* let minX = bounds[0].x
*/
getBounds: function () {
if (!this.cachedBoundingBox) {
let minpoint = new Vector3D(0, 0, 0)
let maxpoint = new Vector3D(0, 0, 0)
let polygons = this.polygons
let numpolygons = polygons.length
for (let i = 0; i < numpolygons; i++) {
let polygon = polygons[i]
let bounds = polygon.boundingBox()
if (i === 0) {
minpoint = bounds[0]
maxpoint = bounds[1]
} else {
minpoint = minpoint.min(bounds[0])
maxpoint = maxpoint.max(bounds[1])
}
}
this.cachedBoundingBox = [minpoint, maxpoint]
}
return this.cachedBoundingBox
},
// returns true if there is a possibility that the two solids overlap
// returns false if we can be sure that they do not overlap
mayOverlap: function (csg) {
if ((this.polygons.length === 0) || (csg.polygons.length === 0)) {
return false
} else {
let mybounds = this.getBounds()
let otherbounds = csg.getBounds()
if (mybounds[1].x < otherbounds[0].x) return false
if (mybounds[0].x > otherbounds[1].x) return false
if (mybounds[1].y < otherbounds[0].y) return false
if (mybounds[0].y > otherbounds[1].y) return false
if (mybounds[1].z < otherbounds[0].z) return false
if (mybounds[0].z > otherbounds[1].z) return false
return true
}
},
// Cut the solid by a plane. Returns the solid on the back side of the plane
cutByPlane: function (plane) {
if (this.polygons.length === 0) {
return new CSG()
}
// Ideally we would like to do an intersection with a polygon of inifinite size
// but this is not supported by our implementation. As a workaround, we will create
// a cube, with one face on the plane, and a size larger enough so that the entire
// solid fits in the cube.
// find the max distance of any vertex to the center of the plane:
let planecenter = plane.normal.times(plane.w)
let maxdistance = 0
this.polygons.map(function (polygon) {
polygon.vertices.map(function (vertex) {
let distance = vertex.pos.distanceToSquared(planecenter)
if (distance > maxdistance) maxdistance = distance
})
})
maxdistance = Math.sqrt(maxdistance)
maxdistance *= 1.01 // make sure it's really larger
// Now build a polygon on the plane, at any point farther than maxdistance from the plane center:
let vertices = []
let orthobasis = new OrthoNormalBasis(plane)
vertices.push(new Vertex(orthobasis.to3D(new Vector2D(maxdistance, -maxdistance))))
vertices.push(new Vertex(orthobasis.to3D(new Vector2D(-maxdistance, -maxdistance))))
vertices.push(new Vertex(orthobasis.to3D(new Vector2D(-maxdistance, maxdistance))))
vertices.push(new Vertex(orthobasis.to3D(new Vector2D(maxdistance, maxdistance))))
let polygon = new Polygon(vertices, null, plane.flipped())
// and extrude the polygon into a cube, backwards of the plane:
let cube = polygon.extrude(plane.normal.times(-maxdistance))
// Now we can do the intersection:
let result = this.intersect(cube)
result.properties = this.properties // keep original properties
return result
},
// Connect a solid to another solid, such that two Connectors become connected
// myConnector: a Connector of this solid
// otherConnector: a Connector to which myConnector should be connected
// mirror: false: the 'axis' vectors of the connectors should point in the same direction
// true: the 'axis' vectors of the connectors should point in opposite direction
// normalrotation: degrees of rotation between the 'normal' vectors of the two
// connectors
connectTo: function (myConnector, otherConnector, mirror, normalrotation) {
let matrix = myConnector.getTransformationTo(otherConnector, mirror, normalrotation)
return this.transform(matrix)
},
// set the .shared property of all polygons
// Returns a new CSG solid, the original is unmodified!
setShared: function (shared) {
let polygons = this.polygons.map(function (p) {
return new Polygon(p.vertices, shared, p.plane)
})
let result = CSG.fromPolygons(polygons)
result.properties = this.properties // keep original properties
result.isRetesselated = this.isRetesselated
result.isCanonicalized = this.isCanonicalized
return result
},
setColor: function (args) {
let newshared = Polygon.Shared.fromColor.apply(this, arguments)
return this.setShared(newshared)
},
toCompactBinary: function () {
let csg = this.canonicalized(),
numpolygons = csg.polygons.length,
numpolygonvertices = 0,
numvertices = 0,
vertexmap = {},
vertices = [],
numplanes = 0,
planemap = {},
polygonindex = 0,
planes = [],
shareds = [],
sharedmap = {},
numshared = 0
// for (let i = 0, iMax = csg.polygons.length; i < iMax; i++) {
// let p = csg.polygons[i];
// for (let j = 0, jMax = p.length; j < jMax; j++) {
// ++numpolygonvertices;
// let vertextag = p[j].getTag();
// if(!(vertextag in vertexmap)) {
// vertexmap[vertextag] = numvertices++;
// vertices.push(p[j]);
// }
// }
csg.polygons.map(function (p) {
p.vertices.map(function (v) {
++numpolygonvertices
let vertextag = v.getTag()
if (!(vertextag in vertexmap)) {
vertexmap[vertextag] = numvertices++
vertices.push(v)
}
})
let planetag = p.plane.getTag()
if (!(planetag in planemap)) {
planemap[planetag] = numplanes++
planes.push(p.plane)
}
let sharedtag = p.shared.getTag()
if (!(sharedtag in sharedmap)) {
sharedmap[sharedtag] = numshared++
shareds.push(p.shared)
}
})
let numVerticesPerPolygon = new Uint32Array(numpolygons)
let polygonSharedIndexes = new Uint32Array(numpolygons)
let polygonVertices = new Uint32Array(numpolygonvertices)
let polygonPlaneIndexes = new Uint32Array(numpolygons)
let vertexData = new Float64Array(numvertices * 3)
let planeData = new Float64Array(numplanes * 4)
let polygonVerticesIndex = 0
for (let polygonindex = 0; polygonindex < numpolygons; ++polygonindex) {
let p = csg.polygons[polygonindex]
numVerticesPerPolygon[polygonindex] = p.vertices.length
p.vertices.map(function (v) {
let vertextag = v.getTag()
let vertexindex = vertexmap[vertextag]
polygonVertices[polygonVerticesIndex++] = vertexindex
})
let planetag = p.plane.getTag()
let planeindex = planemap[planetag]
polygonPlaneIndexes[polygonindex] = planeindex
let sharedtag = p.shared.getTag()
let sharedindex = sharedmap[sharedtag]
polygonSharedIndexes[polygonindex] = sharedindex
}
let verticesArrayIndex = 0
vertices.map(function (v) {
let pos = v.pos
vertexData[verticesArrayIndex++] = pos._x
vertexData[verticesArrayIndex++] = pos._y
vertexData[verticesArrayIndex++] = pos._z
})
let planesArrayIndex = 0
planes.map(function (p) {
let normal = p.normal
planeData[planesArrayIndex++] = normal._x
planeData[planesArrayIndex++] = normal._y
planeData[planesArrayIndex++] = normal._z
planeData[planesArrayIndex++] = p.w
})
let result = {
'class': 'CSG',
numPolygons: numpolygons,
numVerticesPerPolygon: numVerticesPerPolygon,
polygonPlaneIndexes: polygonPlaneIndexes,
polygonSharedIndexes: polygonSharedIndexes,
polygonVertices: polygonVertices,
vertexData: vertexData,
planeData: planeData,
shared: shareds
}
return result
},
// Get the transformation that transforms this CSG such that it is lying on the z=0 plane,
// as flat as possible (i.e. the least z-height).
// So that it is in an orientation suitable for CNC milling
getTransformationAndInverseTransformationToFlatLying: function () {
if (this.polygons.length === 0) {
let m = new Matrix4x4() // unity
return [m, m]
} else {
// get a list of unique planes in the CSG:
let csg = this.canonicalized()
let planemap = {}
csg.polygons.map(function (polygon) {
planemap[polygon.plane.getTag()] = polygon.plane
})
// try each plane in the CSG and find the plane that, when we align it flat onto z=0,
// gives the least height in z-direction.
// If two planes give the same height, pick the plane that originally had a normal closest
// to [0,0,-1].
let xvector = new Vector3D(1, 0, 0)
let yvector = new Vector3D(0, 1, 0)
let zvector = new Vector3D(0, 0, 1)
let z0connectorx = new Connector([0, 0, 0], [0, 0, -1], xvector)
let z0connectory = new Connector([0, 0, 0], [0, 0, -1], yvector)
let isfirst = true
let minheight = 0
let maxdotz = 0
let besttransformation, bestinversetransformation
for (let planetag in planemap) {
let plane = planemap[planetag]
let pointonplane = plane.normal.times(plane.w)
let transformation, inversetransformation
// We need a normal vecrtor for the transformation
// determine which is more perpendicular to the plane normal: x or y?
// we will align this as much as possible to the x or y axis vector
let xorthogonality = plane.normal.cross(xvector).length()
let yorthogonality = plane.normal.cross(yvector).length()
if (xorthogonality > yorthogonality) {
// x is better:
let planeconnector = new Connector(pointonplane, plane.normal, xvector)
transformation = planeconnector.getTransformationTo(z0connectorx, false, 0)
inversetransformation = z0connectorx.getTransformationTo(planeconnector, false, 0)
} else {
// y is better:
let planeconnector = new Connector(pointonplane, plane.normal, yvector)
transformation = planeconnector.getTransformationTo(z0connectory, false, 0)
inversetransformation = z0connectory.getTransformationTo(planeconnector, false, 0)
}
let transformedcsg = csg.transform(transformation)
let dotz = -plane.normal.dot(zvector)
let bounds = transformedcsg.getBounds()
let zheight = bounds[1].z - bounds[0].z
let isbetter = isfirst
if (!isbetter) {
if (zheight < minheight) {
isbetter = true
} else if (zheight === minheight) {
if (dotz > maxdotz) isbetter = true
}
}
if (isbetter) {
// translate the transformation around the z-axis and onto the z plane:
let translation = new Vector3D([-0.5 * (bounds[1].x + bounds[0].x), -0.5 * (bounds[1].y + bounds[0].y), -bounds[0].z])
transformation = transformation.multiply(Matrix4x4.translation(translation))
inversetransformation = Matrix4x4.translation(translation.negated()).multiply(inversetransformation)
minheight = zheight
maxdotz = dotz
besttransformation = transformation
bestinversetransformation = inversetransformation
}
isfirst = false
}
return [besttransformation, bestinversetransformation]
}
},
getTransformationToFlatLying: function () {
let result = this.getTransformationAndInverseTransformationToFlatLying()
return result[0]
},
lieFlat: function () {
let transformation = this.getTransformationToFlatLying()
return this.transform(transformation)
},
// project the 3D CSG onto a plane
// This returns a 2D CAG with the 'shadow' shape of the 3D solid when projected onto the
// plane represented by the orthonormal basis
projectToOrthoNormalBasis: function (orthobasis) {
let cags = []
this.polygons.filter(function (p) {
// only return polys in plane, others may disturb result
return p.plane.normal.minus(orthobasis.plane.normal).lengthSquared() < (EPS * EPS)
})
.map(function (polygon) {
let cag = polygon.projectToOrthoNormalBasis(orthobasis)
if (cag.sides.length > 0) {
cags.push(cag)
}
})
let result = new CAG().union(cags)
return result
},
sectionCut: function (orthobasis) {
let plane1 = orthobasis.plane
let plane2 = orthobasis.plane.flipped()
plane1 = new Plane(plane1.normal, plane1.w)
plane2 = new Plane(plane2.normal, plane2.w + (5 * EPS))
let cut3d = this.cutByPlane(plane1)
cut3d = cut3d.cutByPlane(plane2)
return cut3d.projectToOrthoNormalBasis(orthobasis)
},
fixTJunctions: function () {
return fixTJunctions(CSG.fromPolygons, this)
},
toTriangles: function () {
let polygons = []
this.polygons.forEach(function (poly) {
let firstVertex = poly.vertices[0]
for (let i = poly.vertices.length - 3; i >= 0; i--) {
polygons.push(new Polygon([
firstVertex, poly.vertices[i + 1], poly.vertices[i + 2]
],
poly.shared, poly.plane))
}
})
return polygons
},
/**
* Returns an array of values for the requested features of this solid.
* Supported Features: 'volume', 'area'
* @param {String[]} features - list of features to calculate
* @returns {Float[]} values
* @example
* let volume = A.getFeatures('volume')
* let values = A.getFeatures('area','volume')
*/
getFeatures: function (features) {
if (!(features instanceof Array)) {
features = [features]
}
let result = this.toTriangles().map(function (triPoly) {
return triPoly.getTetraFeatures(features)
})
.reduce(function (pv, v) {
return v.map(function (feat, i) {
return feat + (pv === 0 ? 0 : pv[i])
})
}, 0)
return (result.length === 1) ? result[0] : result
}
}
/** Construct a CSG solid from a list of `Polygon` instances.
* @param {Polygon[]} polygons - list of polygons
* @returns {CSG} new CSG object
*/
CSG.fromPolygons = function fromPolygons (polygons) {
let csg = new CSG()
csg.polygons = polygons
csg.isCanonicalized = false
csg.isRetesselated = false
return csg
}
const CSGFromCSGFuzzyFactory = function (factory, sourcecsg) {
let _this = factory
let newpolygons = []
sourcecsg.polygons.forEach(function (polygon) {
let newpolygon = _this.getPolygon(polygon)
// see getPolygon above: we may get a polygon with no vertices, discard it:
if (newpolygon.vertices.length >= 3) {
newpolygons.push(newpolygon)
}
})
return CSG.fromPolygons(newpolygons)
}
module.exports = CSG

View File

@ -1,111 +0,0 @@
const Vector3D = require('./math/Vector3')
const Vertex = require('./math/Vertex3')
const Plane = require('./math/Plane')
const Polygon2D = require('./math/Polygon2')
const Polygon3D = require('./math/Polygon3')
/** Construct a CSG solid from a list of pre-generated slices.
* See Polygon.prototype.solidFromSlices() for details.
* @param {Object} options - options passed to solidFromSlices()
* @returns {CSG} new CSG object
*/
function fromSlices (options) {
return (new Polygon2D.createFromPoints([
[0, 0, 0],
[1, 0, 0],
[1, 1, 0],
[0, 1, 0]
])).solidFromSlices(options)
}
/** Reconstruct a CSG solid from an object with identical property names.
* @param {Object} obj - anonymous object, typically from JSON
* @returns {CSG} new CSG object
*/
function fromObject (obj) {
const CSG = require('./CSG')
let polygons = obj.polygons.map(function (p) {
return Polygon3D.fromObject(p)
})
let csg = CSG.fromPolygons(polygons)
csg.isCanonicalized = obj.isCanonicalized
csg.isRetesselated = obj.isRetesselated
return csg
}
/** Reconstruct a CSG from the output of toCompactBinary().
* @param {CompactBinary} bin - see toCompactBinary().
* @returns {CSG} new CSG object
*/
function fromCompactBinary (bin) {
const CSG = require('./CSG') // FIXME: circular dependency ??
if (bin['class'] !== 'CSG') throw new Error('Not a CSG')
let planes = []
let planeData = bin.planeData
let numplanes = planeData.length / 4
let arrayindex = 0
let x, y, z, w, normal, plane
for (let planeindex = 0; planeindex < numplanes; planeindex++) {
x = planeData[arrayindex++]
y = planeData[arrayindex++]
z = planeData[arrayindex++]
w = planeData[arrayindex++]
normal = Vector3D.Create(x, y, z)
plane = new Plane(normal, w)
planes.push(plane)
}
let vertices = []
const vertexData = bin.vertexData
const numvertices = vertexData.length / 3
let pos
let vertex
arrayindex = 0
for (let vertexindex = 0; vertexindex < numvertices; vertexindex++) {
x = vertexData[arrayindex++]
y = vertexData[arrayindex++]
z = vertexData[arrayindex++]
pos = Vector3D.Create(x, y, z)
vertex = new Vertex(pos)
vertices.push(vertex)
}
let shareds = bin.shared.map(function (shared) {
return Polygon3D.Shared.fromObject(shared)
})
let polygons = []
let numpolygons = bin.numPolygons
let numVerticesPerPolygon = bin.numVerticesPerPolygon
let polygonVertices = bin.polygonVertices
let polygonPlaneIndexes = bin.polygonPlaneIndexes
let polygonSharedIndexes = bin.polygonSharedIndexes
let numpolygonvertices
let polygonvertices
let shared
let polygon // already defined plane,
arrayindex = 0
for (let polygonindex = 0; polygonindex < numpolygons; polygonindex++) {
numpolygonvertices = numVerticesPerPolygon[polygonindex]
polygonvertices = []
for (let i = 0; i < numpolygonvertices; i++) {
polygonvertices.push(vertices[polygonVertices[arrayindex++]])
}
plane = planes[polygonPlaneIndexes[polygonindex]]
shared = shareds[polygonSharedIndexes[polygonindex]]
polygon = new Polygon3D(polygonvertices, shared, plane)
polygons.push(polygon)
}
let csg = CSG.fromPolygons(polygons)
csg.isCanonicalized = true
csg.isRetesselated = true
return csg
}
module.exports = {
//fromPolygons,
fromSlices,
fromObject,
fromCompactBinary
}

View File

@ -1,56 +0,0 @@
// //////////////////////////////
// ## class fuzzyFactory
// This class acts as a factory for objects. We can search for an object with approximately
// the desired properties (say a rectangle with width 2 and height 1)
// The lookupOrCreate() method looks for an existing object (for example it may find an existing rectangle
// with width 2.0001 and height 0.999. If no object is found, the user supplied callback is
// called, which should generate a new object. The new object is inserted into the database
// so it can be found by future lookupOrCreate() calls.
// Constructor:
// numdimensions: the number of parameters for each object
// for example for a 2D rectangle this would be 2
// tolerance: The maximum difference for each parameter allowed to be considered a match
const FuzzyFactory = function (numdimensions, tolerance) {
this.lookuptable = {}
this.multiplier = 1.0 / tolerance
}
FuzzyFactory.prototype = {
// let obj = f.lookupOrCreate([el1, el2, el3], function(elements) {/* create the new object */});
// Performs a fuzzy lookup of the object with the specified elements.
// If found, returns the existing object
// If not found, calls the supplied callback function which should create a new object with
// the specified properties. This object is inserted in the lookup database.
lookupOrCreate: function (els, creatorCallback) {
let hash = ''
let multiplier = this.multiplier
els.forEach(function (el) {
let valueQuantized = Math.round(el * multiplier)
hash += valueQuantized + '/'
})
if (hash in this.lookuptable) {
return this.lookuptable[hash]
} else {
let object = creatorCallback(els)
let hashparts = els.map(function (el) {
let q0 = Math.floor(el * multiplier)
let q1 = q0 + 1
return ['' + q0 + '/', '' + q1 + '/']
})
let numelements = els.length
let numhashes = 1 << numelements
for (let hashmask = 0; hashmask < numhashes; ++hashmask) {
let hashmaskShifted = hashmask
hash = ''
hashparts.forEach(function (hashpart) {
hash += hashpart[hashmaskShifted & 1]
hashmaskShifted >>= 1
})
this.lookuptable[hash] = object
}
return object
}
}
}
module.exports = FuzzyFactory

View File

@ -1,25 +0,0 @@
const FuzzyFactory = require('./FuzzyFactory')
const {EPS} = require('./constants')
const Side = require('./math/Side')
const FuzzyCAGFactory = function () {
this.vertexfactory = new FuzzyFactory(2, EPS)
}
FuzzyCAGFactory.prototype = {
getVertex: function (sourcevertex) {
let elements = [sourcevertex.pos._x, sourcevertex.pos._y]
let result = this.vertexfactory.lookupOrCreate(elements, function (els) {
return sourcevertex
})
return result
},
getSide: function (sourceside) {
let vertex0 = this.getVertex(sourceside.vertex0)
let vertex1 = this.getVertex(sourceside.vertex1)
return new Side(vertex0, vertex1)
}
}
module.exports = FuzzyCAGFactory

View File

@ -1,68 +0,0 @@
const {EPS} = require('./constants')
const Polygon = require('./math/Polygon3')
const FuzzyFactory = require('./FuzzyFactory')
// ////////////////////////////////////
const FuzzyCSGFactory = function () {
this.vertexfactory = new FuzzyFactory(3, EPS)
this.planefactory = new FuzzyFactory(4, EPS)
this.polygonsharedfactory = {}
}
FuzzyCSGFactory.prototype = {
getPolygonShared: function (sourceshared) {
let hash = sourceshared.getHash()
if (hash in this.polygonsharedfactory) {
return this.polygonsharedfactory[hash]
} else {
this.polygonsharedfactory[hash] = sourceshared
return sourceshared
}
},
getVertex: function (sourcevertex) {
let elements = [sourcevertex.pos._x, sourcevertex.pos._y, sourcevertex.pos._z]
let result = this.vertexfactory.lookupOrCreate(elements, function (els) {
return sourcevertex
})
return result
},
getPlane: function (sourceplane) {
let elements = [sourceplane.normal._x, sourceplane.normal._y, sourceplane.normal._z, sourceplane.w]
let result = this.planefactory.lookupOrCreate(elements, function (els) {
return sourceplane
})
return result
},
getPolygon: function (sourcepolygon) {
let newplane = this.getPlane(sourcepolygon.plane)
let newshared = this.getPolygonShared(sourcepolygon.shared)
let _this = this
let newvertices = sourcepolygon.vertices.map(function (vertex) {
return _this.getVertex(vertex)
})
// two vertices that were originally very close may now have become
// truly identical (referring to the same Vertex object).
// Remove duplicate vertices:
let newverticesDedup = []
if (newvertices.length > 0) {
let prevvertextag = newvertices[newvertices.length - 1].getTag()
newvertices.forEach(function (vertex) {
let vertextag = vertex.getTag()
if (vertextag !== prevvertextag) {
newverticesDedup.push(vertex)
}
prevvertextag = vertextag
})
}
// If it's degenerate, remove all vertices:
if (newverticesDedup.length < 3) {
newverticesDedup = []
}
return new Polygon(newverticesDedup, newshared, newplane)
}
}
module.exports = FuzzyCSGFactory

View File

@ -1,82 +0,0 @@
// ////////////////////////////////////
// # Class Properties
// This class is used to store properties of a solid
// A property can for example be a Vertex, a Plane or a Line3D
// Whenever an affine transform is applied to the CSG solid, all its properties are
// transformed as well.
// The properties can be stored in a complex nested structure (using arrays and objects)
const Properties = function () {}
Properties.prototype = {
_transform: function (matrix4x4) {
let result = new Properties()
Properties.transformObj(this, result, matrix4x4)
return result
},
_merge: function (otherproperties) {
let result = new Properties()
Properties.cloneObj(this, result)
Properties.addFrom(result, otherproperties)
return result
}
}
Properties.transformObj = function (source, result, matrix4x4) {
for (let propertyname in source) {
if (propertyname === '_transform') continue
if (propertyname === '_merge') continue
let propertyvalue = source[propertyname]
let transformed = propertyvalue
if (typeof (propertyvalue) === 'object') {
if (('transform' in propertyvalue) && (typeof (propertyvalue.transform) === 'function')) {
transformed = propertyvalue.transform(matrix4x4)
} else if (propertyvalue instanceof Array) {
transformed = []
Properties.transformObj(propertyvalue, transformed, matrix4x4)
} else if (propertyvalue instanceof Properties) {
transformed = new Properties()
Properties.transformObj(propertyvalue, transformed, matrix4x4)
}
}
result[propertyname] = transformed
}
}
Properties.cloneObj = function (source, result) {
for (let propertyname in source) {
if (propertyname === '_transform') continue
if (propertyname === '_merge') continue
let propertyvalue = source[propertyname]
let cloned = propertyvalue
if (typeof (propertyvalue) === 'object') {
if (propertyvalue instanceof Array) {
cloned = []
for (let i = 0; i < propertyvalue.length; i++) {
cloned.push(propertyvalue[i])
}
} else if (propertyvalue instanceof Properties) {
cloned = new Properties()
Properties.cloneObj(propertyvalue, cloned)
}
}
result[propertyname] = cloned
}
}
Properties.addFrom = function (result, otherproperties) {
for (let propertyname in otherproperties) {
if (propertyname === '_transform') continue
if (propertyname === '_merge') continue
if ((propertyname in result) &&
(typeof (result[propertyname]) === 'object') &&
(result[propertyname] instanceof Properties) &&
(typeof (otherproperties[propertyname]) === 'object') &&
(otherproperties[propertyname] instanceof Properties)) {
Properties.addFrom(result[propertyname], otherproperties[propertyname])
} else if (!(propertyname in result)) {
result[propertyname] = otherproperties[propertyname]
}
}
}
module.exports = Properties

View File

@ -1,220 +0,0 @@
const Vector3D = require('./math/Vector3')
const Line3D = require('./math/Line3')
const Matrix4x4 = require('./math/Matrix4')
const OrthoNormalBasis = require('./math/OrthoNormalBasis')
const Plane = require('./math/Plane')
// # class Connector
// A connector allows to attach two objects at predefined positions
// For example a servo motor and a servo horn:
// Both can have a Connector called 'shaft'
// The horn can be moved and rotated such that the two connectors match
// and the horn is attached to the servo motor at the proper position.
// Connectors are stored in the properties of a CSG solid so they are
// ge the same transformations applied as the solid
const Connector = function (point, axisvector, normalvector) {
this.point = new Vector3D(point)
this.axisvector = new Vector3D(axisvector).unit()
this.normalvector = new Vector3D(normalvector).unit()
}
Connector.prototype = {
normalized: function () {
let axisvector = this.axisvector.unit()
// make the normal vector truly normal:
let n = this.normalvector.cross(axisvector).unit()
let normalvector = axisvector.cross(n)
return new Connector(this.point, axisvector, normalvector)
},
transform: function (matrix4x4) {
let point = this.point.multiply4x4(matrix4x4)
let axisvector = this.point.plus(this.axisvector).multiply4x4(matrix4x4).minus(point)
let normalvector = this.point.plus(this.normalvector).multiply4x4(matrix4x4).minus(point)
return new Connector(point, axisvector, normalvector)
},
// Get the transformation matrix to connect this Connector to another connector
// other: a Connector to which this connector should be connected
// mirror: false: the 'axis' vectors of the connectors should point in the same direction
// true: the 'axis' vectors of the connectors should point in opposite direction
// normalrotation: degrees of rotation between the 'normal' vectors of the two
// connectors
getTransformationTo: function (other, mirror, normalrotation) {
mirror = mirror ? true : false
normalrotation = normalrotation ? Number(normalrotation) : 0
let us = this.normalized()
other = other.normalized()
// shift to the origin:
let transformation = Matrix4x4.translation(this.point.negated())
// construct the plane crossing through the origin and the two axes:
let axesplane = Plane.anyPlaneFromVector3Ds(
new Vector3D(0, 0, 0), us.axisvector, other.axisvector)
let axesbasis = new OrthoNormalBasis(axesplane)
let angle1 = axesbasis.to2D(us.axisvector).angle()
let angle2 = axesbasis.to2D(other.axisvector).angle()
let rotation = 180.0 * (angle2 - angle1) / Math.PI
if (mirror) rotation += 180.0
transformation = transformation.multiply(axesbasis.getProjectionMatrix())
transformation = transformation.multiply(Matrix4x4.rotationZ(rotation))
transformation = transformation.multiply(axesbasis.getInverseProjectionMatrix())
let usAxesAligned = us.transform(transformation)
// Now we have done the transformation for aligning the axes.
// We still need to align the normals:
let normalsplane = Plane.fromNormalAndPoint(other.axisvector, new Vector3D(0, 0, 0))
let normalsbasis = new OrthoNormalBasis(normalsplane)
angle1 = normalsbasis.to2D(usAxesAligned.normalvector).angle()
angle2 = normalsbasis.to2D(other.normalvector).angle()
rotation = 180.0 * (angle2 - angle1) / Math.PI
rotation += normalrotation
transformation = transformation.multiply(normalsbasis.getProjectionMatrix())
transformation = transformation.multiply(Matrix4x4.rotationZ(rotation))
transformation = transformation.multiply(normalsbasis.getInverseProjectionMatrix())
// and translate to the destination point:
transformation = transformation.multiply(Matrix4x4.translation(other.point))
// let usAligned = us.transform(transformation);
return transformation
},
axisLine: function () {
return new Line3D(this.point, this.axisvector)
},
// creates a new Connector, with the connection point moved in the direction of the axisvector
extend: function (distance) {
let newpoint = this.point.plus(this.axisvector.unit().times(distance))
return new Connector(newpoint, this.axisvector, this.normalvector)
}
}
const ConnectorList = function (connectors) {
this.connectors_ = connectors ? connectors.slice() : []
}
ConnectorList.defaultNormal = [0, 0, 1]
ConnectorList.fromPath2D = function (path2D, arg1, arg2) {
if (arguments.length === 3) {
return ConnectorList._fromPath2DTangents(path2D, arg1, arg2)
} else if (arguments.length === 2) {
return ConnectorList._fromPath2DExplicit(path2D, arg1)
} else {
throw new Error('call with path2D and either 2 direction vectors, or a function returning direction vectors')
}
}
/*
* calculate the connector axisvectors by calculating the "tangent" for path2D.
* This is undefined for start and end points, so axis for these have to be manually
* provided.
*/
ConnectorList._fromPath2DTangents = function (path2D, start, end) {
// path2D
let axis
let pathLen = path2D.points.length
let result = new ConnectorList([new Connector(path2D.points[0],
start, ConnectorList.defaultNormal)])
// middle points
path2D.points.slice(1, pathLen - 1).forEach(function (p2, i) {
axis = path2D.points[i + 2].minus(path2D.points[i]).toVector3D(0)
result.appendConnector(new Connector(p2.toVector3D(0), axis,
ConnectorList.defaultNormal))
}, this)
result.appendConnector(new Connector(path2D.points[pathLen - 1], end,
ConnectorList.defaultNormal))
result.closed = path2D.closed
return result
}
/*
* angleIsh: either a static angle, or a function(point) returning an angle
*/
ConnectorList._fromPath2DExplicit = function (path2D, angleIsh) {
function getAngle (angleIsh, pt, i) {
if (typeof angleIsh === 'function') {
angleIsh = angleIsh(pt, i)
}
return angleIsh
}
let result = new ConnectorList(
path2D.points.map(function (p2, i) {
return new Connector(p2.toVector3D(0),
Vector3D.Create(1, 0, 0).rotateZ(getAngle(angleIsh, p2, i)),
ConnectorList.defaultNormal)
}, this)
)
result.closed = path2D.closed
return result
}
ConnectorList.prototype = {
setClosed: function (closed) {
this.closed = !!closed // FIXME: what the hell ?
},
appendConnector: function (conn) {
this.connectors_.push(conn)
},
/*
* arguments: cagish: a cag or a function(connector) returning a cag
* closed: whether the 3d path defined by connectors location
* should be closed or stay open
* Note: don't duplicate connectors in the path
* TODO: consider an option "maySelfIntersect" to close & force union all single segments
*/
followWith: function (cagish) {
const CSG = require('./CSG') // FIXME , circular dependency connectors => CSG => connectors
this.verify()
function getCag (cagish, connector) {
if (typeof cagish === 'function') {
cagish = cagish(connector.point, connector.axisvector, connector.normalvector)
}
return cagish
}
let polygons = []
let currCag
let prevConnector = this.connectors_[this.connectors_.length - 1]
let prevCag = getCag(cagish, prevConnector)
// add walls
this.connectors_.forEach(function (connector, notFirst) {
currCag = getCag(cagish, connector)
if (notFirst || this.closed) {
polygons.push.apply(polygons, prevCag._toWallPolygons({
toConnector1: prevConnector, toConnector2: connector, cag: currCag}))
} else {
// it is the first, and shape not closed -> build start wall
polygons.push.apply(polygons,
currCag._toPlanePolygons({toConnector: connector, flipped: true}))
}
if (notFirst === this.connectors_.length - 1 && !this.closed) {
// build end wall
polygons.push.apply(polygons,
currCag._toPlanePolygons({toConnector: connector}))
}
prevCag = currCag
prevConnector = connector
}, this)
return CSG.fromPolygons(polygons).reTesselated().canonicalized()
},
/*
* general idea behind these checks: connectors need to have smooth transition from one to another
* TODO: add a check that 2 follow-on CAGs are not intersecting
*/
verify: function () {
let connI
let connI1
for (let i = 0; i < this.connectors_.length - 1; i++) {
connI = this.connectors_[i]
connI1 = this.connectors_[i + 1]
if (connI1.point.minus(connI.point).dot(connI.axisvector) <= 0) {
throw new Error('Invalid ConnectorList. Each connectors position needs to be within a <90deg range of previous connectors axisvector')
}
if (connI.axisvector.dot(connI1.axisvector) <= 0) {
throw new Error('invalid ConnectorList. No neighboring connectors axisvectors may span a >=90deg angle')
}
}
}
}
module.exports = {Connector, ConnectorList}

View File

@ -1,55 +0,0 @@
const _CSGDEBUG = false
/** Number of polygons per 360 degree revolution for 2D objects.
* @default
*/
const defaultResolution2D = 32 // FIXME this seems excessive
/** Number of polygons per 360 degree revolution for 3D objects.
* @default
*/
const defaultResolution3D = 12
/** Epsilon used during determination of near zero distances.
* @default
*/
const EPS = 1e-5
/** Epsilon used during determination of near zero areas.
* @default
*/
const angleEPS = 0.10
/** Epsilon used during determination of near zero areas.
* This is the minimal area of a minimal polygon.
* @default
*/
const areaEPS = 0.50 * EPS * EPS * Math.sin(angleEPS)
const all = 0
const top = 1
const bottom = 2
const left = 3
const right = 4
const front = 5
const back = 6
// Tag factory: we can request a unique tag through CSG.getTag()
let staticTag = 1
const getTag = () => staticTag++
module.exports = {
_CSGDEBUG,
defaultResolution2D,
defaultResolution3D,
EPS,
angleEPS,
areaEPS,
all,
top,
bottom,
left,
right,
front,
back,
staticTag,
getTag
}

View File

@ -1,33 +0,0 @@
const CSG = require('./CSG')
const {cube} = require('./primitives3d')
// For debugging
// Creates a new solid with a tiny cube at every vertex of the source solid
// this is seperated from the CSG class itself because of the dependency on cube
const toPointCloud = function (csg, cuberadius) {
csg = csg.reTesselated()
let result = new CSG()
// make a list of all unique vertices
// For each vertex we also collect the list of normals of the planes touching the vertices
let vertexmap = {}
csg.polygons.map(function (polygon) {
polygon.vertices.map(function (vertex) {
vertexmap[vertex.getTag()] = vertex.pos
})
})
for (let vertextag in vertexmap) {
let pos = vertexmap[vertextag]
let _cube = cube({
center: pos,
radius: cuberadius
})
result = result.unionSub(_cube, false, false)
}
result = result.reTesselated()
return result
}
module.exports = {toPointCloud}

View File

@ -1,90 +0,0 @@
const Vector2D = require('./Vector2')
const {solve2Linear} = require('../utils')
/** class Line2D
* Represents a directional line in 2D space
* A line is parametrized by its normal vector (perpendicular to the line, rotated 90 degrees counter clockwise)
* and w. The line passes through the point <normal>.times(w).
* Equation: p is on line if normal.dot(p)==w
* @param {Vector2D} normal normal must be a unit vector!
* @returns {Line2D}
*/
const Line2D = function (normal, w) {
normal = new Vector2D(normal)
w = parseFloat(w)
let l = normal.length()
// normalize:
w *= l
normal = normal.times(1.0 / l)
this.normal = normal
this.w = w
}
Line2D.fromPoints = function (p1, p2) {
p1 = new Vector2D(p1)
p2 = new Vector2D(p2)
let direction = p2.minus(p1)
let normal = direction.normal().negated().unit()
let w = p1.dot(normal)
return new Line2D(normal, w)
}
Line2D.prototype = {
// same line but opposite direction:
reverse: function () {
return new Line2D(this.normal.negated(), -this.w)
},
equals: function (l) {
return (l.normal.equals(this.normal) && (l.w === this.w))
},
origin: function () {
return this.normal.times(this.w)
},
direction: function () {
return this.normal.normal()
},
xAtY: function (y) {
// (py == y) && (normal * p == w)
// -> px = (w - normal._y * y) / normal.x
let x = (this.w - this.normal._y * y) / this.normal.x
return x
},
absDistanceToPoint: function (point) {
point = new Vector2D(point)
let pointProjected = point.dot(this.normal)
let distance = Math.abs(pointProjected - this.w)
return distance
},
/* FIXME: has error - origin is not defined, the method is never used
closestPoint: function(point) {
point = new Vector2D(point);
let vector = point.dot(this.direction());
return origin.plus(vector);
},
*/
// intersection between two lines, returns point as Vector2D
intersectWithLine: function (line2d) {
let point = solve2Linear(this.normal.x, this.normal.y, line2d.normal.x, line2d.normal.y, this.w, line2d.w)
point = new Vector2D(point) // make vector2d
return point
},
transform: function (matrix4x4) {
let origin = new Vector2D(0, 0)
let pointOnPlane = this.normal.times(this.w)
let neworigin = origin.multiply4x4(matrix4x4)
let neworiginPlusNormal = this.normal.multiply4x4(matrix4x4)
let newnormal = neworiginPlusNormal.minus(neworigin)
let newpointOnPlane = pointOnPlane.multiply4x4(matrix4x4)
let neww = newnormal.dot(newpointOnPlane)
return new Line2D(newnormal, neww)
}
}
module.exports = Line2D

View File

@ -1,100 +0,0 @@
const Vector3D = require('./Vector3')
const {EPS} = require('../constants')
const {solve2Linear} = require('../utils')
// # class Line3D
// Represents a line in 3D space
// direction must be a unit vector
// point is a random point on the line
const Line3D = function (point, direction) {
point = new Vector3D(point)
direction = new Vector3D(direction)
this.point = point
this.direction = direction.unit()
}
Line3D.fromPoints = function (p1, p2) {
p1 = new Vector3D(p1)
p2 = new Vector3D(p2)
let direction = p2.minus(p1)
return new Line3D(p1, direction)
}
Line3D.fromPlanes = function (p1, p2) {
let direction = p1.normal.cross(p2.normal)
let l = direction.length()
if (l < EPS) {
throw new Error('Parallel planes')
}
direction = direction.times(1.0 / l)
let mabsx = Math.abs(direction.x)
let mabsy = Math.abs(direction.y)
let mabsz = Math.abs(direction.z)
let origin
if ((mabsx >= mabsy) && (mabsx >= mabsz)) {
// direction vector is mostly pointing towards x
// find a point p for which x is zero:
let r = solve2Linear(p1.normal.y, p1.normal.z, p2.normal.y, p2.normal.z, p1.w, p2.w)
origin = new Vector3D(0, r[0], r[1])
} else if ((mabsy >= mabsx) && (mabsy >= mabsz)) {
// find a point p for which y is zero:
let r = solve2Linear(p1.normal.x, p1.normal.z, p2.normal.x, p2.normal.z, p1.w, p2.w)
origin = new Vector3D(r[0], 0, r[1])
} else {
// find a point p for which z is zero:
let r = solve2Linear(p1.normal.x, p1.normal.y, p2.normal.x, p2.normal.y, p1.w, p2.w)
origin = new Vector3D(r[0], r[1], 0)
}
return new Line3D(origin, direction)
}
Line3D.prototype = {
intersectWithPlane: function (plane) {
// plane: plane.normal * p = plane.w
// line: p=line.point + labda * line.direction
let labda = (plane.w - plane.normal.dot(this.point)) / plane.normal.dot(this.direction)
let point = this.point.plus(this.direction.times(labda))
return point
},
clone: function (line) {
return new Line3D(this.point.clone(), this.direction.clone())
},
reverse: function () {
return new Line3D(this.point.clone(), this.direction.negated())
},
transform: function (matrix4x4) {
let newpoint = this.point.multiply4x4(matrix4x4)
let pointPlusDirection = this.point.plus(this.direction)
let newPointPlusDirection = pointPlusDirection.multiply4x4(matrix4x4)
let newdirection = newPointPlusDirection.minus(newpoint)
return new Line3D(newpoint, newdirection)
},
closestPointOnLine: function (point) {
point = new Vector3D(point)
let t = point.minus(this.point).dot(this.direction) / this.direction.dot(this.direction)
let closestpoint = this.point.plus(this.direction.times(t))
return closestpoint
},
distanceToPoint: function (point) {
point = new Vector3D(point)
let closestpoint = this.closestPointOnLine(point)
let distancevector = point.minus(closestpoint)
let distance = distancevector.length()
return distance
},
equals: function (line3d) {
if (!this.direction.equals(line3d.direction)) return false
let distance = this.distanceToPoint(line3d.point)
if (distance > EPS) return false
return true
}
}
module.exports = Line3D

View File

@ -1,284 +0,0 @@
const Vector3D = require('./Vector3')
const Vector2D = require('./Vector2')
const OrthoNormalBasis = require('./OrthoNormalBasis')
const Plane = require('./Plane')
// # class Matrix4x4:
// Represents a 4x4 matrix. Elements are specified in row order
const Matrix4x4 = function (elements) {
if (arguments.length >= 1) {
this.elements = elements
} else {
// if no arguments passed: create unity matrix
this.elements = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
}
}
Matrix4x4.prototype = {
plus: function (m) {
var r = []
for (var i = 0; i < 16; i++) {
r[i] = this.elements[i] + m.elements[i]
}
return new Matrix4x4(r)
},
minus: function (m) {
var r = []
for (var i = 0; i < 16; i++) {
r[i] = this.elements[i] - m.elements[i]
}
return new Matrix4x4(r)
},
// right multiply by another 4x4 matrix:
multiply: function (m) {
// cache elements in local variables, for speedup:
var this0 = this.elements[0]
var this1 = this.elements[1]
var this2 = this.elements[2]
var this3 = this.elements[3]
var this4 = this.elements[4]
var this5 = this.elements[5]
var this6 = this.elements[6]
var this7 = this.elements[7]
var this8 = this.elements[8]
var this9 = this.elements[9]
var this10 = this.elements[10]
var this11 = this.elements[11]
var this12 = this.elements[12]
var this13 = this.elements[13]
var this14 = this.elements[14]
var this15 = this.elements[15]
var m0 = m.elements[0]
var m1 = m.elements[1]
var m2 = m.elements[2]
var m3 = m.elements[3]
var m4 = m.elements[4]
var m5 = m.elements[5]
var m6 = m.elements[6]
var m7 = m.elements[7]
var m8 = m.elements[8]
var m9 = m.elements[9]
var m10 = m.elements[10]
var m11 = m.elements[11]
var m12 = m.elements[12]
var m13 = m.elements[13]
var m14 = m.elements[14]
var m15 = m.elements[15]
var result = []
result[0] = this0 * m0 + this1 * m4 + this2 * m8 + this3 * m12
result[1] = this0 * m1 + this1 * m5 + this2 * m9 + this3 * m13
result[2] = this0 * m2 + this1 * m6 + this2 * m10 + this3 * m14
result[3] = this0 * m3 + this1 * m7 + this2 * m11 + this3 * m15
result[4] = this4 * m0 + this5 * m4 + this6 * m8 + this7 * m12
result[5] = this4 * m1 + this5 * m5 + this6 * m9 + this7 * m13
result[6] = this4 * m2 + this5 * m6 + this6 * m10 + this7 * m14
result[7] = this4 * m3 + this5 * m7 + this6 * m11 + this7 * m15
result[8] = this8 * m0 + this9 * m4 + this10 * m8 + this11 * m12
result[9] = this8 * m1 + this9 * m5 + this10 * m9 + this11 * m13
result[10] = this8 * m2 + this9 * m6 + this10 * m10 + this11 * m14
result[11] = this8 * m3 + this9 * m7 + this10 * m11 + this11 * m15
result[12] = this12 * m0 + this13 * m4 + this14 * m8 + this15 * m12
result[13] = this12 * m1 + this13 * m5 + this14 * m9 + this15 * m13
result[14] = this12 * m2 + this13 * m6 + this14 * m10 + this15 * m14
result[15] = this12 * m3 + this13 * m7 + this14 * m11 + this15 * m15
return new Matrix4x4(result)
},
clone: function () {
var elements = this.elements.map(function (p) {
return p
})
return new Matrix4x4(elements)
},
// Right multiply the matrix by a Vector3D (interpreted as 3 row, 1 column)
// (result = M*v)
// Fourth element is taken as 1
rightMultiply1x3Vector: function (v) {
var v0 = v._x
var v1 = v._y
var v2 = v._z
var v3 = 1
var x = v0 * this.elements[0] + v1 * this.elements[1] + v2 * this.elements[2] + v3 * this.elements[3]
var y = v0 * this.elements[4] + v1 * this.elements[5] + v2 * this.elements[6] + v3 * this.elements[7]
var z = v0 * this.elements[8] + v1 * this.elements[9] + v2 * this.elements[10] + v3 * this.elements[11]
var w = v0 * this.elements[12] + v1 * this.elements[13] + v2 * this.elements[14] + v3 * this.elements[15]
// scale such that fourth element becomes 1:
if (w !== 1) {
var invw = 1.0 / w
x *= invw
y *= invw
z *= invw
}
return new Vector3D(x, y, z)
},
// Multiply a Vector3D (interpreted as 3 column, 1 row) by this matrix
// (result = v*M)
// Fourth element is taken as 1
leftMultiply1x3Vector: function (v) {
var v0 = v._x
var v1 = v._y
var v2 = v._z
var v3 = 1
var x = v0 * this.elements[0] + v1 * this.elements[4] + v2 * this.elements[8] + v3 * this.elements[12]
var y = v0 * this.elements[1] + v1 * this.elements[5] + v2 * this.elements[9] + v3 * this.elements[13]
var z = v0 * this.elements[2] + v1 * this.elements[6] + v2 * this.elements[10] + v3 * this.elements[14]
var w = v0 * this.elements[3] + v1 * this.elements[7] + v2 * this.elements[11] + v3 * this.elements[15]
// scale such that fourth element becomes 1:
if (w !== 1) {
var invw = 1.0 / w
x *= invw
y *= invw
z *= invw
}
return new Vector3D(x, y, z)
},
// Right multiply the matrix by a Vector2D (interpreted as 2 row, 1 column)
// (result = M*v)
// Fourth element is taken as 1
rightMultiply1x2Vector: function (v) {
var v0 = v.x
var v1 = v.y
var v2 = 0
var v3 = 1
var x = v0 * this.elements[0] + v1 * this.elements[1] + v2 * this.elements[2] + v3 * this.elements[3]
var y = v0 * this.elements[4] + v1 * this.elements[5] + v2 * this.elements[6] + v3 * this.elements[7]
var z = v0 * this.elements[8] + v1 * this.elements[9] + v2 * this.elements[10] + v3 * this.elements[11]
var w = v0 * this.elements[12] + v1 * this.elements[13] + v2 * this.elements[14] + v3 * this.elements[15]
// scale such that fourth element becomes 1:
if (w !== 1) {
var invw = 1.0 / w
x *= invw
y *= invw
z *= invw
}
return new Vector2D(x, y)
},
// Multiply a Vector2D (interpreted as 2 column, 1 row) by this matrix
// (result = v*M)
// Fourth element is taken as 1
leftMultiply1x2Vector: function (v) {
var v0 = v.x
var v1 = v.y
var v2 = 0
var v3 = 1
var x = v0 * this.elements[0] + v1 * this.elements[4] + v2 * this.elements[8] + v3 * this.elements[12]
var y = v0 * this.elements[1] + v1 * this.elements[5] + v2 * this.elements[9] + v3 * this.elements[13]
var z = v0 * this.elements[2] + v1 * this.elements[6] + v2 * this.elements[10] + v3 * this.elements[14]
var w = v0 * this.elements[3] + v1 * this.elements[7] + v2 * this.elements[11] + v3 * this.elements[15]
// scale such that fourth element becomes 1:
if (w !== 1) {
var invw = 1.0 / w
x *= invw
y *= invw
z *= invw
}
return new Vector2D(x, y)
},
// determine whether this matrix is a mirroring transformation
isMirroring: function () {
var u = new Vector3D(this.elements[0], this.elements[4], this.elements[8])
var v = new Vector3D(this.elements[1], this.elements[5], this.elements[9])
var w = new Vector3D(this.elements[2], this.elements[6], this.elements[10])
// for a true orthogonal, non-mirrored base, u.cross(v) == w
// If they have an opposite direction then we are mirroring
var mirrorvalue = u.cross(v).dot(w)
var ismirror = (mirrorvalue < 0)
return ismirror
}
}
// return the unity matrix
Matrix4x4.unity = function () {
return new Matrix4x4()
}
// Create a rotation matrix for rotating around the x axis
Matrix4x4.rotationX = function (degrees) {
var radians = degrees * Math.PI * (1.0 / 180.0)
var cos = Math.cos(radians)
var sin = Math.sin(radians)
var els = [
1, 0, 0, 0, 0, cos, sin, 0, 0, -sin, cos, 0, 0, 0, 0, 1
]
return new Matrix4x4(els)
}
// Create a rotation matrix for rotating around the y axis
Matrix4x4.rotationY = function (degrees) {
var radians = degrees * Math.PI * (1.0 / 180.0)
var cos = Math.cos(radians)
var sin = Math.sin(radians)
var els = [
cos, 0, -sin, 0, 0, 1, 0, 0, sin, 0, cos, 0, 0, 0, 0, 1
]
return new Matrix4x4(els)
}
// Create a rotation matrix for rotating around the z axis
Matrix4x4.rotationZ = function (degrees) {
var radians = degrees * Math.PI * (1.0 / 180.0)
var cos = Math.cos(radians)
var sin = Math.sin(radians)
var els = [
cos, sin, 0, 0, -sin, cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
]
return new Matrix4x4(els)
}
// Matrix for rotation about arbitrary point and axis
Matrix4x4.rotation = function (rotationCenter, rotationAxis, degrees) {
rotationCenter = new Vector3D(rotationCenter)
rotationAxis = new Vector3D(rotationAxis)
var rotationPlane = Plane.fromNormalAndPoint(rotationAxis, rotationCenter)
var orthobasis = new OrthoNormalBasis(rotationPlane)
var transformation = Matrix4x4.translation(rotationCenter.negated())
transformation = transformation.multiply(orthobasis.getProjectionMatrix())
transformation = transformation.multiply(Matrix4x4.rotationZ(degrees))
transformation = transformation.multiply(orthobasis.getInverseProjectionMatrix())
transformation = transformation.multiply(Matrix4x4.translation(rotationCenter))
return transformation
}
// Create an affine matrix for translation:
Matrix4x4.translation = function (v) {
// parse as Vector3D, so we can pass an array or a Vector3D
var vec = new Vector3D(v)
var els = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, vec.x, vec.y, vec.z, 1]
return new Matrix4x4(els)
}
// Create an affine matrix for mirroring into an arbitrary plane:
Matrix4x4.mirroring = function (plane) {
var nx = plane.normal.x
var ny = plane.normal.y
var nz = plane.normal.z
var w = plane.w
var els = [
(1.0 - 2.0 * nx * nx), (-2.0 * ny * nx), (-2.0 * nz * nx), 0,
(-2.0 * nx * ny), (1.0 - 2.0 * ny * ny), (-2.0 * nz * ny), 0,
(-2.0 * nx * nz), (-2.0 * ny * nz), (1.0 - 2.0 * nz * nz), 0,
(2.0 * nx * w), (2.0 * ny * w), (2.0 * nz * w), 1
]
return new Matrix4x4(els)
}
// Create an affine matrix for scaling:
Matrix4x4.scaling = function (v) {
// parse as Vector3D, so we can pass an array or a Vector3D
var vec = new Vector3D(v)
var els = [
vec.x, 0, 0, 0, 0, vec.y, 0, 0, 0, 0, vec.z, 0, 0, 0, 0, 1
]
return new Matrix4x4(els)
}
module.exports = Matrix4x4

View File

@ -1,202 +0,0 @@
const Vector2D = require('./Vector2')
const Vector3D = require('./Vector3')
const Line2D = require('./Line2')
const Line3D = require('./Line3')
const Plane = require('./Plane')
// # class OrthoNormalBasis
// Reprojects points on a 3D plane onto a 2D plane
// or from a 2D plane back onto the 3D plane
const OrthoNormalBasis = function (plane, rightvector) {
if (arguments.length < 2) {
// choose an arbitrary right hand vector, making sure it is somewhat orthogonal to the plane normal:
rightvector = plane.normal.randomNonParallelVector()
} else {
rightvector = new Vector3D(rightvector)
}
this.v = plane.normal.cross(rightvector).unit()
this.u = this.v.cross(plane.normal)
this.plane = plane
this.planeorigin = plane.normal.times(plane.w)
}
// Get an orthonormal basis for the standard XYZ planes.
// Parameters: the names of two 3D axes. The 2d x axis will map to the first given 3D axis, the 2d y
// axis will map to the second.
// Prepend the axis with a "-" to invert the direction of this axis.
// For example: OrthoNormalBasis.GetCartesian("-Y","Z")
// will return an orthonormal basis where the 2d X axis maps to the 3D inverted Y axis, and
// the 2d Y axis maps to the 3D Z axis.
OrthoNormalBasis.GetCartesian = function (xaxisid, yaxisid) {
let axisid = xaxisid + '/' + yaxisid
let planenormal, rightvector
if (axisid === 'X/Y') {
planenormal = [0, 0, 1]
rightvector = [1, 0, 0]
} else if (axisid === 'Y/-X') {
planenormal = [0, 0, 1]
rightvector = [0, 1, 0]
} else if (axisid === '-X/-Y') {
planenormal = [0, 0, 1]
rightvector = [-1, 0, 0]
} else if (axisid === '-Y/X') {
planenormal = [0, 0, 1]
rightvector = [0, -1, 0]
} else if (axisid === '-X/Y') {
planenormal = [0, 0, -1]
rightvector = [-1, 0, 0]
} else if (axisid === '-Y/-X') {
planenormal = [0, 0, -1]
rightvector = [0, -1, 0]
} else if (axisid === 'X/-Y') {
planenormal = [0, 0, -1]
rightvector = [1, 0, 0]
} else if (axisid === 'Y/X') {
planenormal = [0, 0, -1]
rightvector = [0, 1, 0]
} else if (axisid === 'X/Z') {
planenormal = [0, -1, 0]
rightvector = [1, 0, 0]
} else if (axisid === 'Z/-X') {
planenormal = [0, -1, 0]
rightvector = [0, 0, 1]
} else if (axisid === '-X/-Z') {
planenormal = [0, -1, 0]
rightvector = [-1, 0, 0]
} else if (axisid === '-Z/X') {
planenormal = [0, -1, 0]
rightvector = [0, 0, -1]
} else if (axisid === '-X/Z') {
planenormal = [0, 1, 0]
rightvector = [-1, 0, 0]
} else if (axisid === '-Z/-X') {
planenormal = [0, 1, 0]
rightvector = [0, 0, -1]
} else if (axisid === 'X/-Z') {
planenormal = [0, 1, 0]
rightvector = [1, 0, 0]
} else if (axisid === 'Z/X') {
planenormal = [0, 1, 0]
rightvector = [0, 0, 1]
} else if (axisid === 'Y/Z') {
planenormal = [1, 0, 0]
rightvector = [0, 1, 0]
} else if (axisid === 'Z/-Y') {
planenormal = [1, 0, 0]
rightvector = [0, 0, 1]
} else if (axisid === '-Y/-Z') {
planenormal = [1, 0, 0]
rightvector = [0, -1, 0]
} else if (axisid === '-Z/Y') {
planenormal = [1, 0, 0]
rightvector = [0, 0, -1]
} else if (axisid === '-Y/Z') {
planenormal = [-1, 0, 0]
rightvector = [0, -1, 0]
} else if (axisid === '-Z/-Y') {
planenormal = [-1, 0, 0]
rightvector = [0, 0, -1]
} else if (axisid === 'Y/-Z') {
planenormal = [-1, 0, 0]
rightvector = [0, 1, 0]
} else if (axisid === 'Z/Y') {
planenormal = [-1, 0, 0]
rightvector = [0, 0, 1]
} else {
throw new Error('OrthoNormalBasis.GetCartesian: invalid combination of axis identifiers. Should pass two string arguments from [X,Y,Z,-X,-Y,-Z], being two different axes.')
}
return new OrthoNormalBasis(new Plane(new Vector3D(planenormal), 0), new Vector3D(rightvector))
}
/*
// test code for OrthoNormalBasis.GetCartesian()
OrthoNormalBasis.GetCartesian_Test=function() {
let axisnames=["X","Y","Z","-X","-Y","-Z"];
let axisvectors=[[1,0,0], [0,1,0], [0,0,1], [-1,0,0], [0,-1,0], [0,0,-1]];
for(let axis1=0; axis1 < 3; axis1++) {
for(let axis1inverted=0; axis1inverted < 2; axis1inverted++) {
let axis1name=axisnames[axis1+3*axis1inverted];
let axis1vector=axisvectors[axis1+3*axis1inverted];
for(let axis2=0; axis2 < 3; axis2++) {
if(axis2 != axis1) {
for(let axis2inverted=0; axis2inverted < 2; axis2inverted++) {
let axis2name=axisnames[axis2+3*axis2inverted];
let axis2vector=axisvectors[axis2+3*axis2inverted];
let orthobasis=OrthoNormalBasis.GetCartesian(axis1name, axis2name);
let test1=orthobasis.to3D(new Vector2D([1,0]));
let test2=orthobasis.to3D(new Vector2D([0,1]));
let expected1=new Vector3D(axis1vector);
let expected2=new Vector3D(axis2vector);
let d1=test1.distanceTo(expected1);
let d2=test2.distanceTo(expected2);
if( (d1 > 0.01) || (d2 > 0.01) ) {
throw new Error("Wrong!");
}}}}}}
throw new Error("OK");
};
*/
// The z=0 plane, with the 3D x and y vectors mapped to the 2D x and y vector
OrthoNormalBasis.Z0Plane = function () {
let plane = new Plane(new Vector3D([0, 0, 1]), 0)
return new OrthoNormalBasis(plane, new Vector3D([1, 0, 0]))
}
OrthoNormalBasis.prototype = {
getProjectionMatrix: function () {
const Matrix4x4 = require('./Matrix4') // FIXME: circular dependencies Matrix=>OrthoNormalBasis => Matrix
return new Matrix4x4([
this.u.x, this.v.x, this.plane.normal.x, 0,
this.u.y, this.v.y, this.plane.normal.y, 0,
this.u.z, this.v.z, this.plane.normal.z, 0,
0, 0, -this.plane.w, 1
])
},
getInverseProjectionMatrix: function () {
const Matrix4x4 = require('./Matrix4') // FIXME: circular dependencies Matrix=>OrthoNormalBasis => Matrix
let p = this.plane.normal.times(this.plane.w)
return new Matrix4x4([
this.u.x, this.u.y, this.u.z, 0,
this.v.x, this.v.y, this.v.z, 0,
this.plane.normal.x, this.plane.normal.y, this.plane.normal.z, 0,
p.x, p.y, p.z, 1
])
},
to2D: function (vec3) {
return new Vector2D(vec3.dot(this.u), vec3.dot(this.v))
},
to3D: function (vec2) {
return this.planeorigin.plus(this.u.times(vec2.x)).plus(this.v.times(vec2.y))
},
line3Dto2D: function (line3d) {
let a = line3d.point
let b = line3d.direction.plus(a)
let a2d = this.to2D(a)
let b2d = this.to2D(b)
return Line2D.fromPoints(a2d, b2d)
},
line2Dto3D: function (line2d) {
let a = line2d.origin()
let b = line2d.direction().plus(a)
let a3d = this.to3D(a)
let b3d = this.to3D(b)
return Line3D.fromPoints(a3d, b3d)
},
transform: function (matrix4x4) {
// todo: this may not work properly in case of mirroring
let newplane = this.plane.transform(matrix4x4)
let rightpointTransformed = this.u.transform(matrix4x4)
let originTransformed = new Vector3D(0, 0, 0).transform(matrix4x4)
let newrighthandvector = rightpointTransformed.minus(originTransformed)
let newbasis = new OrthoNormalBasis(newplane, newrighthandvector)
return newbasis
}
}
module.exports = OrthoNormalBasis

View File

@ -1,472 +0,0 @@
const Vector2D = require('./Vector2')
const {EPS, angleEPS} = require('../constants')
const {parseOptionAs2DVector, parseOptionAsFloat, parseOptionAsInt, parseOptionAsBool} = require('../optionParsers')
const {defaultResolution2D} = require('../constants')
const Vertex = require('./Vertex2')
const Side = require('./Side')
/** Class Path2D
* Represents a series of points, connected by infinitely thin lines.
* A path can be open or closed, i.e. additional line between first and last points.
* The difference between Path2D and CAG is that a path is a 'thin' line, whereas a CAG is an enclosed area.
* @constructor
* @param {Vector2D[]} [points=[]] - list of points
* @param {boolean} [closed=false] - closer of path
*
* @example
* new CSG.Path2D()
* new CSG.Path2D([[10,10], [-10,10], [-10,-10], [10,-10]], true) // closed
*/
const Path2D = function (points, closed) {
closed = !!closed
points = points || []
// re-parse the points into Vector2D
// and remove any duplicate points
let prevpoint = null
if (closed && (points.length > 0)) {
prevpoint = new Vector2D(points[points.length - 1])
}
let newpoints = []
points.map(function (point) {
point = new Vector2D(point)
let skip = false
if (prevpoint !== null) {
let distance = point.distanceTo(prevpoint)
skip = distance < EPS
}
if (!skip) newpoints.push(point)
prevpoint = point
})
this.points = newpoints
this.closed = closed
}
/** Construct an arc.
* @param {Object} [options] - options for construction
* @param {Vector2D} [options.center=[0,0]] - center of circle
* @param {Number} [options.radius=1] - radius of circle
* @param {Number} [options.startangle=0] - starting angle of the arc, in degrees
* @param {Number} [options.endangle=360] - ending angle of the arc, in degrees
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @param {Boolean} [options.maketangent=false] - adds line segments at both ends of the arc to ensure that the gradients at the edges are tangent
* @returns {Path2D} new Path2D object (not closed)
*
* @example
* let path = CSG.Path2D.arc({
* center: [5, 5],
* radius: 10,
* startangle: 90,
* endangle: 180,
* resolution: 36,
* maketangent: true
* });
*/
Path2D.arc = function (options) {
let center = parseOptionAs2DVector(options, 'center', 0)
let radius = parseOptionAsFloat(options, 'radius', 1)
let startangle = parseOptionAsFloat(options, 'startangle', 0)
let endangle = parseOptionAsFloat(options, 'endangle', 360)
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution2D)
let maketangent = parseOptionAsBool(options, 'maketangent', false)
// no need to make multiple turns:
while (endangle - startangle >= 720) {
endangle -= 360
}
while (endangle - startangle <= -720) {
endangle += 360
}
let points = []
let point
let absangledif = Math.abs(endangle - startangle)
if (absangledif < angleEPS) {
point = Vector2D.fromAngle(startangle / 180.0 * Math.PI).times(radius)
points.push(point.plus(center))
} else {
let numsteps = Math.floor(resolution * absangledif / 360) + 1
let edgestepsize = numsteps * 0.5 / absangledif // step size for half a degree
if (edgestepsize > 0.25) edgestepsize = 0.25
let numstepsMod = maketangent ? (numsteps + 2) : numsteps
for (let i = 0; i <= numstepsMod; i++) {
let step = i
if (maketangent) {
step = (i - 1) * (numsteps - 2 * edgestepsize) / numsteps + edgestepsize
if (step < 0) step = 0
if (step > numsteps) step = numsteps
}
let angle = startangle + step * (endangle - startangle) / numsteps
point = Vector2D.fromAngle(angle / 180.0 * Math.PI).times(radius)
points.push(point.plus(center))
}
}
return new Path2D(points, false)
}
Path2D.prototype = {
concat: function (otherpath) {
if (this.closed || otherpath.closed) {
throw new Error('Paths must not be closed')
}
let newpoints = this.points.concat(otherpath.points)
return new Path2D(newpoints)
},
/**
* Get the points that make up the path.
* note that this is current internal list of points, not an immutable copy.
* @returns {Vector2[]} array of points the make up the path
*/
getPoints: function() {
return this.points;
},
/**
* Append an point to the end of the path.
* @param {Vector2D} point - point to append
* @returns {Path2D} new Path2D object (not closed)
*/
appendPoint: function (point) {
if (this.closed) {
throw new Error('Path must not be closed')
}
point = new Vector2D(point) // cast to Vector2D
let newpoints = this.points.concat([point])
return new Path2D(newpoints)
},
/**
* Append a list of points to the end of the path.
* @param {Vector2D[]} points - points to append
* @returns {Path2D} new Path2D object (not closed)
*/
appendPoints: function (points) {
if (this.closed) {
throw new Error('Path must not be closed')
}
let newpoints = this.points
points.forEach(function (point) {
newpoints.push(new Vector2D(point)) // cast to Vector2D
})
return new Path2D(newpoints)
},
close: function () {
return new Path2D(this.points, true)
},
/**
* Determine if the path is a closed or not.
* @returns {Boolean} true when the path is closed, otherwise false
*/
isClosed: function() {
return this.closed
},
// Extrude the path by following it with a rectangle (upright, perpendicular to the path direction)
// Returns a CSG solid
// width: width of the extrusion, in the z=0 plane
// height: height of the extrusion in the z direction
// resolution: number of segments per 360 degrees for the curve in a corner
rectangularExtrude: function (width, height, resolution) {
let cag = this.expandToCAG(width / 2, resolution)
let result = cag.extrude({
offset: [0, 0, height]
})
return result
},
// Expand the path to a CAG
// This traces the path with a circle with radius pathradius
expandToCAG: function (pathradius, resolution) {
const CAG = require('../CAG') // FIXME: cyclic dependencies CAG => PATH2 => CAG
let sides = []
let numpoints = this.points.length
let startindex = 0
if (this.closed && (numpoints > 2)) startindex = -1
let prevvertex
for (let i = startindex; i < numpoints; i++) {
let pointindex = i
if (pointindex < 0) pointindex = numpoints - 1
let point = this.points[pointindex]
let vertex = new Vertex(point)
if (i > startindex) {
let side = new Side(prevvertex, vertex)
sides.push(side)
}
prevvertex = vertex
}
let shellcag = CAG.fromSides(sides)
let expanded = shellcag.expandedShell(pathradius, resolution)
return expanded
},
innerToCAG: function() {
const CAG = require('../CAG') // FIXME: cyclic dependencies CAG => PATH2 => CAG
if (!this.closed) throw new Error("The path should be closed!");
return CAG.fromPoints(this.points);
},
transform: function (matrix4x4) {
let newpoints = this.points.map(function (point) {
return point.multiply4x4(matrix4x4)
})
return new Path2D(newpoints, this.closed)
},
/**
* Append a Bezier curve to the end of the path, using the control points to transition the curve through start and end points.
* <br>
* The Bézier curve starts at the last point in the path,
* and ends at the last given control point. Other control points are intermediate control points.
* <br>
* The first control point may be null to ensure a smooth transition occurs. In this case,
* the second to last control point of the path is mirrored into the control points of the Bezier curve.
* In other words, the trailing gradient of the path matches the new gradient of the curve.
* @param {Vector2D[]} controlpoints - list of control points
* @param {Object} [options] - options for construction
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @returns {Path2D} new Path2D object (not closed)
*
* @example
* let p5 = new CSG.Path2D([[10,-20]],false);
* p5 = p5.appendBezier([[10,-10],[25,-10],[25,-20]]);
* p5 = p5.appendBezier([[25,-30],[40,-30],[40,-20]]);
*/
appendBezier: function (controlpoints, options) {
if (arguments.length < 2) {
options = {}
}
if (this.closed) {
throw new Error('Path must not be closed')
}
if (!(controlpoints instanceof Array)) {
throw new Error('appendBezier: should pass an array of control points')
}
if (controlpoints.length < 1) {
throw new Error('appendBezier: need at least 1 control point')
}
if (this.points.length < 1) {
throw new Error('appendBezier: path must already contain a point (the endpoint of the path is used as the starting point for the bezier curve)')
}
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution2D)
if (resolution < 4) resolution = 4
let factorials = []
let controlpointsParsed = []
controlpointsParsed.push(this.points[this.points.length - 1]) // start at the previous end point
for (let i = 0; i < controlpoints.length; ++i) {
let p = controlpoints[i]
if (p === null) {
// we can pass null as the first control point. In that case a smooth gradient is ensured:
if (i !== 0) {
throw new Error('appendBezier: null can only be passed as the first control point')
}
if (controlpoints.length < 2) {
throw new Error('appendBezier: null can only be passed if there is at least one more control point')
}
let lastBezierControlPoint
if ('lastBezierControlPoint' in this) {
lastBezierControlPoint = this.lastBezierControlPoint
} else {
if (this.points.length < 2) {
throw new Error('appendBezier: null is passed as a control point but this requires a previous bezier curve or at least two points in the existing path')
}
lastBezierControlPoint = this.points[this.points.length - 2]
}
// mirror the last bezier control point:
p = this.points[this.points.length - 1].times(2).minus(lastBezierControlPoint)
} else {
p = new Vector2D(p) // cast to Vector2D
}
controlpointsParsed.push(p)
}
let bezierOrder = controlpointsParsed.length - 1
let fact = 1
for (let i = 0; i <= bezierOrder; ++i) {
if (i > 0) fact *= i
factorials.push(fact)
}
let binomials = []
for (let i = 0; i <= bezierOrder; ++i) {
let binomial = factorials[bezierOrder] / (factorials[i] * factorials[bezierOrder - i])
binomials.push(binomial)
}
let getPointForT = function (t) {
let t_k = 1 // = pow(t,k)
let one_minus_t_n_minus_k = Math.pow(1 - t, bezierOrder) // = pow( 1-t, bezierOrder - k)
let inv_1_minus_t = (t !== 1) ? (1 / (1 - t)) : 1
let point = new Vector2D(0, 0)
for (let k = 0; k <= bezierOrder; ++k) {
if (k === bezierOrder) one_minus_t_n_minus_k = 1
let bernstein_coefficient = binomials[k] * t_k * one_minus_t_n_minus_k
point = point.plus(controlpointsParsed[k].times(bernstein_coefficient))
t_k *= t
one_minus_t_n_minus_k *= inv_1_minus_t
}
return point
}
let newpoints = []
let newpoints_t = []
let numsteps = bezierOrder + 1
for (let i = 0; i < numsteps; ++i) {
let t = i / (numsteps - 1)
let point = getPointForT(t)
newpoints.push(point)
newpoints_t.push(t)
}
// subdivide each segment until the angle at each vertex becomes small enough:
let subdivideBase = 1
let maxangle = Math.PI * 2 / resolution // segments may have differ no more in angle than this
let maxsinangle = Math.sin(maxangle)
while (subdivideBase < newpoints.length - 1) {
let dir1 = newpoints[subdivideBase].minus(newpoints[subdivideBase - 1]).unit()
let dir2 = newpoints[subdivideBase + 1].minus(newpoints[subdivideBase]).unit()
let sinangle = dir1.cross(dir2) // this is the sine of the angle
if (Math.abs(sinangle) > maxsinangle) {
// angle is too big, we need to subdivide
let t0 = newpoints_t[subdivideBase - 1]
let t1 = newpoints_t[subdivideBase + 1]
let t0_new = t0 + (t1 - t0) * 1 / 3
let t1_new = t0 + (t1 - t0) * 2 / 3
let point0_new = getPointForT(t0_new)
let point1_new = getPointForT(t1_new)
// remove the point at subdivideBase and replace with 2 new points:
newpoints.splice(subdivideBase, 1, point0_new, point1_new)
newpoints_t.splice(subdivideBase, 1, t0_new, t1_new)
// re - evaluate the angles, starting at the previous junction since it has changed:
subdivideBase--
if (subdivideBase < 1) subdivideBase = 1
} else {
++subdivideBase
}
}
// append to the previous points, but skip the first new point because it is identical to the last point:
newpoints = this.points.concat(newpoints.slice(1))
let result = new Path2D(newpoints)
result.lastBezierControlPoint = controlpointsParsed[controlpointsParsed.length - 2]
return result
},
/**
* Append an arc to the end of the path.
* This implementation follows the SVG arc specs. For the details see
* http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
* @param {Vector2D} endpoint - end point of arc
* @param {Object} [options] - options for construction
* @param {Number} [options.radius=0] - radius of arc (X and Y), see also xradius and yradius
* @param {Number} [options.xradius=0] - X radius of arc, see also radius
* @param {Number} [options.yradius=0] - Y radius of arc, see also radius
* @param {Number} [options.xaxisrotation=0] - rotation (in degrees) of the X axis of the arc with respect to the X axis of the coordinate system
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @param {Boolean} [options.clockwise=false] - draw an arc clockwise with respect to the center point
* @param {Boolean} [options.large=false] - draw an arc longer than 180 degrees
* @returns {Path2D} new Path2D object (not closed)
*
* @example
* let p1 = new CSG.Path2D([[27.5,-22.96875]],false);
* p1 = p1.appendPoint([27.5,-3.28125]);
* p1 = p1.appendArc([12.5,-22.96875],{xradius: 15,yradius: -19.6875,xaxisrotation: 0,clockwise: false,large: false});
* p1 = p1.close();
*/
appendArc: function (endpoint, options) {
let decimals = 100000
if (arguments.length < 2) {
options = {}
}
if (this.closed) {
throw new Error('Path must not be closed')
}
if (this.points.length < 1) {
throw new Error('appendArc: path must already contain a point (the endpoint of the path is used as the starting point for the arc)')
}
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution2D)
if (resolution < 4) resolution = 4
let xradius, yradius
if (('xradius' in options) || ('yradius' in options)) {
if ('radius' in options) {
throw new Error('Should either give an xradius and yradius parameter, or a radius parameter')
}
xradius = parseOptionAsFloat(options, 'xradius', 0)
yradius = parseOptionAsFloat(options, 'yradius', 0)
} else {
xradius = parseOptionAsFloat(options, 'radius', 0)
yradius = xradius
}
let xaxisrotation = parseOptionAsFloat(options, 'xaxisrotation', 0)
let clockwise = parseOptionAsBool(options, 'clockwise', false)
let largearc = parseOptionAsBool(options, 'large', false)
let startpoint = this.points[this.points.length - 1]
endpoint = new Vector2D(endpoint)
// round to precision in order to have determinate calculations
xradius = Math.round(xradius * decimals) / decimals
yradius = Math.round(yradius * decimals) / decimals
endpoint = new Vector2D(Math.round(endpoint.x * decimals) / decimals, Math.round(endpoint.y * decimals) / decimals)
let sweepFlag = !clockwise
let newpoints = []
if ((xradius === 0) || (yradius === 0)) {
// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes:
// If rx = 0 or ry = 0, then treat this as a straight line from (x1, y1) to (x2, y2) and stop
newpoints.push(endpoint)
} else {
xradius = Math.abs(xradius)
yradius = Math.abs(yradius)
// see http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes :
let phi = xaxisrotation * Math.PI / 180.0
let cosphi = Math.cos(phi)
let sinphi = Math.sin(phi)
let minushalfdistance = startpoint.minus(endpoint).times(0.5)
// F.6.5.1:
// round to precision in order to have determinate calculations
let x = Math.round((cosphi * minushalfdistance.x + sinphi * minushalfdistance.y) * decimals) / decimals
let y = Math.round((-sinphi * minushalfdistance.x + cosphi * minushalfdistance.y) * decimals) / decimals
let startTranslated = new Vector2D(x, y)
// F.6.6.2:
let biglambda = (startTranslated.x * startTranslated.x) / (xradius * xradius) + (startTranslated.y * startTranslated.y) / (yradius * yradius)
if (biglambda > 1.0) {
// F.6.6.3:
let sqrtbiglambda = Math.sqrt(biglambda)
xradius *= sqrtbiglambda
yradius *= sqrtbiglambda
// round to precision in order to have determinate calculations
xradius = Math.round(xradius * decimals) / decimals
yradius = Math.round(yradius * decimals) / decimals
}
// F.6.5.2:
let multiplier1 = Math.sqrt((xradius * xradius * yradius * yradius - xradius * xradius * startTranslated.y * startTranslated.y - yradius * yradius * startTranslated.x * startTranslated.x) / (xradius * xradius * startTranslated.y * startTranslated.y + yradius * yradius * startTranslated.x * startTranslated.x))
if (sweepFlag === largearc) multiplier1 = -multiplier1
let centerTranslated = new Vector2D(xradius * startTranslated.y / yradius, -yradius * startTranslated.x / xradius).times(multiplier1)
// F.6.5.3:
let center = new Vector2D(cosphi * centerTranslated.x - sinphi * centerTranslated.y, sinphi * centerTranslated.x + cosphi * centerTranslated.y).plus((startpoint.plus(endpoint)).times(0.5))
// F.6.5.5:
let vec1 = new Vector2D((startTranslated.x - centerTranslated.x) / xradius, (startTranslated.y - centerTranslated.y) / yradius)
let vec2 = new Vector2D((-startTranslated.x - centerTranslated.x) / xradius, (-startTranslated.y - centerTranslated.y) / yradius)
let theta1 = vec1.angleRadians()
let theta2 = vec2.angleRadians()
let deltatheta = theta2 - theta1
deltatheta = deltatheta % (2 * Math.PI)
if ((!sweepFlag) && (deltatheta > 0)) {
deltatheta -= 2 * Math.PI
} else if ((sweepFlag) && (deltatheta < 0)) {
deltatheta += 2 * Math.PI
}
// Ok, we have the center point and angle range (from theta1, deltatheta radians) so we can create the ellipse
let numsteps = Math.ceil(Math.abs(deltatheta) / (2 * Math.PI) * resolution) + 1
if (numsteps < 1) numsteps = 1
for (let step = 1; step <= numsteps; step++) {
let theta = theta1 + step / numsteps * deltatheta
let costheta = Math.cos(theta)
let sintheta = Math.sin(theta)
// F.6.3.1:
let point = new Vector2D(cosphi * xradius * costheta - sinphi * yradius * sintheta, sinphi * xradius * costheta + cosphi * yradius * sintheta).plus(center)
newpoints.push(point)
}
}
newpoints = this.points.concat(newpoints)
let result = new Path2D(newpoints)
return result
}
}
module.exports = Path2D

View File

@ -1,140 +0,0 @@
const Vector3D = require('./Vector3')
const Line3D = require('./Line3')
const {EPS, getTag} = require('../constants')
// # class Plane
// Represents a plane in 3D space.
const Plane = function (normal, w) {
this.normal = normal
this.w = w
}
// create from an untyped object with identical property names:
Plane.fromObject = function (obj) {
let normal = new Vector3D(obj.normal)
let w = parseFloat(obj.w)
return new Plane(normal, w)
}
Plane.fromVector3Ds = function (a, b, c) {
let n = b.minus(a).cross(c.minus(a)).unit()
return new Plane(n, n.dot(a))
}
// like fromVector3Ds, but allow the vectors to be on one point or one line
// in such a case a random plane through the given points is constructed
Plane.anyPlaneFromVector3Ds = function (a, b, c) {
let v1 = b.minus(a)
let v2 = c.minus(a)
if (v1.length() < EPS) {
v1 = v2.randomNonParallelVector()
}
if (v2.length() < EPS) {
v2 = v1.randomNonParallelVector()
}
let normal = v1.cross(v2)
if (normal.length() < EPS) {
// this would mean that v1 == v2.negated()
v2 = v1.randomNonParallelVector()
normal = v1.cross(v2)
}
normal = normal.unit()
return new Plane(normal, normal.dot(a))
}
Plane.fromPoints = function (a, b, c) {
a = new Vector3D(a)
b = new Vector3D(b)
c = new Vector3D(c)
return Plane.fromVector3Ds(a, b, c)
}
Plane.fromNormalAndPoint = function (normal, point) {
normal = new Vector3D(normal)
point = new Vector3D(point)
normal = normal.unit()
let w = point.dot(normal)
return new Plane(normal, w)
}
Plane.prototype = {
flipped: function () {
return new Plane(this.normal.negated(), -this.w)
},
getTag: function () {
let result = this.tag
if (!result) {
result = getTag()
this.tag = result
}
return result
},
equals: function (n) {
return this.normal.equals(n.normal) && this.w === n.w
},
transform: function (matrix4x4) {
let ismirror = matrix4x4.isMirroring()
// get two vectors in the plane:
let r = this.normal.randomNonParallelVector()
let u = this.normal.cross(r)
let v = this.normal.cross(u)
// get 3 points in the plane:
let point1 = this.normal.times(this.w)
let point2 = point1.plus(u)
let point3 = point1.plus(v)
// transform the points:
point1 = point1.multiply4x4(matrix4x4)
point2 = point2.multiply4x4(matrix4x4)
point3 = point3.multiply4x4(matrix4x4)
// and create a new plane from the transformed points:
let newplane = Plane.fromVector3Ds(point1, point2, point3)
if (ismirror) {
// the transform is mirroring
// We should mirror the plane:
newplane = newplane.flipped()
}
return newplane
},
// robust splitting of a line by a plane
// will work even if the line is parallel to the plane
splitLineBetweenPoints: function (p1, p2) {
let direction = p2.minus(p1)
let labda = (this.w - this.normal.dot(p1)) / this.normal.dot(direction)
if (isNaN(labda)) labda = 0
if (labda > 1) labda = 1
if (labda < 0) labda = 0
let result = p1.plus(direction.times(labda))
return result
},
// returns Vector3D
intersectWithLine: function (line3d) {
return line3d.intersectWithPlane(this)
},
// intersection of two planes
intersectWithPlane: function (plane) {
return Line3D.fromPlanes(this, plane)
},
signedDistanceToPoint: function (point) {
let t = this.normal.dot(point) - this.w
return t
},
toString: function () {
return '[normal: ' + this.normal.toString() + ', w: ' + this.w + ']'
},
mirrorPoint: function (point3d) {
let distance = this.signedDistanceToPoint(point3d)
let mirrored = point3d.minus(this.normal.times(distance * 2.0))
return mirrored
}
}
module.exports = Plane

View File

@ -1,19 +0,0 @@
const CAG = require('../CAG')
/*
2D polygons are now supported through the CAG class.
With many improvements (see documentation):
- shapes do no longer have to be convex
- union/intersect/subtract is supported
- expand / contract are supported
But we'll keep CSG.Polygon2D as a stub for backwards compatibility
*/
function Polygon2D (points) {
const cag = CAG.fromPoints(points)
this.sides = cag.sides
}
Polygon2D.prototype = CAG.prototype
module.exports = Polygon2D

View File

@ -1,575 +0,0 @@
const Vector3D = require('./Vector3')
const Vertex = require('./Vertex3')
const Matrix4x4 = require('./Matrix4')
const {_CSGDEBUG, EPS, getTag, areaEPS} = require('../constants')
const {fnSortByIndex} = require('../utils')
/** Class Polygon
* Represents a convex polygon. The vertices used to initialize a polygon must
* be coplanar and form a convex loop. They do not have to be `Vertex`
* instances but they must behave similarly (duck typing can be used for
* customization).
* <br>
* Each convex polygon has a `shared` property, which is shared between all
* polygons that are clones of each other or were split from the same polygon.
* This can be used to define per-polygon properties (such as surface color).
* <br>
* The plane of the polygon is calculated from the vertex coordinates if not provided.
* The plane can alternatively be passed as the third argument to avoid calculations.
*
* @constructor
* @param {Vertex[]} vertices - list of vertices
* @param {Polygon.Shared} [shared=defaultShared] - shared property to apply
* @param {Plane} [plane] - plane of the polygon
*
* @example
* const vertices = [
* new CSG.Vertex(new CSG.Vector3D([0, 0, 0])),
* new CSG.Vertex(new CSG.Vector3D([0, 10, 0])),
* new CSG.Vertex(new CSG.Vector3D([0, 10, 10]))
* ]
* let observed = new Polygon(vertices)
*/
let Polygon = function (vertices, shared, plane) {
this.vertices = vertices
if (!shared) shared = Polygon.defaultShared
this.shared = shared
// let numvertices = vertices.length;
if (arguments.length >= 3) {
this.plane = plane
} else {
const Plane = require('./Plane') // FIXME: circular dependencies
this.plane = Plane.fromVector3Ds(vertices[0].pos, vertices[1].pos, vertices[2].pos)
}
if (_CSGDEBUG) {
if (!this.checkIfConvex()) {
throw new Error('Not convex!')
}
}
}
// create from an untyped object with identical property names:
Polygon.fromObject = function (obj) {
const Plane = require('./Plane') // FIXME: circular dependencies
let vertices = obj.vertices.map(function (v) {
return Vertex.fromObject(v)
})
let shared = Polygon.Shared.fromObject(obj.shared)
let plane = Plane.fromObject(obj.plane)
return new Polygon(vertices, shared, plane)
}
Polygon.prototype = {
/** Check whether the polygon is convex. (it should be, otherwise we will get unexpected results)
* @returns {boolean}
*/
checkIfConvex: function () {
return Polygon.verticesConvex(this.vertices, this.plane.normal)
},
// FIXME what? why does this return this, and not a new polygon?
// FIXME is this used?
setColor: function (args) {
let newshared = Polygon.Shared.fromColor.apply(this, arguments)
this.shared = newshared
return this
},
getSignedVolume: function () {
let signedVolume = 0
for (let i = 0; i < this.vertices.length - 2; i++) {
signedVolume += this.vertices[0].pos.dot(this.vertices[i + 1].pos
.cross(this.vertices[i + 2].pos))
}
signedVolume /= 6
return signedVolume
},
// Note: could calculate vectors only once to speed up
getArea: function () {
let polygonArea = 0
for (let i = 0; i < this.vertices.length - 2; i++) {
polygonArea += this.vertices[i + 1].pos.minus(this.vertices[0].pos)
.cross(this.vertices[i + 2].pos.minus(this.vertices[i + 1].pos)).length()
}
polygonArea /= 2
return polygonArea
},
// accepts array of features to calculate
// returns array of results
getTetraFeatures: function (features) {
let result = []
features.forEach(function (feature) {
if (feature === 'volume') {
result.push(this.getSignedVolume())
} else if (feature === 'area') {
result.push(this.getArea())
}
}, this)
return result
},
// Extrude a polygon into the direction offsetvector
// Returns a CSG object
extrude: function (offsetvector) {
const CSG = require('../CSG') // because of circular dependencies
let newpolygons = []
let polygon1 = this
let direction = polygon1.plane.normal.dot(offsetvector)
if (direction > 0) {
polygon1 = polygon1.flipped()
}
newpolygons.push(polygon1)
let polygon2 = polygon1.translate(offsetvector)
let numvertices = this.vertices.length
for (let i = 0; i < numvertices; i++) {
let sidefacepoints = []
let nexti = (i < (numvertices - 1)) ? i + 1 : 0
sidefacepoints.push(polygon1.vertices[i].pos)
sidefacepoints.push(polygon2.vertices[i].pos)
sidefacepoints.push(polygon2.vertices[nexti].pos)
sidefacepoints.push(polygon1.vertices[nexti].pos)
let sidefacepolygon = Polygon.createFromPoints(sidefacepoints, this.shared)
newpolygons.push(sidefacepolygon)
}
polygon2 = polygon2.flipped()
newpolygons.push(polygon2)
return CSG.fromPolygons(newpolygons)
},
translate: function (offset) {
return this.transform(Matrix4x4.translation(offset))
},
// returns an array with a Vector3D (center point) and a radius
boundingSphere: function () {
if (!this.cachedBoundingSphere) {
let box = this.boundingBox()
let middle = box[0].plus(box[1]).times(0.5)
let radius3 = box[1].minus(middle)
let radius = radius3.length()
this.cachedBoundingSphere = [middle, radius]
}
return this.cachedBoundingSphere
},
// returns an array of two Vector3Ds (minimum coordinates and maximum coordinates)
boundingBox: function () {
if (!this.cachedBoundingBox) {
let minpoint, maxpoint
let vertices = this.vertices
let numvertices = vertices.length
if (numvertices === 0) {
minpoint = new Vector3D(0, 0, 0)
} else {
minpoint = vertices[0].pos
}
maxpoint = minpoint
for (let i = 1; i < numvertices; i++) {
let point = vertices[i].pos
minpoint = minpoint.min(point)
maxpoint = maxpoint.max(point)
}
this.cachedBoundingBox = [minpoint, maxpoint]
}
return this.cachedBoundingBox
},
flipped: function () {
let newvertices = this.vertices.map(function (v) {
return v.flipped()
})
newvertices.reverse()
let newplane = this.plane.flipped()
return new Polygon(newvertices, this.shared, newplane)
},
// Affine transformation of polygon. Returns a new Polygon
transform: function (matrix4x4) {
let newvertices = this.vertices.map(function (v) {
return v.transform(matrix4x4)
})
let newplane = this.plane.transform(matrix4x4)
if (matrix4x4.isMirroring()) {
// need to reverse the vertex order
// in order to preserve the inside/outside orientation:
newvertices.reverse()
}
return new Polygon(newvertices, this.shared, newplane)
},
toString: function () {
let result = 'Polygon plane: ' + this.plane.toString() + '\n'
this.vertices.map(function (vertex) {
result += ' ' + vertex.toString() + '\n'
})
return result
},
// project the 3D polygon onto a plane
projectToOrthoNormalBasis: function (orthobasis) {
const CAG = require('../CAG')
const {fromPointsNoCheck} = require('../CAGFactories') // circular dependencies
let points2d = this.vertices.map(function (vertex) {
return orthobasis.to2D(vertex.pos)
})
let result = fromPointsNoCheck(points2d)
let area = result.area()
if (Math.abs(area) < areaEPS) {
// the polygon was perpendicular to the orthnormal plane. The resulting 2D polygon would be degenerate
// return an empty area instead:
result = new CAG()
} else if (area < 0) {
result = result.flipped()
}
return result
},
//FIXME: WHY is this for 3D polygons and not for 2D shapes ?
/**
* Creates solid from slices (Polygon) by generating walls
* @param {Object} options Solid generating options
* - numslices {Number} Number of slices to be generated
* - callback(t, slice) {Function} Callback function generating slices.
* arguments: t = [0..1], slice = [0..numslices - 1]
* return: Polygon or null to skip
* - loop {Boolean} no flats, only walls, it's used to generate solids like a tor
*/
solidFromSlices: function (options) {
const CSG = require('../CSG')
let polygons = [],
csg = null,
prev = null,
bottom = null,
top = null,
numSlices = 2,
bLoop = false,
fnCallback,
flipped = null
if (options) {
bLoop = Boolean(options['loop'])
if (options.numslices) { numSlices = options.numslices }
if (options.callback) {
fnCallback = options.callback
}
}
if (!fnCallback) {
let square = new Polygon.createFromPoints([
[0, 0, 0],
[1, 0, 0],
[1, 1, 0],
[0, 1, 0]
])
fnCallback = function (t, slice) {
return t === 0 || t === 1 ? square.translate([0, 0, t]) : null
}
}
for (let i = 0, iMax = numSlices - 1; i <= iMax; i++) {
csg = fnCallback.call(this, i / iMax, i)
if (csg) {
if (!(csg instanceof Polygon)) {
throw new Error('Polygon.solidFromSlices callback error: Polygon expected')
}
csg.checkIfConvex()
if (prev) { // generate walls
if (flipped === null) { // not generated yet
flipped = prev.plane.signedDistanceToPoint(csg.vertices[0].pos) < 0
}
this._addWalls(polygons, prev, csg, flipped)
} else { // the first - will be a bottom
bottom = csg
}
prev = csg
} // callback can return null to skip that slice
}
top = csg
if (bLoop) {
let bSameTopBottom = bottom.vertices.length === top.vertices.length &&
bottom.vertices.every(function (v, index) {
return v.pos.equals(top.vertices[index].pos)
})
// if top and bottom are not the same -
// generate walls between them
if (!bSameTopBottom) {
this._addWalls(polygons, top, bottom, flipped)
} // else - already generated
} else {
// save top and bottom
// TODO: flip if necessary
polygons.unshift(flipped ? bottom : bottom.flipped())
polygons.push(flipped ? top.flipped() : top)
}
return CSG.fromPolygons(polygons)
},
/**
*
* @param walls Array of wall polygons
* @param bottom Bottom polygon
* @param top Top polygon
*/
_addWalls: function (walls, bottom, top, bFlipped) {
let bottomPoints = bottom.vertices.slice(0) // make a copy
let topPoints = top.vertices.slice(0) // make a copy
let color = top.shared || null
// check if bottom perimeter is closed
if (!bottomPoints[0].pos.equals(bottomPoints[bottomPoints.length - 1].pos)) {
bottomPoints.push(bottomPoints[0])
}
// check if top perimeter is closed
if (!topPoints[0].pos.equals(topPoints[topPoints.length - 1].pos)) {
topPoints.push(topPoints[0])
}
if (bFlipped) {
bottomPoints = bottomPoints.reverse()
topPoints = topPoints.reverse()
}
let iTopLen = topPoints.length - 1
let iBotLen = bottomPoints.length - 1
let iExtra = iTopLen - iBotLen// how many extra triangles we need
let bMoreTops = iExtra > 0
let bMoreBottoms = iExtra < 0
let aMin = [] // indexes to start extra triangles (polygon with minimal square)
// init - we need exactly /iExtra/ small triangles
for (let i = Math.abs(iExtra); i > 0; i--) {
aMin.push({
len: Infinity,
index: -1
})
}
let len
if (bMoreBottoms) {
for (let i = 0; i < iBotLen; i++) {
len = bottomPoints[i].pos.distanceToSquared(bottomPoints[i + 1].pos)
// find the element to replace
for (let j = aMin.length - 1; j >= 0; j--) {
if (aMin[j].len > len) {
aMin[j].len = len
aMin.index = j
break
}
} // for
}
} else if (bMoreTops) {
for (let i = 0; i < iTopLen; i++) {
len = topPoints[i].pos.distanceToSquared(topPoints[i + 1].pos)
// find the element to replace
for (let j = aMin.length - 1; j >= 0; j--) {
if (aMin[j].len > len) {
aMin[j].len = len
aMin.index = j
break
}
} // for
}
} // if
// sort by index
aMin.sort(fnSortByIndex)
let getTriangle = function addWallsPutTriangle (pointA, pointB, pointC, color) {
return new Polygon([pointA, pointB, pointC], color)
// return bFlipped ? triangle.flipped() : triangle;
}
let bpoint = bottomPoints[0]
let tpoint = topPoints[0]
let secondPoint
let nBotFacet
let nTopFacet // length of triangle facet side
for (let iB = 0, iT = 0, iMax = iTopLen + iBotLen; iB + iT < iMax;) {
if (aMin.length) {
if (bMoreTops && iT === aMin[0].index) { // one vertex is on the bottom, 2 - on the top
secondPoint = topPoints[++iT]
// console.log('<<< extra top: ' + secondPoint + ', ' + tpoint + ', bottom: ' + bpoint);
walls.push(getTriangle(
secondPoint, tpoint, bpoint, color
))
tpoint = secondPoint
aMin.shift()
continue
} else if (bMoreBottoms && iB === aMin[0].index) {
secondPoint = bottomPoints[++iB]
walls.push(getTriangle(
tpoint, bpoint, secondPoint, color
))
bpoint = secondPoint
aMin.shift()
continue
}
}
// choose the shortest path
if (iB < iBotLen) { // one vertex is on the top, 2 - on the bottom
nBotFacet = tpoint.pos.distanceToSquared(bottomPoints[iB + 1].pos)
} else {
nBotFacet = Infinity
}
if (iT < iTopLen) { // one vertex is on the bottom, 2 - on the top
nTopFacet = bpoint.pos.distanceToSquared(topPoints[iT + 1].pos)
} else {
nTopFacet = Infinity
}
if (nBotFacet <= nTopFacet) {
secondPoint = bottomPoints[++iB]
walls.push(getTriangle(
tpoint, bpoint, secondPoint, color
))
bpoint = secondPoint
} else if (iT < iTopLen) { // nTopFacet < Infinity
secondPoint = topPoints[++iT]
// console.log('<<< top: ' + secondPoint + ', ' + tpoint + ', bottom: ' + bpoint);
walls.push(getTriangle(
secondPoint, tpoint, bpoint, color
))
tpoint = secondPoint
};
}
return walls
}
}
Polygon.verticesConvex = function (vertices, planenormal) {
let numvertices = vertices.length
if (numvertices > 2) {
let prevprevpos = vertices[numvertices - 2].pos
let prevpos = vertices[numvertices - 1].pos
for (let i = 0; i < numvertices; i++) {
let pos = vertices[i].pos
if (!Polygon.isConvexPoint(prevprevpos, prevpos, pos, planenormal)) {
return false
}
prevprevpos = prevpos
prevpos = pos
}
}
return true
}
/** Create a polygon from the given points.
*
* @param {Array[]} points - list of points
* @param {Polygon.Shared} [shared=defaultShared] - shared property to apply
* @param {Plane} [plane] - plane of the polygon
*
* @example
* const points = [
* [0, 0, 0],
* [0, 10, 0],
* [0, 10, 10]
* ]
* let observed = CSG.Polygon.createFromPoints(points)
*/
Polygon.createFromPoints = function (points, shared, plane) {
let vertices = []
points.map(function (p) {
let vec = new Vector3D(p)
let vertex = new Vertex(vec)
vertices.push(vertex)
})
let polygon
if (arguments.length < 3) {
polygon = new Polygon(vertices, shared)
} else {
polygon = new Polygon(vertices, shared, plane)
}
return polygon
}
// calculate whether three points form a convex corner
// prevpoint, point, nextpoint: the 3 coordinates (Vector3D instances)
// normal: the normal vector of the plane
Polygon.isConvexPoint = function (prevpoint, point, nextpoint, normal) {
let crossproduct = point.minus(prevpoint).cross(nextpoint.minus(point))
let crossdotnormal = crossproduct.dot(normal)
return (crossdotnormal >= 0)
}
Polygon.isStrictlyConvexPoint = function (prevpoint, point, nextpoint, normal) {
let crossproduct = point.minus(prevpoint).cross(nextpoint.minus(point))
let crossdotnormal = crossproduct.dot(normal)
return (crossdotnormal >= EPS)
}
/** Class Polygon.Shared
* Holds the shared properties for each polygon (Currently only color).
* @constructor
* @param {Array[]} color - array containing RGBA values, or null
*
* @example
* let shared = new CSG.Polygon.Shared([0, 0, 0, 1])
*/
Polygon.Shared = function (color) {
if (color !== null) {
if (color.length !== 4) {
throw new Error('Expecting 4 element array')
}
}
this.color = color
}
Polygon.Shared.fromObject = function (obj) {
return new Polygon.Shared(obj.color)
}
/** Create Polygon.Shared from color values.
* @param {number} r - value of RED component
* @param {number} g - value of GREEN component
* @param {number} b - value of BLUE component
* @param {number} [a] - value of ALPHA component
* @param {Array[]} [color] - OR array containing RGB values (optional Alpha)
*
* @example
* let s1 = Polygon.Shared.fromColor(0,0,0)
* let s2 = Polygon.Shared.fromColor([0,0,0,1])
*/
Polygon.Shared.fromColor = function (args) {
let color
if (arguments.length === 1) {
color = arguments[0].slice() // make deep copy
} else {
color = []
for (let i = 0; i < arguments.length; i++) {
color.push(arguments[i])
}
}
if (color.length === 3) {
color.push(1)
} else if (color.length !== 4) {
throw new Error('setColor expects either an array with 3 or 4 elements, or 3 or 4 parameters.')
}
return new Polygon.Shared(color)
}
Polygon.Shared.prototype = {
getTag: function () {
let result = this.tag
if (!result) {
result = getTag()
this.tag = result
}
return result
},
// get a string uniquely identifying this object
getHash: function () {
if (!this.color) return 'null'
return this.color.join('/')
}
}
Polygon.defaultShared = new Polygon.Shared(null)
module.exports = Polygon

View File

@ -1,102 +0,0 @@
const Vector2D = require('./Vector2')
const Vertex = require('./Vertex2')
const Vertex3 = require('./Vertex3')
const Polygon = require('./Polygon3')
const {getTag} = require('../constants')
const Side = function (vertex0, vertex1) {
if (!(vertex0 instanceof Vertex)) throw new Error('Assertion failed')
if (!(vertex1 instanceof Vertex)) throw new Error('Assertion failed')
this.vertex0 = vertex0
this.vertex1 = vertex1
}
Side.fromObject = function (obj) {
var vertex0 = Vertex.fromObject(obj.vertex0)
var vertex1 = Vertex.fromObject(obj.vertex1)
return new Side(vertex0, vertex1)
}
Side._fromFakePolygon = function (polygon) {
// this can happen based on union, seems to be residuals -
// return null and handle in caller
if (polygon.vertices.length < 4) {
return null
}
var vert1Indices = []
var pts2d = polygon.vertices.filter(function (v, i) {
if (v.pos.z > 0) {
vert1Indices.push(i)
return true
}
return false
})
.map(function (v) {
return new Vector2D(v.pos.x, v.pos.y)
})
if (pts2d.length !== 2) {
throw new Error('Assertion failed: _fromFakePolygon: not enough points found')
}
var d = vert1Indices[1] - vert1Indices[0]
if (d === 1 || d === 3) {
if (d === 1) {
pts2d.reverse()
}
} else {
throw new Error('Assertion failed: _fromFakePolygon: unknown index ordering')
}
var result = new Side(new Vertex(pts2d[0]), new Vertex(pts2d[1]))
return result
}
Side.prototype = {
toString: function () {
return this.vertex0 + ' -> ' + this.vertex1
},
toPolygon3D: function (z0, z1) {
//console.log(this.vertex0.pos)
const vertices = [
new Vertex3(this.vertex0.pos.toVector3D(z0)),
new Vertex3(this.vertex1.pos.toVector3D(z0)),
new Vertex3(this.vertex1.pos.toVector3D(z1)),
new Vertex3(this.vertex0.pos.toVector3D(z1))
]
return new Polygon(vertices)
},
transform: function (matrix4x4) {
var newp1 = this.vertex0.pos.transform(matrix4x4)
var newp2 = this.vertex1.pos.transform(matrix4x4)
return new Side(new Vertex(newp1), new Vertex(newp2))
},
flipped: function () {
return new Side(this.vertex1, this.vertex0)
},
direction: function () {
return this.vertex1.pos.minus(this.vertex0.pos)
},
getTag: function () {
var result = this.tag
if (!result) {
result = getTag()
this.tag = result
}
return result
},
lengthSquared: function () {
let x = this.vertex1.pos.x - this.vertex0.pos.x
let y = this.vertex1.pos.y - this.vertex0.pos.y
return x * x + y * y
},
length: function () {
return Math.sqrt(this.lengthSquared())
}
}
module.exports = Side

View File

@ -1,196 +0,0 @@
const {IsFloat} = require('../utils')
/** Class Vector2D
* Represents a 2D vector with X, Y coordinates
* @constructor
*
* @example
* new CSG.Vector2D(1, 2);
* new CSG.Vector3D([1, 2]);
* new CSG.Vector3D({ x: 1, y: 2});
*/
const Vector2D = function (x, y) {
if (arguments.length === 2) {
this._x = parseFloat(x)
this._y = parseFloat(y)
} else {
var ok = true
if (arguments.length === 1) {
if (typeof (x) === 'object') {
if (x instanceof Vector2D) {
this._x = x._x
this._y = x._y
} else if (x instanceof Array) {
this._x = parseFloat(x[0])
this._y = parseFloat(x[1])
} else if (('x' in x) && ('y' in x)) {
this._x = parseFloat(x.x)
this._y = parseFloat(x.y)
} else ok = false
} else {
var v = parseFloat(x)
this._x = v
this._y = v
}
} else ok = false
if (ok) {
if ((!IsFloat(this._x)) || (!IsFloat(this._y))) ok = false
}
if (!ok) {
throw new Error('wrong arguments')
}
}
}
Vector2D.fromAngle = function (radians) {
return Vector2D.fromAngleRadians(radians)
}
Vector2D.fromAngleDegrees = function (degrees) {
var radians = Math.PI * degrees / 180
return Vector2D.fromAngleRadians(radians)
}
Vector2D.fromAngleRadians = function (radians) {
return Vector2D.Create(Math.cos(radians), Math.sin(radians))
}
// This does the same as new Vector2D(x,y) but it doesn't go through the constructor
// and the parameters are not validated. Is much faster.
Vector2D.Create = function (x, y) {
var result = Object.create(Vector2D.prototype)
result._x = x
result._y = y
return result
}
Vector2D.prototype = {
get x () {
return this._x
},
get y () {
return this._y
},
set x (v) {
throw new Error('Vector2D is immutable')
},
set y (v) {
throw new Error('Vector2D is immutable')
},
// extend to a 3D vector by adding a z coordinate:
toVector3D: function (z) {
const Vector3D = require('./Vector3') // FIXME: circular dependencies Vector2 => Vector3 => Vector2
return new Vector3D(this._x, this._y, z)
},
equals: function (a) {
return (this._x === a._x) && (this._y === a._y)
},
clone: function () {
return Vector2D.Create(this._x, this._y)
},
negated: function () {
return Vector2D.Create(-this._x, -this._y)
},
plus: function (a) {
return Vector2D.Create(this._x + a._x, this._y + a._y)
},
minus: function (a) {
return Vector2D.Create(this._x - a._x, this._y - a._y)
},
times: function (a) {
return Vector2D.Create(this._x * a, this._y * a)
},
dividedBy: function (a) {
return Vector2D.Create(this._x / a, this._y / a)
},
dot: function (a) {
return this._x * a._x + this._y * a._y
},
lerp: function (a, t) {
return this.plus(a.minus(this).times(t))
},
length: function () {
return Math.sqrt(this.dot(this))
},
distanceTo: function (a) {
return this.minus(a).length()
},
distanceToSquared: function (a) {
return this.minus(a).lengthSquared()
},
lengthSquared: function () {
return this.dot(this)
},
unit: function () {
return this.dividedBy(this.length())
},
cross: function (a) {
return this._x * a._y - this._y * a._x
},
// returns the vector rotated by 90 degrees clockwise
normal: function () {
return Vector2D.Create(this._y, -this._x)
},
// Right multiply by a 4x4 matrix (the vector is interpreted as a row vector)
// Returns a new Vector2D
multiply4x4: function (matrix4x4) {
return matrix4x4.leftMultiply1x2Vector(this)
},
transform: function (matrix4x4) {
return matrix4x4.leftMultiply1x2Vector(this)
},
angle: function () {
return this.angleRadians()
},
angleDegrees: function () {
var radians = this.angleRadians()
return 180 * radians / Math.PI
},
angleRadians: function () {
// y=sin, x=cos
return Math.atan2(this._y, this._x)
},
min: function (p) {
return Vector2D.Create(
Math.min(this._x, p._x), Math.min(this._y, p._y))
},
max: function (p) {
return Vector2D.Create(
Math.max(this._x, p._x), Math.max(this._y, p._y))
},
toString: function () {
return '(' + this._x.toFixed(5) + ', ' + this._y.toFixed(5) + ')'
},
abs: function () {
return Vector2D.Create(Math.abs(this._x), Math.abs(this._y))
}
}
module.exports = Vector2D

View File

@ -1,213 +0,0 @@
const {IsFloat} = require('../utils')
const Vector2D = require('./Vector2')
/** Class Vector3D
* Represents a 3D vector with X, Y, Z coordinates.
* @constructor
*
* @example
* new CSG.Vector3D(1, 2, 3);
* new CSG.Vector3D([1, 2, 3]);
* new CSG.Vector3D({ x: 1, y: 2, z: 3 });
* new CSG.Vector3D(1, 2); // assumes z=0
* new CSG.Vector3D([1, 2]); // assumes z=0
*/
const Vector3D = function (x, y, z) {
if (arguments.length === 3) {
this._x = parseFloat(x)
this._y = parseFloat(y)
this._z = parseFloat(z)
} else if (arguments.length === 2) {
this._x = parseFloat(x)
this._y = parseFloat(y)
this._z = 0
} else {
var ok = true
if (arguments.length === 1) {
if (typeof (x) === 'object') {
if (x instanceof Vector3D) {
this._x = x._x
this._y = x._y
this._z = x._z
} else if (x instanceof Vector2D) {
this._x = x._x
this._y = x._y
this._z = 0
} else if (x instanceof Array) {
if ((x.length < 2) || (x.length > 3)) {
ok = false
} else {
this._x = parseFloat(x[0])
this._y = parseFloat(x[1])
if (x.length === 3) {
this._z = parseFloat(x[2])
} else {
this._z = 0
}
}
} else if (('x' in x) && ('y' in x)) {
this._x = parseFloat(x.x)
this._y = parseFloat(x.y)
if ('z' in x) {
this._z = parseFloat(x.z)
} else {
this._z = 0
}
} else if (('_x' in x) && ('_y' in x)) {
this._x = parseFloat(x._x)
this._y = parseFloat(x._y)
if ('_z' in x) {
this._z = parseFloat(x._z)
} else {
this._z = 0
}
} else ok = false
} else {
var v = parseFloat(x)
this._x = v
this._y = v
this._z = v
}
} else ok = false
if (ok) {
if ((!IsFloat(this._x)) || (!IsFloat(this._y)) || (!IsFloat(this._z))) ok = false
} else {
throw new Error('wrong arguments')
}
}
}
// This does the same as new Vector3D(x,y,z) but it doesn't go through the constructor
// and the parameters are not validated. Is much faster.
Vector3D.Create = function (x, y, z) {
var result = Object.create(Vector3D.prototype)
result._x = x
result._y = y
result._z = z
return result
}
Vector3D.prototype = {
get x () {
return this._x
},
get y () {
return this._y
},
get z () {
return this._z
},
set x (v) {
throw new Error('Vector3D is immutable')
},
set y (v) {
throw new Error('Vector3D is immutable')
},
set z (v) {
throw new Error('Vector3D is immutable')
},
clone: function () {
return Vector3D.Create(this._x, this._y, this._z)
},
negated: function () {
return Vector3D.Create(-this._x, -this._y, -this._z)
},
abs: function () {
return Vector3D.Create(Math.abs(this._x), Math.abs(this._y), Math.abs(this._z))
},
plus: function (a) {
return Vector3D.Create(this._x + a._x, this._y + a._y, this._z + a._z)
},
minus: function (a) {
return Vector3D.Create(this._x - a._x, this._y - a._y, this._z - a._z)
},
times: function (a) {
return Vector3D.Create(this._x * a, this._y * a, this._z * a)
},
dividedBy: function (a) {
return Vector3D.Create(this._x / a, this._y / a, this._z / a)
},
dot: function (a) {
return this._x * a._x + this._y * a._y + this._z * a._z
},
lerp: function (a, t) {
return this.plus(a.minus(this).times(t))
},
lengthSquared: function () {
return this.dot(this)
},
length: function () {
return Math.sqrt(this.lengthSquared())
},
unit: function () {
return this.dividedBy(this.length())
},
cross: function (a) {
return Vector3D.Create(
this._y * a._z - this._z * a._y, this._z * a._x - this._x * a._z, this._x * a._y - this._y * a._x)
},
distanceTo: function (a) {
return this.minus(a).length()
},
distanceToSquared: function (a) {
return this.minus(a).lengthSquared()
},
equals: function (a) {
return (this._x === a._x) && (this._y === a._y) && (this._z === a._z)
},
// Right multiply by a 4x4 matrix (the vector is interpreted as a row vector)
// Returns a new Vector3D
multiply4x4: function (matrix4x4) {
return matrix4x4.leftMultiply1x3Vector(this)
},
transform: function (matrix4x4) {
return matrix4x4.leftMultiply1x3Vector(this)
},
toString: function () {
return '(' + this._x.toFixed(5) + ', ' + this._y.toFixed(5) + ', ' + this._z.toFixed(5) + ')'
},
// find a vector that is somewhat perpendicular to this one
randomNonParallelVector: function () {
var abs = this.abs()
if ((abs._x <= abs._y) && (abs._x <= abs._z)) {
return Vector3D.Create(1, 0, 0)
} else if ((abs._y <= abs._x) && (abs._y <= abs._z)) {
return Vector3D.Create(0, 1, 0)
} else {
return Vector3D.Create(0, 0, 1)
}
},
min: function (p) {
return Vector3D.Create(
Math.min(this._x, p._x), Math.min(this._y, p._y), Math.min(this._z, p._z))
},
max: function (p) {
return Vector3D.Create(
Math.max(this._x, p._x), Math.max(this._y, p._y), Math.max(this._z, p._z))
}
}
module.exports = Vector3D

View File

@ -1,26 +0,0 @@
const Vector2D = require('./Vector2')
const {getTag} = require('../constants')
const Vertex = function (pos) {
this.pos = pos
}
Vertex.fromObject = function (obj) {
return new Vertex(new Vector2D(obj.pos._x, obj.pos._y))
}
Vertex.prototype = {
toString: function () {
return '(' + this.pos.x.toFixed(5) + ',' + this.pos.y.toFixed(5) + ')'
},
getTag: function () {
var result = this.tag
if (!result) {
result = getTag()
this.tag = result
}
return result
}
}
module.exports = Vertex

View File

@ -1,56 +0,0 @@
const Vector3D = require('./Vector3')
const {getTag} = require('../constants')
// # class Vertex
// Represents a vertex of a polygon. Use your own vertex class instead of this
// one to provide additional features like texture coordinates and vertex
// colors. Custom vertex classes need to provide a `pos` property
// `flipped()`, and `interpolate()` methods that behave analogous to the ones
// FIXME: And a lot MORE (see plane.fromVector3Ds for ex) ! This is fragile code
// defined by `Vertex`.
const Vertex = function (pos) {
this.pos = pos
}
// create from an untyped object with identical property names:
Vertex.fromObject = function (obj) {
var pos = new Vector3D(obj.pos)
return new Vertex(pos)
}
Vertex.prototype = {
// Return a vertex with all orientation-specific data (e.g. vertex normal) flipped. Called when the
// orientation of a polygon is flipped.
flipped: function () {
return this
},
getTag: function () {
var result = this.tag
if (!result) {
result = getTag()
this.tag = result
}
return result
},
// Create a new vertex between this vertex and `other` by linearly
// interpolating all properties using a parameter of `t`. Subclasses should
// override this to interpolate additional properties.
interpolate: function (other, t) {
var newpos = this.pos.lerp(other.pos, t)
return new Vertex(newpos)
},
// Affine transformation of vertex. Returns a new Vertex
transform: function (matrix4x4) {
var newpos = this.pos.multiply4x4(matrix4x4)
return new Vertex(newpos)
},
toString: function () {
return this.pos.toString()
}
}
module.exports = Vertex

View File

@ -1,25 +0,0 @@
const {EPS} = require('../constants')
const {solve2Linear} = require('../utils')
// see if the line between p0start and p0end intersects with the line between p1start and p1end
// returns true if the lines strictly intersect, the end points are not counted!
const linesIntersect = function (p0start, p0end, p1start, p1end) {
if (p0end.equals(p1start) || p1end.equals(p0start)) {
let d = p1end.minus(p1start).unit().plus(p0end.minus(p0start).unit()).length()
if (d < EPS) {
return true
}
} else {
let d0 = p0end.minus(p0start)
let d1 = p1end.minus(p1start)
// FIXME These epsilons need review and testing
if (Math.abs(d0.cross(d1)) < 1e-9) return false // lines are parallel
let alphas = solve2Linear(-d0.x, d1.x, -d0.y, d1.y, p0start.x - p1start.x, p0start.y - p1start.y)
if ((alphas[0] > 1e-6) && (alphas[0] < 0.999999) && (alphas[1] > 1e-5) && (alphas[1] < 0.999999)) return true
// if( (alphas[0] >= 0) && (alphas[0] <= 1) && (alphas[1] >= 0) && (alphas[1] <= 1) ) return true;
}
return false
}
module.exports = {linesIntersect}

View File

@ -1,342 +0,0 @@
const {EPS} = require('../constants')
const OrthoNormalBasis = require('./OrthoNormalBasis')
const {interpolateBetween2DPointsForY, insertSorted, fnNumberSort} = require('../utils')
const Vertex = require('./Vertex3')
const Vector2D = require('./Vector2')
const Line2D = require('./Line2')
const Polygon = require('./Polygon3')
// Retesselation function for a set of coplanar polygons. See the introduction at the top of
// this file.
const reTesselateCoplanarPolygons = function (sourcepolygons, destpolygons) {
let numpolygons = sourcepolygons.length
if (numpolygons > 0) {
let plane = sourcepolygons[0].plane
let shared = sourcepolygons[0].shared
let orthobasis = new OrthoNormalBasis(plane)
let polygonvertices2d = [] // array of array of Vector2D
let polygontopvertexindexes = [] // array of indexes of topmost vertex per polygon
let topy2polygonindexes = {}
let ycoordinatetopolygonindexes = {}
let xcoordinatebins = {}
let ycoordinatebins = {}
// convert all polygon vertices to 2D
// Make a list of all encountered y coordinates
// And build a map of all polygons that have a vertex at a certain y coordinate:
let ycoordinateBinningFactor = 1.0 / EPS * 10
for (let polygonindex = 0; polygonindex < numpolygons; polygonindex++) {
let poly3d = sourcepolygons[polygonindex]
let vertices2d = []
let numvertices = poly3d.vertices.length
let minindex = -1
if (numvertices > 0) {
let miny, maxy, maxindex
for (let i = 0; i < numvertices; i++) {
let pos2d = orthobasis.to2D(poly3d.vertices[i].pos)
// perform binning of y coordinates: If we have multiple vertices very
// close to each other, give them the same y coordinate:
let ycoordinatebin = Math.floor(pos2d.y * ycoordinateBinningFactor)
let newy
if (ycoordinatebin in ycoordinatebins) {
newy = ycoordinatebins[ycoordinatebin]
} else if (ycoordinatebin + 1 in ycoordinatebins) {
newy = ycoordinatebins[ycoordinatebin + 1]
} else if (ycoordinatebin - 1 in ycoordinatebins) {
newy = ycoordinatebins[ycoordinatebin - 1]
} else {
newy = pos2d.y
ycoordinatebins[ycoordinatebin] = pos2d.y
}
pos2d = Vector2D.Create(pos2d.x, newy)
vertices2d.push(pos2d)
let y = pos2d.y
if ((i === 0) || (y < miny)) {
miny = y
minindex = i
}
if ((i === 0) || (y > maxy)) {
maxy = y
maxindex = i
}
if (!(y in ycoordinatetopolygonindexes)) {
ycoordinatetopolygonindexes[y] = {}
}
ycoordinatetopolygonindexes[y][polygonindex] = true
}
if (miny >= maxy) {
// degenerate polygon, all vertices have same y coordinate. Just ignore it from now:
vertices2d = []
numvertices = 0
minindex = -1
} else {
if (!(miny in topy2polygonindexes)) {
topy2polygonindexes[miny] = []
}
topy2polygonindexes[miny].push(polygonindex)
}
} // if(numvertices > 0)
// reverse the vertex order:
vertices2d.reverse()
minindex = numvertices - minindex - 1
polygonvertices2d.push(vertices2d)
polygontopvertexindexes.push(minindex)
}
let ycoordinates = []
for (let ycoordinate in ycoordinatetopolygonindexes) ycoordinates.push(ycoordinate)
ycoordinates.sort(fnNumberSort)
// Now we will iterate over all y coordinates, from lowest to highest y coordinate
// activepolygons: source polygons that are 'active', i.e. intersect with our y coordinate
// Is sorted so the polygons are in left to right order
// Each element in activepolygons has these properties:
// polygonindex: the index of the source polygon (i.e. an index into the sourcepolygons
// and polygonvertices2d arrays)
// leftvertexindex: the index of the vertex at the left side of the polygon (lowest x)
// that is at or just above the current y coordinate
// rightvertexindex: dito at right hand side of polygon
// topleft, bottomleft: coordinates of the left side of the polygon crossing the current y coordinate
// topright, bottomright: coordinates of the right hand side of the polygon crossing the current y coordinate
let activepolygons = []
let prevoutpolygonrow = []
for (let yindex = 0; yindex < ycoordinates.length; yindex++) {
let newoutpolygonrow = []
let ycoordinate_as_string = ycoordinates[yindex]
let ycoordinate = Number(ycoordinate_as_string)
// update activepolygons for this y coordinate:
// - Remove any polygons that end at this y coordinate
// - update leftvertexindex and rightvertexindex (which point to the current vertex index
// at the the left and right side of the polygon
// Iterate over all polygons that have a corner at this y coordinate:
let polygonindexeswithcorner = ycoordinatetopolygonindexes[ycoordinate_as_string]
for (let activepolygonindex = 0; activepolygonindex < activepolygons.length; ++activepolygonindex) {
let activepolygon = activepolygons[activepolygonindex]
let polygonindex = activepolygon.polygonindex
if (polygonindexeswithcorner[polygonindex]) {
// this active polygon has a corner at this y coordinate:
let vertices2d = polygonvertices2d[polygonindex]
let numvertices = vertices2d.length
let newleftvertexindex = activepolygon.leftvertexindex
let newrightvertexindex = activepolygon.rightvertexindex
// See if we need to increase leftvertexindex or decrease rightvertexindex:
while (true) {
let nextleftvertexindex = newleftvertexindex + 1
if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0
if (vertices2d[nextleftvertexindex].y !== ycoordinate) break
newleftvertexindex = nextleftvertexindex
}
let nextrightvertexindex = newrightvertexindex - 1
if (nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1
if (vertices2d[nextrightvertexindex].y === ycoordinate) {
newrightvertexindex = nextrightvertexindex
}
if ((newleftvertexindex !== activepolygon.leftvertexindex) && (newleftvertexindex === newrightvertexindex)) {
// We have increased leftvertexindex or decreased rightvertexindex, and now they point to the same vertex
// This means that this is the bottom point of the polygon. We'll remove it:
activepolygons.splice(activepolygonindex, 1)
--activepolygonindex
} else {
activepolygon.leftvertexindex = newleftvertexindex
activepolygon.rightvertexindex = newrightvertexindex
activepolygon.topleft = vertices2d[newleftvertexindex]
activepolygon.topright = vertices2d[newrightvertexindex]
let nextleftvertexindex = newleftvertexindex + 1
if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0
activepolygon.bottomleft = vertices2d[nextleftvertexindex]
let nextrightvertexindex = newrightvertexindex - 1
if (nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1
activepolygon.bottomright = vertices2d[nextrightvertexindex]
}
} // if polygon has corner here
} // for activepolygonindex
let nextycoordinate
if (yindex >= ycoordinates.length - 1) {
// last row, all polygons must be finished here:
activepolygons = []
nextycoordinate = null
} else // yindex < ycoordinates.length-1
{
nextycoordinate = Number(ycoordinates[yindex + 1])
let middleycoordinate = 0.5 * (ycoordinate + nextycoordinate)
// update activepolygons by adding any polygons that start here:
let startingpolygonindexes = topy2polygonindexes[ycoordinate_as_string]
for (let polygonindex_key in startingpolygonindexes) {
let polygonindex = startingpolygonindexes[polygonindex_key]
let vertices2d = polygonvertices2d[polygonindex]
let numvertices = vertices2d.length
let topvertexindex = polygontopvertexindexes[polygonindex]
// the top of the polygon may be a horizontal line. In that case topvertexindex can point to any point on this line.
// Find the left and right topmost vertices which have the current y coordinate:
let topleftvertexindex = topvertexindex
while (true) {
let i = topleftvertexindex + 1
if (i >= numvertices) i = 0
if (vertices2d[i].y !== ycoordinate) break
if (i === topvertexindex) break // should not happen, but just to prevent endless loops
topleftvertexindex = i
}
let toprightvertexindex = topvertexindex
while (true) {
let i = toprightvertexindex - 1
if (i < 0) i = numvertices - 1
if (vertices2d[i].y !== ycoordinate) break
if (i === topleftvertexindex) break // should not happen, but just to prevent endless loops
toprightvertexindex = i
}
let nextleftvertexindex = topleftvertexindex + 1
if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0
let nextrightvertexindex = toprightvertexindex - 1
if (nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1
let newactivepolygon = {
polygonindex: polygonindex,
leftvertexindex: topleftvertexindex,
rightvertexindex: toprightvertexindex,
topleft: vertices2d[topleftvertexindex],
topright: vertices2d[toprightvertexindex],
bottomleft: vertices2d[nextleftvertexindex],
bottomright: vertices2d[nextrightvertexindex]
}
insertSorted(activepolygons, newactivepolygon, function (el1, el2) {
let x1 = interpolateBetween2DPointsForY(
el1.topleft, el1.bottomleft, middleycoordinate)
let x2 = interpolateBetween2DPointsForY(
el2.topleft, el2.bottomleft, middleycoordinate)
if (x1 > x2) return 1
if (x1 < x2) return -1
return 0
})
} // for(let polygonindex in startingpolygonindexes)
} // yindex < ycoordinates.length-1
// if( (yindex === ycoordinates.length-1) || (nextycoordinate - ycoordinate > EPS) )
if (true) {
// Now activepolygons is up to date
// Build the output polygons for the next row in newoutpolygonrow:
for (let activepolygonKey in activepolygons) {
let activepolygon = activepolygons[activepolygonKey]
let polygonindex = activepolygon.polygonindex
let vertices2d = polygonvertices2d[polygonindex]
let numvertices = vertices2d.length
let x = interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, ycoordinate)
let topleft = Vector2D.Create(x, ycoordinate)
x = interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, ycoordinate)
let topright = Vector2D.Create(x, ycoordinate)
x = interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, nextycoordinate)
let bottomleft = Vector2D.Create(x, nextycoordinate)
x = interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, nextycoordinate)
let bottomright = Vector2D.Create(x, nextycoordinate)
let outpolygon = {
topleft: topleft,
topright: topright,
bottomleft: bottomleft,
bottomright: bottomright,
leftline: Line2D.fromPoints(topleft, bottomleft),
rightline: Line2D.fromPoints(bottomright, topright)
}
if (newoutpolygonrow.length > 0) {
let prevoutpolygon = newoutpolygonrow[newoutpolygonrow.length - 1]
let d1 = outpolygon.topleft.distanceTo(prevoutpolygon.topright)
let d2 = outpolygon.bottomleft.distanceTo(prevoutpolygon.bottomright)
if ((d1 < EPS) && (d2 < EPS)) {
// we can join this polygon with the one to the left:
outpolygon.topleft = prevoutpolygon.topleft
outpolygon.leftline = prevoutpolygon.leftline
outpolygon.bottomleft = prevoutpolygon.bottomleft
newoutpolygonrow.splice(newoutpolygonrow.length - 1, 1)
}
}
newoutpolygonrow.push(outpolygon)
} // for(activepolygon in activepolygons)
if (yindex > 0) {
// try to match the new polygons against the previous row:
let prevcontinuedindexes = {}
let matchedindexes = {}
for (let i = 0; i < newoutpolygonrow.length; i++) {
let thispolygon = newoutpolygonrow[i]
for (let ii = 0; ii < prevoutpolygonrow.length; ii++) {
if (!matchedindexes[ii]) // not already processed?
{
// We have a match if the sidelines are equal or if the top coordinates
// are on the sidelines of the previous polygon
let prevpolygon = prevoutpolygonrow[ii]
if (prevpolygon.bottomleft.distanceTo(thispolygon.topleft) < EPS) {
if (prevpolygon.bottomright.distanceTo(thispolygon.topright) < EPS) {
// Yes, the top of this polygon matches the bottom of the previous:
matchedindexes[ii] = true
// Now check if the joined polygon would remain convex:
let d1 = thispolygon.leftline.direction().x - prevpolygon.leftline.direction().x
let d2 = thispolygon.rightline.direction().x - prevpolygon.rightline.direction().x
let leftlinecontinues = Math.abs(d1) < EPS
let rightlinecontinues = Math.abs(d2) < EPS
let leftlineisconvex = leftlinecontinues || (d1 >= 0)
let rightlineisconvex = rightlinecontinues || (d2 >= 0)
if (leftlineisconvex && rightlineisconvex) {
// yes, both sides have convex corners:
// This polygon will continue the previous polygon
thispolygon.outpolygon = prevpolygon.outpolygon
thispolygon.leftlinecontinues = leftlinecontinues
thispolygon.rightlinecontinues = rightlinecontinues
prevcontinuedindexes[ii] = true
}
break
}
}
} // if(!prevcontinuedindexes[ii])
} // for ii
} // for i
for (let ii = 0; ii < prevoutpolygonrow.length; ii++) {
if (!prevcontinuedindexes[ii]) {
// polygon ends here
// Finish the polygon with the last point(s):
let prevpolygon = prevoutpolygonrow[ii]
prevpolygon.outpolygon.rightpoints.push(prevpolygon.bottomright)
if (prevpolygon.bottomright.distanceTo(prevpolygon.bottomleft) > EPS) {
// polygon ends with a horizontal line:
prevpolygon.outpolygon.leftpoints.push(prevpolygon.bottomleft)
}
// reverse the left half so we get a counterclockwise circle:
prevpolygon.outpolygon.leftpoints.reverse()
let points2d = prevpolygon.outpolygon.rightpoints.concat(prevpolygon.outpolygon.leftpoints)
let vertices3d = []
points2d.map(function (point2d) {
let point3d = orthobasis.to3D(point2d)
let vertex3d = new Vertex(point3d)
vertices3d.push(vertex3d)
})
let polygon = new Polygon(vertices3d, shared, plane)
destpolygons.push(polygon)
}
}
} // if(yindex > 0)
for (let i = 0; i < newoutpolygonrow.length; i++) {
let thispolygon = newoutpolygonrow[i]
if (!thispolygon.outpolygon) {
// polygon starts here:
thispolygon.outpolygon = {
leftpoints: [],
rightpoints: []
}
thispolygon.outpolygon.leftpoints.push(thispolygon.topleft)
if (thispolygon.topleft.distanceTo(thispolygon.topright) > EPS) {
// we have a horizontal line at the top:
thispolygon.outpolygon.rightpoints.push(thispolygon.topright)
}
} else {
// continuation of a previous row
if (!thispolygon.leftlinecontinues) {
thispolygon.outpolygon.leftpoints.push(thispolygon.topleft)
}
if (!thispolygon.rightlinecontinues) {
thispolygon.outpolygon.rightpoints.push(thispolygon.topright)
}
}
}
prevoutpolygonrow = newoutpolygonrow
}
} // for yindex
} // if(numpolygons > 0)
}
module.exports = {reTesselateCoplanarPolygons}

View File

@ -1,81 +0,0 @@
const Matrix4x4 = require('./math/Matrix4')
const Vector3D = require('./math/Vector3')
const Plane = require('./math/Plane')
// Add several convenience methods to the classes that support a transform() method:
const addTransformationMethodsToPrototype = function (prot) {
prot.mirrored = function (plane) {
return this.transform(Matrix4x4.mirroring(plane))
}
prot.mirroredX = function () {
let plane = new Plane(Vector3D.Create(1, 0, 0), 0)
return this.mirrored(plane)
}
prot.mirroredY = function () {
let plane = new Plane(Vector3D.Create(0, 1, 0), 0)
return this.mirrored(plane)
}
prot.mirroredZ = function () {
let plane = new Plane(Vector3D.Create(0, 0, 1), 0)
return this.mirrored(plane)
}
prot.translate = function (v) {
return this.transform(Matrix4x4.translation(v))
}
prot.scale = function (f) {
return this.transform(Matrix4x4.scaling(f))
}
prot.rotateX = function (deg) {
return this.transform(Matrix4x4.rotationX(deg))
}
prot.rotateY = function (deg) {
return this.transform(Matrix4x4.rotationY(deg))
}
prot.rotateZ = function (deg) {
return this.transform(Matrix4x4.rotationZ(deg))
}
prot.rotate = function (rotationCenter, rotationAxis, degrees) {
return this.transform(Matrix4x4.rotation(rotationCenter, rotationAxis, degrees))
}
prot.rotateEulerAngles = function (alpha, beta, gamma, position) {
position = position || [0, 0, 0]
let Rz1 = Matrix4x4.rotationZ(alpha)
let Rx = Matrix4x4.rotationX(beta)
let Rz2 = Matrix4x4.rotationZ(gamma)
let T = Matrix4x4.translation(new Vector3D(position))
return this.transform(Rz2.multiply(Rx).multiply(Rz1).multiply(T))
}
}
// TODO: consider generalization and adding to addTransformationMethodsToPrototype
const addCenteringToPrototype = function (prot, axes) {
prot.center = function (cAxes) {
cAxes = Array.prototype.map.call(arguments, function (a) {
return a // .toLowerCase();
})
// no args: center on all axes
if (!cAxes.length) {
cAxes = axes.slice()
}
let b = this.getBounds()
return this.translate(axes.map(function (a) {
return cAxes.indexOf(a) > -1 ? -(b[0][a] + b[1][a]) / 2 : 0
}))
}
}
module.exports = {
addTransformationMethodsToPrototype,
addCenteringToPrototype
}

View File

@ -1,76 +0,0 @@
const Vector3D = require('./math/Vector3')
const Vector2D = require('./math/Vector2')
// Parse an option from the options object
// If the option is not present, return the default value
const parseOption = function (options, optionname, defaultvalue) {
var result = defaultvalue
if (options && optionname in options) {
result = options[optionname]
}
return result
}
// Parse an option and force into a Vector3D. If a scalar is passed it is converted
// into a vector with equal x,y,z
const parseOptionAs3DVector = function (options, optionname, defaultvalue) {
var result = parseOption(options, optionname, defaultvalue)
result = new Vector3D(result)
return result
}
const parseOptionAs3DVectorList = function (options, optionname, defaultvalue) {
var result = parseOption(options, optionname, defaultvalue)
return result.map(function (res) {
return new Vector3D(res)
})
}
// Parse an option and force into a Vector2D. If a scalar is passed it is converted
// into a vector with equal x,y
const parseOptionAs2DVector = function (options, optionname, defaultvalue) {
var result = parseOption(options, optionname, defaultvalue)
result = new Vector2D(result)
return result
}
const parseOptionAsFloat = function (options, optionname, defaultvalue) {
var result = parseOption(options, optionname, defaultvalue)
if (typeof (result) === 'string') {
result = Number(result)
}
if (isNaN(result) || typeof (result) !== 'number') {
throw new Error('Parameter ' + optionname + ' should be a number')
}
return result
}
const parseOptionAsInt = function (options, optionname, defaultvalue) {
var result = parseOption(options, optionname, defaultvalue)
result = Number(Math.floor(result))
if (isNaN(result)) {
throw new Error('Parameter ' + optionname + ' should be a number')
}
return result
}
const parseOptionAsBool = function (options, optionname, defaultvalue) {
var result = parseOption(options, optionname, defaultvalue)
if (typeof (result) === 'string') {
if (result === 'true') result = true
else if (result === 'false') result = false
else if (result === 0) result = false
}
result = !!result
return result
}
module.exports = {
parseOption,
parseOptionAsInt,
parseOptionAsFloat,
parseOptionAsBool,
parseOptionAs3DVector,
parseOptionAs2DVector,
parseOptionAs3DVectorList
}

View File

@ -1,184 +0,0 @@
const CAG = require('./CAG')
const {parseOptionAs2DVector, parseOptionAsFloat, parseOptionAsInt} = require('./optionParsers')
const {defaultResolution2D} = require('./constants')
const Vector2D = require('./math/Vector2')
const Path2D = require('./math/Path2')
const {fromCompactBinary} = require('./CAGFactories')
/** Construct a circle.
* @param {Object} [options] - options for construction
* @param {Vector2D} [options.center=[0,0]] - center of circle
* @param {Number} [options.radius=1] - radius of circle
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @returns {CAG} new CAG object
*/
const circle = function (options) {
options = options || {}
let center = parseOptionAs2DVector(options, 'center', [0, 0])
let radius = parseOptionAsFloat(options, 'radius', 1)
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution2D)
let points = []
for (let i = 0; i < resolution; i++) {
let radians = 2 * Math.PI * i / resolution
let point = Vector2D.fromAngleRadians(radians).times(radius).plus(center)
points.push(point)
}
return CAG.fromPoints(points)
}
/** Construct an ellispe.
* @param {Object} [options] - options for construction
* @param {Vector2D} [options.center=[0,0]] - center of ellipse
* @param {Vector2D} [options.radius=[1,1]] - radius of ellipse, width and height
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @returns {CAG} new CAG object
*/
const ellipse = function (options) {
options = options || {}
let c = parseOptionAs2DVector(options, 'center', [0, 0])
let r = parseOptionAs2DVector(options, 'radius', [1, 1])
r = r.abs() // negative radii make no sense
let res = parseOptionAsInt(options, 'resolution', defaultResolution2D)
let e2 = new Path2D([[c.x, c.y + r.y]])
e2 = e2.appendArc([c.x, c.y - r.y], {
xradius: r.x,
yradius: r.y,
xaxisrotation: 0,
resolution: res,
clockwise: true,
large: false
})
e2 = e2.appendArc([c.x, c.y + r.y], {
xradius: r.x,
yradius: r.y,
xaxisrotation: 0,
resolution: res,
clockwise: true,
large: false
})
e2 = e2.close()
return CAG.fromPath2(e2)
}
/** Construct a rectangle.
* @param {Object} [options] - options for construction
* @param {Vector2D} [options.center=[0,0]] - center of rectangle
* @param {Vector2D} [options.radius=[1,1]] - radius of rectangle, width and height
* @param {Vector2D} [options.corner1=[0,0]] - bottom left corner of rectangle (alternate)
* @param {Vector2D} [options.corner2=[0,0]] - upper right corner of rectangle (alternate)
* @returns {CAG} new CAG object
*/
const rectangle = function (options) {
options = options || {}
let c, r
if (('corner1' in options) || ('corner2' in options)) {
if (('center' in options) || ('radius' in options)) {
throw new Error('rectangle: should either give a radius and center parameter, or a corner1 and corner2 parameter')
}
let corner1 = parseOptionAs2DVector(options, 'corner1', [0, 0])
let corner2 = parseOptionAs2DVector(options, 'corner2', [1, 1])
c = corner1.plus(corner2).times(0.5)
r = corner2.minus(corner1).times(0.5)
} else {
c = parseOptionAs2DVector(options, 'center', [0, 0])
r = parseOptionAs2DVector(options, 'radius', [1, 1])
}
r = r.abs() // negative radii make no sense
let rswap = new Vector2D(r.x, -r.y)
let points = [
c.plus(r), c.plus(rswap), c.minus(r), c.minus(rswap)
]
return CAG.fromPoints(points)
}
/** Construct a rounded rectangle.
* @param {Object} [options] - options for construction
* @param {Vector2D} [options.center=[0,0]] - center of rounded rectangle
* @param {Vector2D} [options.radius=[1,1]] - radius of rounded rectangle, width and height
* @param {Vector2D} [options.corner1=[0,0]] - bottom left corner of rounded rectangle (alternate)
* @param {Vector2D} [options.corner2=[0,0]] - upper right corner of rounded rectangle (alternate)
* @param {Number} [options.roundradius=0.2] - round radius of corners
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @returns {CAG} new CAG object
*
* @example
* let r = roundedRectangle({
* center: [0, 0],
* radius: [5, 10],
* roundradius: 2,
* resolution: 36,
* });
*/
const roundedRectangle = function (options) {
options = options || {}
let center, radius
if (('corner1' in options) || ('corner2' in options)) {
if (('center' in options) || ('radius' in options)) {
throw new Error('roundedRectangle: should either give a radius and center parameter, or a corner1 and corner2 parameter')
}
let corner1 = parseOptionAs2DVector(options, 'corner1', [0, 0])
let corner2 = parseOptionAs2DVector(options, 'corner2', [1, 1])
center = corner1.plus(corner2).times(0.5)
radius = corner2.minus(corner1).times(0.5)
} else {
center = parseOptionAs2DVector(options, 'center', [0, 0])
radius = parseOptionAs2DVector(options, 'radius', [1, 1])
}
radius = radius.abs() // negative radii make no sense
let roundradius = parseOptionAsFloat(options, 'roundradius', 0.2)
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution2D)
let maxroundradius = Math.min(radius.x, radius.y)
maxroundradius -= 0.1
roundradius = Math.min(roundradius, maxroundradius)
roundradius = Math.max(0, roundradius)
radius = new Vector2D(radius.x - roundradius, radius.y - roundradius)
let rect = CAG.rectangle({
center: center,
radius: radius
})
if (roundradius > 0) {
rect = rect.expand(roundradius, resolution)
}
return rect
}
/** Reconstruct a CAG from the output of toCompactBinary().
* @param {CompactBinary} bin - see toCompactBinary()
* @returns {CAG} new CAG object
*/
CAG.fromCompactBinary = function (bin) {
if (bin['class'] !== 'CAG') throw new Error('Not a CAG')
let vertices = []
let vertexData = bin.vertexData
let numvertices = vertexData.length / 2
let arrayindex = 0
for (let vertexindex = 0; vertexindex < numvertices; vertexindex++) {
let x = vertexData[arrayindex++]
let y = vertexData[arrayindex++]
let pos = new Vector2D(x, y)
let vertex = new CAG.Vertex(pos)
vertices.push(vertex)
}
let sides = []
let numsides = bin.sideVertexIndices.length / 2
arrayindex = 0
for (let sideindex = 0; sideindex < numsides; sideindex++) {
let vertexindex0 = bin.sideVertexIndices[arrayindex++]
let vertexindex1 = bin.sideVertexIndices[arrayindex++]
let side = new CAG.Side(vertices[vertexindex0], vertices[vertexindex1])
sides.push(side)
}
let cag = CAG.fromSides(sides)
cag.isCanonicalized = true
return cag
}
module.exports = {
circle,
ellipse,
rectangle,
roundedRectangle,
fromCompactBinary
}

View File

@ -1,548 +0,0 @@
const CSG = require('./CSG')
const {parseOption, parseOptionAs3DVector, parseOptionAs2DVector, parseOptionAs3DVectorList, parseOptionAsFloat, parseOptionAsInt} = require('./optionParsers')
const {defaultResolution3D, defaultResolution2D, EPS} = require('./constants')
const Vector3D = require('./math/Vector3')
const Vertex = require('./math/Vertex3')
const Polygon = require('./math/Polygon3')
const {Connector} = require('./connectors')
const Properties = require('./Properties')
/** Construct an axis-aligned solid cuboid.
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.center=[0,0,0]] - center of cube
* @param {Vector3D} [options.radius=[1,1,1]] - radius of cube, single scalar also possible
* @returns {CSG} new 3D solid
*
* @example
* let cube = CSG.cube({
* center: [5, 5, 5],
* radius: 5, // scalar radius
* });
*/
const cube = function (options) {
let c
let r
let corner1
let corner2
options = options || {}
if (('corner1' in options) || ('corner2' in options)) {
if (('center' in options) || ('radius' in options)) {
throw new Error('cube: should either give a radius and center parameter, or a corner1 and corner2 parameter')
}
corner1 = parseOptionAs3DVector(options, 'corner1', [0, 0, 0])
corner2 = parseOptionAs3DVector(options, 'corner2', [1, 1, 1])
c = corner1.plus(corner2).times(0.5)
r = corner2.minus(corner1).times(0.5)
} else {
c = parseOptionAs3DVector(options, 'center', [0, 0, 0])
r = parseOptionAs3DVector(options, 'radius', [1, 1, 1])
}
r = r.abs() // negative radii make no sense
let result = CSG.fromPolygons([
[
[0, 4, 6, 2],
[-1, 0, 0]
],
[
[1, 3, 7, 5],
[+1, 0, 0]
],
[
[0, 1, 5, 4],
[0, -1, 0]
],
[
[2, 6, 7, 3],
[0, +1, 0]
],
[
[0, 2, 3, 1],
[0, 0, -1]
],
[
[4, 5, 7, 6],
[0, 0, +1]
]
].map(function (info) {
let vertices = info[0].map(function (i) {
let pos = new Vector3D(
c.x + r.x * (2 * !!(i & 1) - 1), c.y + r.y * (2 * !!(i & 2) - 1), c.z + r.z * (2 * !!(i & 4) - 1))
return new Vertex(pos)
})
return new Polygon(vertices, null /* , plane */)
}))
result.properties.cube = new Properties()
result.properties.cube.center = new Vector3D(c)
// add 6 connectors, at the centers of each face:
result.properties.cube.facecenters = [
new Connector(new Vector3D([r.x, 0, 0]).plus(c), [1, 0, 0], [0, 0, 1]),
new Connector(new Vector3D([-r.x, 0, 0]).plus(c), [-1, 0, 0], [0, 0, 1]),
new Connector(new Vector3D([0, r.y, 0]).plus(c), [0, 1, 0], [0, 0, 1]),
new Connector(new Vector3D([0, -r.y, 0]).plus(c), [0, -1, 0], [0, 0, 1]),
new Connector(new Vector3D([0, 0, r.z]).plus(c), [0, 0, 1], [1, 0, 0]),
new Connector(new Vector3D([0, 0, -r.z]).plus(c), [0, 0, -1], [1, 0, 0])
]
return result
}
/** Construct a solid sphere
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.center=[0,0,0]] - center of sphere
* @param {Number} [options.radius=1] - radius of sphere
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @param {Array} [options.axes] - an array with 3 vectors for the x, y and z base vectors
* @returns {CSG} new 3D solid
*
*
* @example
* let sphere = CSG.sphere({
* center: [0, 0, 0],
* radius: 2,
* resolution: 32,
* });
*/
const sphere = function (options) {
options = options || {}
let center = parseOptionAs3DVector(options, 'center', [0, 0, 0])
let radius = parseOptionAsFloat(options, 'radius', 1)
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
let xvector, yvector, zvector
if ('axes' in options) {
xvector = options.axes[0].unit().times(radius)
yvector = options.axes[1].unit().times(radius)
zvector = options.axes[2].unit().times(radius)
} else {
xvector = new Vector3D([1, 0, 0]).times(radius)
yvector = new Vector3D([0, -1, 0]).times(radius)
zvector = new Vector3D([0, 0, 1]).times(radius)
}
if (resolution < 4) resolution = 4
let qresolution = Math.round(resolution / 4)
let prevcylinderpoint
let polygons = []
for (let slice1 = 0; slice1 <= resolution; slice1++) {
let angle = Math.PI * 2.0 * slice1 / resolution
let cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle)))
if (slice1 > 0) {
// cylinder vertices:
let vertices = []
let prevcospitch, prevsinpitch
for (let slice2 = 0; slice2 <= qresolution; slice2++) {
let pitch = 0.5 * Math.PI * slice2 / qresolution
let cospitch = Math.cos(pitch)
let sinpitch = Math.sin(pitch)
if (slice2 > 0) {
vertices = []
vertices.push(new Vertex(center.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
vertices.push(new Vertex(center.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
if (slice2 < qresolution) {
vertices.push(new Vertex(center.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
}
vertices.push(new Vertex(center.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
polygons.push(new Polygon(vertices))
vertices = []
vertices.push(new Vertex(center.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
vertices.push(new Vertex(center.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
if (slice2 < qresolution) {
vertices.push(new Vertex(center.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
}
vertices.push(new Vertex(center.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
vertices.reverse()
polygons.push(new Polygon(vertices))
}
prevcospitch = cospitch
prevsinpitch = sinpitch
}
}
prevcylinderpoint = cylinderpoint
}
let result = CSG.fromPolygons(polygons)
result.properties.sphere = new Properties()
result.properties.sphere.center = new Vector3D(center)
result.properties.sphere.facepoint = center.plus(xvector)
return result
}
/** Construct a solid cylinder.
* @param {Object} [options] - options for construction
* @param {Vector} [options.start=[0,-1,0]] - start point of cylinder
* @param {Vector} [options.end=[0,1,0]] - end point of cylinder
* @param {Number} [options.radius=1] - radius of cylinder, must be scalar
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*
* @example
* let cylinder = CSG.cylinder({
* start: [0, -10, 0],
* end: [0, 10, 0],
* radius: 10,
* resolution: 16
* });
*/
const cylinder = function (options) {
let s = parseOptionAs3DVector(options, 'start', [0, -1, 0])
let e = parseOptionAs3DVector(options, 'end', [0, 1, 0])
let r = parseOptionAsFloat(options, 'radius', 1)
let rEnd = parseOptionAsFloat(options, 'radiusEnd', r)
let rStart = parseOptionAsFloat(options, 'radiusStart', r)
let alpha = parseOptionAsFloat(options, 'sectorAngle', 360)
alpha = alpha > 360 ? alpha % 360 : alpha
if ((rEnd < 0) || (rStart < 0)) {
throw new Error('Radius should be non-negative')
}
if ((rEnd === 0) && (rStart === 0)) {
throw new Error('Either radiusStart or radiusEnd should be positive')
}
let slices = parseOptionAsInt(options, 'resolution', defaultResolution2D) // FIXME is this 3D?
let ray = e.minus(s)
let axisZ = ray.unit() //, isY = (Math.abs(axisZ.y) > 0.5);
let axisX = axisZ.randomNonParallelVector().unit()
// let axisX = new Vector3D(isY, !isY, 0).cross(axisZ).unit();
let axisY = axisX.cross(axisZ).unit()
let start = new Vertex(s)
let end = new Vertex(e)
let polygons = []
function point (stack, slice, radius) {
let angle = slice * Math.PI * alpha / 180
let out = axisX.times(Math.cos(angle)).plus(axisY.times(Math.sin(angle)))
let pos = s.plus(ray.times(stack)).plus(out.times(radius))
return new Vertex(pos)
}
if (alpha > 0) {
for (let i = 0; i < slices; i++) {
let t0 = i / slices
let t1 = (i + 1) / slices
if (rEnd === rStart) {
polygons.push(new Polygon([start, point(0, t0, rEnd), point(0, t1, rEnd)]))
polygons.push(new Polygon([point(0, t1, rEnd), point(0, t0, rEnd), point(1, t0, rEnd), point(1, t1, rEnd)]))
polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
} else {
if (rStart > 0) {
polygons.push(new Polygon([start, point(0, t0, rStart), point(0, t1, rStart)]))
polygons.push(new Polygon([point(0, t0, rStart), point(1, t0, rEnd), point(0, t1, rStart)]))
}
if (rEnd > 0) {
polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
polygons.push(new Polygon([point(1, t0, rEnd), point(1, t1, rEnd), point(0, t1, rStart)]))
}
}
}
if (alpha < 360) {
polygons.push(new Polygon([start, end, point(0, 0, rStart)]))
polygons.push(new Polygon([point(0, 0, rStart), end, point(1, 0, rEnd)]))
polygons.push(new Polygon([start, point(0, 1, rStart), end]))
polygons.push(new Polygon([point(0, 1, rStart), point(1, 1, rEnd), end]))
}
}
let result = CSG.fromPolygons(polygons)
result.properties.cylinder = new Properties()
result.properties.cylinder.start = new Connector(s, axisZ.negated(), axisX)
result.properties.cylinder.end = new Connector(e, axisZ, axisX)
let cylCenter = s.plus(ray.times(0.5))
let fptVec = axisX.rotate(s, axisZ, -alpha / 2).times((rStart + rEnd) / 2)
let fptVec90 = fptVec.cross(axisZ)
// note this one is NOT a face normal for a cone. - It's horizontal from cyl perspective
result.properties.cylinder.facepointH = new Connector(cylCenter.plus(fptVec), fptVec, axisZ)
result.properties.cylinder.facepointH90 = new Connector(cylCenter.plus(fptVec90), fptVec90, axisZ)
return result
}
/** Construct a cylinder with rounded ends.
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.start=[0,-1,0]] - start point of cylinder
* @param {Vector3D} [options.end=[0,1,0]] - end point of cylinder
* @param {Number} [options.radius=1] - radius of rounded ends, must be scalar
* @param {Vector3D} [options.normal] - vector determining the starting angle for tesselation. Should be non-parallel to start.minus(end)
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*
* @example
* let cylinder = CSG.roundedCylinder({
* start: [0, -10, 0],
* end: [0, 10, 0],
* radius: 2,
* resolution: 16
* });
*/
const roundedCylinder = function (options) {
let p1 = parseOptionAs3DVector(options, 'start', [0, -1, 0])
let p2 = parseOptionAs3DVector(options, 'end', [0, 1, 0])
let radius = parseOptionAsFloat(options, 'radius', 1)
let direction = p2.minus(p1)
let defaultnormal
if (Math.abs(direction.x) > Math.abs(direction.y)) {
defaultnormal = new Vector3D(0, 1, 0)
} else {
defaultnormal = new Vector3D(1, 0, 0)
}
let normal = parseOptionAs3DVector(options, 'normal', defaultnormal)
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
if (resolution < 4) resolution = 4
let polygons = []
let qresolution = Math.floor(0.25 * resolution)
let length = direction.length()
if (length < EPS) {
return sphere({
center: p1,
radius: radius,
resolution: resolution
})
}
let zvector = direction.unit().times(radius)
let xvector = zvector.cross(normal).unit().times(radius)
let yvector = xvector.cross(zvector).unit().times(radius)
let prevcylinderpoint
for (let slice1 = 0; slice1 <= resolution; slice1++) {
let angle = Math.PI * 2.0 * slice1 / resolution
let cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle)))
if (slice1 > 0) {
// cylinder vertices:
let vertices = []
vertices.push(new Vertex(p1.plus(cylinderpoint)))
vertices.push(new Vertex(p1.plus(prevcylinderpoint)))
vertices.push(new Vertex(p2.plus(prevcylinderpoint)))
vertices.push(new Vertex(p2.plus(cylinderpoint)))
polygons.push(new Polygon(vertices))
let prevcospitch, prevsinpitch
for (let slice2 = 0; slice2 <= qresolution; slice2++) {
let pitch = 0.5 * Math.PI * slice2 / qresolution
// let pitch = Math.asin(slice2/qresolution);
let cospitch = Math.cos(pitch)
let sinpitch = Math.sin(pitch)
if (slice2 > 0) {
vertices = []
vertices.push(new Vertex(p1.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
vertices.push(new Vertex(p1.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
if (slice2 < qresolution) {
vertices.push(new Vertex(p1.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
}
vertices.push(new Vertex(p1.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
polygons.push(new Polygon(vertices))
vertices = []
vertices.push(new Vertex(p2.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
vertices.push(new Vertex(p2.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
if (slice2 < qresolution) {
vertices.push(new Vertex(p2.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
}
vertices.push(new Vertex(p2.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
vertices.reverse()
polygons.push(new Polygon(vertices))
}
prevcospitch = cospitch
prevsinpitch = sinpitch
}
}
prevcylinderpoint = cylinderpoint
}
let result = CSG.fromPolygons(polygons)
let ray = zvector.unit()
let axisX = xvector.unit()
result.properties.roundedCylinder = new Properties()
result.properties.roundedCylinder.start = new Connector(p1, ray.negated(), axisX)
result.properties.roundedCylinder.end = new Connector(p2, ray, axisX)
result.properties.roundedCylinder.facepoint = p1.plus(xvector)
return result
}
/** Construct an elliptic cylinder.
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.start=[0,-1,0]] - start point of cylinder
* @param {Vector3D} [options.end=[0,1,0]] - end point of cylinder
* @param {Vector2D} [options.radius=[1,1]] - radius of rounded ends, must be two dimensional array
* @param {Vector2D} [options.radiusStart=[1,1]] - OPTIONAL radius of rounded start, must be two dimensional array
* @param {Vector2D} [options.radiusEnd=[1,1]] - OPTIONAL radius of rounded end, must be two dimensional array
* @param {Number} [options.resolution=defaultResolution2D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*
* @example
* let cylinder = CSG.cylinderElliptic({
* start: [0, -10, 0],
* end: [0, 10, 0],
* radiusStart: [10,5],
* radiusEnd: [8,3],
* resolution: 16
* });
*/
const cylinderElliptic = function (options) {
let s = parseOptionAs3DVector(options, 'start', [0, -1, 0])
let e = parseOptionAs3DVector(options, 'end', [0, 1, 0])
let r = parseOptionAs2DVector(options, 'radius', [1, 1])
let rEnd = parseOptionAs2DVector(options, 'radiusEnd', r)
let rStart = parseOptionAs2DVector(options, 'radiusStart', r)
if ((rEnd._x < 0) || (rStart._x < 0) || (rEnd._y < 0) || (rStart._y < 0)) {
throw new Error('Radius should be non-negative')
}
if ((rEnd._x === 0 || rEnd._y === 0) && (rStart._x === 0 || rStart._y === 0)) {
throw new Error('Either radiusStart or radiusEnd should be positive')
}
let slices = parseOptionAsInt(options, 'resolution', defaultResolution2D) // FIXME is this correct?
let ray = e.minus(s)
let axisZ = ray.unit() //, isY = (Math.abs(axisZ.y) > 0.5);
let axisX = axisZ.randomNonParallelVector().unit()
// let axisX = new Vector3D(isY, !isY, 0).cross(axisZ).unit();
let axisY = axisX.cross(axisZ).unit()
let start = new Vertex(s)
let end = new Vertex(e)
let polygons = []
function point (stack, slice, radius) {
let angle = slice * Math.PI * 2
let out = axisX.times(radius._x * Math.cos(angle)).plus(axisY.times(radius._y * Math.sin(angle)))
let pos = s.plus(ray.times(stack)).plus(out)
return new Vertex(pos)
}
for (let i = 0; i < slices; i++) {
let t0 = i / slices
let t1 = (i + 1) / slices
if (rEnd._x === rStart._x && rEnd._y === rStart._y) {
polygons.push(new Polygon([start, point(0, t0, rEnd), point(0, t1, rEnd)]))
polygons.push(new Polygon([point(0, t1, rEnd), point(0, t0, rEnd), point(1, t0, rEnd), point(1, t1, rEnd)]))
polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
} else {
if (rStart._x > 0) {
polygons.push(new Polygon([start, point(0, t0, rStart), point(0, t1, rStart)]))
polygons.push(new Polygon([point(0, t0, rStart), point(1, t0, rEnd), point(0, t1, rStart)]))
}
if (rEnd._x > 0) {
polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
polygons.push(new Polygon([point(1, t0, rEnd), point(1, t1, rEnd), point(0, t1, rStart)]))
}
}
}
let result = CSG.fromPolygons(polygons)
result.properties.cylinder = new Properties()
result.properties.cylinder.start = new Connector(s, axisZ.negated(), axisX)
result.properties.cylinder.end = new Connector(e, axisZ, axisX)
result.properties.cylinder.facepoint = s.plus(axisX.times(rStart))
return result
}
/** Construct an axis-aligned solid rounded cuboid.
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.center=[0,0,0]] - center of rounded cube
* @param {Vector3D} [options.radius=[1,1,1]] - radius of rounded cube, single scalar is possible
* @param {Number} [options.roundradius=0.2] - radius of rounded edges
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*
* @example
* let cube = CSG.roundedCube({
* center: [2, 0, 2],
* radius: 15,
* roundradius: 2,
* resolution: 36,
* });
*/
const roundedCube = function (options) {
let minRR = 1e-2 // minroundradius 1e-3 gives rounding errors already
let center
let cuberadius
let corner1
let corner2
options = options || {}
if (('corner1' in options) || ('corner2' in options)) {
if (('center' in options) || ('radius' in options)) {
throw new Error('roundedCube: should either give a radius and center parameter, or a corner1 and corner2 parameter')
}
corner1 = parseOptionAs3DVector(options, 'corner1', [0, 0, 0])
corner2 = parseOptionAs3DVector(options, 'corner2', [1, 1, 1])
center = corner1.plus(corner2).times(0.5)
cuberadius = corner2.minus(corner1).times(0.5)
} else {
center = parseOptionAs3DVector(options, 'center', [0, 0, 0])
cuberadius = parseOptionAs3DVector(options, 'radius', [1, 1, 1])
}
cuberadius = cuberadius.abs() // negative radii make no sense
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
if (resolution < 4) resolution = 4
if (resolution % 2 === 1 && resolution < 8) resolution = 8 // avoid ugly
let roundradius = parseOptionAs3DVector(options, 'roundradius', [0.2, 0.2, 0.2])
// slight hack for now - total radius stays ok
roundradius = Vector3D.Create(Math.max(roundradius.x, minRR), Math.max(roundradius.y, minRR), Math.max(roundradius.z, minRR))
let innerradius = cuberadius.minus(roundradius)
if (innerradius.x < 0 || innerradius.y < 0 || innerradius.z < 0) {
throw new Error('roundradius <= radius!')
}
let res = sphere({radius: 1, resolution: resolution})
res = res.scale(roundradius)
innerradius.x > EPS && (res = res.stretchAtPlane([1, 0, 0], [0, 0, 0], 2 * innerradius.x))
innerradius.y > EPS && (res = res.stretchAtPlane([0, 1, 0], [0, 0, 0], 2 * innerradius.y))
innerradius.z > EPS && (res = res.stretchAtPlane([0, 0, 1], [0, 0, 0], 2 * innerradius.z))
res = res.translate([-innerradius.x + center.x, -innerradius.y + center.y, -innerradius.z + center.z])
res = res.reTesselated()
res.properties.roundedCube = new Properties()
res.properties.roundedCube.center = new Vertex(center)
res.properties.roundedCube.facecenters = [
new Connector(new Vector3D([cuberadius.x, 0, 0]).plus(center), [1, 0, 0], [0, 0, 1]),
new Connector(new Vector3D([-cuberadius.x, 0, 0]).plus(center), [-1, 0, 0], [0, 0, 1]),
new Connector(new Vector3D([0, cuberadius.y, 0]).plus(center), [0, 1, 0], [0, 0, 1]),
new Connector(new Vector3D([0, -cuberadius.y, 0]).plus(center), [0, -1, 0], [0, 0, 1]),
new Connector(new Vector3D([0, 0, cuberadius.z]).plus(center), [0, 0, 1], [1, 0, 0]),
new Connector(new Vector3D([0, 0, -cuberadius.z]).plus(center), [0, 0, -1], [1, 0, 0])
]
return res
}
/** Create a polyhedron using Openscad style arguments.
* Define face vertices clockwise looking from outside.
* @param {Object} [options] - options for construction
* @returns {CSG} new 3D solid
*/
const polyhedron = function (options) {
options = options || {}
if (('points' in options) !== ('faces' in options)) {
throw new Error("polyhedron needs 'points' and 'faces' arrays")
}
let vertices = parseOptionAs3DVectorList(options, 'points', [
[1, 1, 0],
[1, -1, 0],
[-1, -1, 0],
[-1, 1, 0],
[0, 0, 1]
])
.map(function (pt) {
return new Vertex(pt)
})
let faces = parseOption(options, 'faces', [
[0, 1, 4],
[1, 2, 4],
[2, 3, 4],
[3, 0, 4],
[1, 0, 3],
[2, 1, 3]
])
// Openscad convention defines inward normals - so we have to invert here
faces.forEach(function (face) {
face.reverse()
})
let polygons = faces.map(function (face) {
return new Polygon(face.map(function (idx) {
return vertices[idx]
}))
})
// TODO: facecenters as connectors? probably overkill. Maybe centroid
// the re-tesselation here happens because it's so easy for a user to
// create parametrized polyhedrons that end up with 1-2 dimensional polygons.
// These will create infinite loops at CSG.Tree()
return CSG.fromPolygons(polygons).reTesselated()
}
module.exports = {
cube,
sphere,
roundedCube,
cylinder,
roundedCylinder,
cylinderElliptic,
polyhedron
}

Some files were not shown because too many files have changed in this diff Show More