diff --git a/.gitignore b/.gitignore index 39caed47..3a8c92a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ /build/ /.gradle/ /.idea/ -/.nb-gradle/ \ No newline at end of file +/.nb-gradle/ +/bin/ +/.settings/ +/.classpath +/.project +/.vscode/ diff --git a/.travis.yml b/.travis.yml index 8de9f2e7..990acaac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,16 @@ sudo: false language: java jdk: - - oraclejdk8 + #- oraclejdk8 + #- oraclejdk9 + - openjdk11 + - openjdk12 + # - oraclejdk10 openjdk 10 is broken on travis (maybe they use the wrong download link?) -before_install: - - "export DISPLAY=:99.0" - - "export TERM=dumb" - - "sh -e /etc/init.d/xvfb start" +# before_install: +# - "export DISPLAY=:99.0" +# - "export TERM=dumb" +# - "sh -e /etc/init.d/xvfb start" install: - TERM=dumb ./gradlew --refresh-dependencies --stacktrace diff --git a/README.md b/README.md index 5c3c02ae..d9df0ebe 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,21 @@ JCSG ======= -[![Build Status](https://travis-ci.org/miho/JCSG.png?branch=master)](https://travis-ci.org/miho/JCSG) [ ![Download](https://api.bintray.com/packages/miho/JCSG/jcsg/images/download.svg) ](https://bintray.com/miho/JCSG/jcsg/_latestVersion) +[![Build Status](https://travis-ci.org/miho/JCSG.png?branch=master)](https://travis-ci.org/miho/JCSG) +[![Javadocs](https://www.javadoc.io/badge/eu.mihosoft.vrl.jcsg/jcsg.svg)](https://www.javadoc.io/doc/eu.mihosoft.vrl.jcsg/jcsg) + + + + +
+ Java implementation of BSP based CSG (Constructive Solid Geometry). It is the only simple and free Java implementation I am aware of. This implementation uses an optimized CSG algorithm based on [csg.js](https://github.com/evanw/csg.js) (see `CSG` and `Node` classes). Thanks to the author for creating the [csg.js](https://github.com/evanw/csg.js) library. +There's also the related [VCSG](https://github.com/miho/VCSG) library, a [plugin for VRL-Studio](https://github.com/VRL-Studio/VRL-JCSG) and two extension libraries: [JCSG-MeshExtension](https://github.com/miho/JCSG-MeshExtensions) and [JCSG-PathExtension](https://github.com/miho/JCSG-PathExtensions). + In addition to CSG this library provides the following features: - optimized `difference()` and `union()` operations (many thanks to Sebastian Reiter) @@ -28,7 +39,7 @@ To see what's possible with JCSG try [JFXScad](https://github.com/miho/JFXScad). ### Requirements -- Java >= 1.8 +- Java >= 11 - Internet connection (dependencies are downloaded automatically) - IDE: [Gradle](http://www.gradle.org/) Plugin (not necessary for command line usage) @@ -43,7 +54,7 @@ Navigate to the [Gradle](http://www.gradle.org/) project (e.g., `path/to/JCSG`) #### Bash (Linux/OS X/Cygwin/other Unix-like shell) - sh gradlew assemble + bash gradlew assemble #### Windows (CMD) @@ -80,3 +91,9 @@ try { Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); } ``` + +## Thanks to + + - [JetBrains](https://www.jetbrains.com) for their [IntelliJ IDEA](https://www.jetbrains.com/idea/) license(s) + + - Evan Wallace for creating the JavaScript library [csg.js](https://github.com/evanw/csg.js) diff --git a/build.gradle b/build.gradle index bde50443..8715a74d 100644 --- a/build.gradle +++ b/build.gradle @@ -2,18 +2,19 @@ plugins { id 'application' id 'java' id 'maven-publish' - id 'net.nemerosa.versioning' version '1.5.0' - id 'com.jfrog.bintray' version '1.6' + id 'org.openjfx.javafxplugin' version '0.0.7' + //id 'net.nemerosa.versioning' version '2.8.2' + id 'com.jfrog.bintray' version '1.8.4' } -sourceCompatibility = '1.8' +sourceCompatibility = '11' [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' //apply from: 'http://gradle-plugins.mihosoft.eu/latest/vlicenseheader.gradle' //repairHeaders.licenseHeaderText = new File(projectDir,'./license-template.txt') -task wrapper(type: Wrapper, description: 'Creates and deploys the Gradle wrapper to the current directory.') { - gradleVersion = '3.3' +wrapper { + gradleVersion = '6.4' } if (!hasProperty('mainClass')) { @@ -26,6 +27,8 @@ mainClassName = mainClass repositories { mavenCentral() jcenter() + + mavenLocal() } // javadoc is way too strict for my taste. @@ -60,10 +63,10 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.+' - compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' - compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.2' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.2', classifier: 'sources' + // compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' + //compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.8' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.8', classifier: 'sources' compile 'org.slf4j:slf4j-simple:1.6.1' } @@ -94,15 +97,24 @@ jar { 'Created-By': System.properties['java.version'] + " (" + System.properties['java.vendor'] + " " + System.properties['java.vm.version'] + ")", 'Build-Date': project.buildDate, 'Build-Time': project.buildTime, - 'Build-Revision': versioning.info.commit, + //'Build-Revision': versioning.info.commit, 'Specification-Title': project.name, 'Specification-Version': project.version, 'Implementation-Title': project.name, - 'Implementation-Version': project.version + 'Implementation-Version': project.version, + 'Automatic-Module-Name': "eu.mihosoft.jcsg" ) } } +javafx { + modules = [ 'javafx.graphics', 'javafx.fxml'] +} + +test { + maxHeapSize = '2G' +} + def pomConfig = { name 'jcsg' diff --git a/gradle.properties b/gradle.properties index 110d31bb..54491709 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.2 +version = 0.5.8-SNAPSHOT diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 6ffa2378..5c2d1cf0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4686a70d..4c5803d1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Feb 03 21:44:21 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip diff --git a/gradlew b/gradlew index 9aa616c2..8e25e6c1 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## ## @@ -28,16 +44,16 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -154,16 +170,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f9553162..24467a14 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,100 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/eu/mihosoft/jcsg/Bounds.java b/src/main/java/eu/mihosoft/jcsg/Bounds.java index f39ce50c..cbfb4b66 100644 --- a/src/main/java/eu/mihosoft/jcsg/Bounds.java +++ b/src/main/java/eu/mihosoft/jcsg/Bounds.java @@ -1,7 +1,35 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. +/** + * Bounds.java + * + * Copyright 2014-2017 Michael Hoffer . All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are + * those of the authors and should not be interpreted as representing official + * policies, either expressed or implied, of Michael Hoffer + * . */ package eu.mihosoft.jcsg; @@ -18,7 +46,7 @@ public class Bounds { private final Vector3d bounds; private final Vector3d min; private final Vector3d max; - private final CSG csg; + private CSG csg; private final Cube cube; /** @@ -42,7 +70,7 @@ public Bounds(Vector3d min, Vector3d max) { this.max = max.clone(); cube = new Cube(center, bounds); - csg = cube.toCSG(); + } @Override @@ -74,6 +102,11 @@ public Vector3d getBounds() { * @return this bounding box as csg */ public CSG toCSG() { + + if (csg == null) { + csg = cube.toCSG(); + } + return csg; } @@ -133,11 +166,9 @@ public boolean contains(Polygon p) { * @param p polygon to check * @return {@code true} if the polygon intersects this bounding box; * {@code false} otherwise - * @deprecated not implemented yet */ - @Deprecated public boolean intersects(Polygon p) { - throw new UnsupportedOperationException("Implementation missing!"); + return p.vertices.stream().filter(this::contains).count()>0; } /** diff --git a/src/main/java/eu/mihosoft/jcsg/CSG.java b/src/main/java/eu/mihosoft/jcsg/CSG.java index f0e9fd31..8cf0d296 100644 --- a/src/main/java/eu/mihosoft/jcsg/CSG.java +++ b/src/main/java/eu/mihosoft/jcsg/CSG.java @@ -1,47 +1,47 @@ /** * CSG.java * - * Copyright 2014-2014 Michael Hoffer . All rights - * reserved. + * Copyright 2014-2014 Michael Hoffer . All rights reserved. * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following + * disclaimer. * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. * - * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * The views and conclusions contained in the software and documentation are - * those of the authors and should not be interpreted as representing official - * policies, either expressed or implied, of Michael Hoffer + * The views and conclusions contained in the software and documentation are those of the authors and should not be + * interpreted as representing official policies, either expressed or implied, of Michael Hoffer * . */ package eu.mihosoft.jcsg; +import static eu.mihosoft.jcsg.STL.file; import eu.mihosoft.vvecmath.Vector3d; import eu.mihosoft.vvecmath.Transform; import eu.mihosoft.jcsg.ext.quickhull3d.HullUtil; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.scene.paint.Color; @@ -53,18 +53,15 @@ * This implementation is a Java port of * https://github.com/evanw/csg.js/ - * with some additional features like polygon extrude, transformations etc. - * Thanks to the author for creating the CSG.js library.

+ * with some additional features like polygon extrude, transformations etc. Thanks to the author for creating the CSG.js + * library.

* * Implementation Details * - * All CSG operations are implemented in terms of two functions, - * {@link Node#clipTo(Node)} and {@link Node#invert()}, - * which remove parts of a BSP tree inside another BSP tree and swap solid and - * empty space, respectively. To find the union of {@code a} and {@code b}, we - * want to remove everything in {@code a} inside {@code b} and everything in - * {@code b} inside {@code a}, then combine polygons from {@code a} and - * {@code b} into one solid: + * All CSG operations are implemented in terms of two functions, {@link Node#clipTo(Node)} and {@link Node#invert()}, + * which remove parts of a BSP tree inside another BSP tree and swap solid and empty space, respectively. To find the + * union of {@code a} and {@code b}, we want to remove everything in {@code a} inside {@code b} and everything in + * {@code b} inside {@code a}, then combine polygons from {@code a} and {@code b} into one solid: * *
  *     a.clipTo(b);
@@ -72,11 +69,9 @@
  *     a.build(b.allPolygons());
  * 
* - * The only tricky part is handling overlapping coplanar polygons in both trees. - * The code above keeps both copies, but we need to keep them in one tree and - * remove them in the other tree. To remove them from {@code b} we can clip the - * inverse of {@code b} against {@code a}. The code for union now looks like - * this: + * The only tricky part is handling overlapping coplanar polygons in both trees. The code above keeps both copies, but + * we need to keep them in one tree and remove them in the other tree. To remove them from {@code b} we can clip the + * inverse of {@code b} against {@code a}. The code for union now looks like this: * *
  *     a.clipTo(b);
@@ -87,9 +82,8 @@
  *     a.build(b.allPolygons());
  * 
* - * Subtraction and intersection naturally follow from set operations. If union - * is {@code A | B}, differenceion is {@code A - B = ~(~A | B)} and intersection - * is {@code A & B = + * Subtraction and intersection naturally follow from set operations. If union is {@code A | B}, differenceion is + * {@code A - B = ~(~A | B)} and intersection is {@code A & B = * ~(~A | ~B)} where {@code ~} is the complement operator. */ public class CSG { @@ -204,8 +198,7 @@ public CSG optimization(OptType type) { } /** - * Return a new CSG solid representing the union of this csg and the - * specified csg. + * Return a new CSG solid representing the union of this csg and the specified csg. * * Note: Neither this csg nor the specified csg are weighted. * @@ -239,33 +232,32 @@ public CSG union(CSG csg) { return _unionNoOpt(csg); } } - + /** * Returns a csg consisting of the polygons of this csg and the specified csg. - * - * The purpose of this method is to allow fast union operations for objects - * that do not intersect. - * - *

WARNING: this method does not apply the csg algorithms. Therefore, - * please ensure that this csg and the specified csg do not intersect. - * + * + * The purpose of this method is to allow fast union operations for objects that do not intersect. + * + *

+ * WARNING: this method does not apply the csg algorithms. Therefore, please ensure that this csg and the + * specified csg do not intersect. + * * @param csg csg - * + * * @return a csg consisting of the polygons of this csg and the specified csg */ public CSG dumbUnion(CSG csg) { - + CSG result = this.clone(); CSG other = csg.clone(); - + result.polygons.addAll(other.polygons); - + return result; } /** - * Return a new CSG solid representing the union of this csg and the - * specified csgs. + * Return a new CSG solid representing the union of this csg and the specified csgs. * * Note: Neither this csg nor the specified csg are weighted. * @@ -299,8 +291,7 @@ public CSG union(List csgs) { } /** - * Return a new CSG solid representing the union of this csg and the - * specified csgs. + * Return a new CSG solid representing the union of this csg and the specified csgs. * * Note: Neither this csg nor the specified csg are weighted. * @@ -412,9 +403,8 @@ private CSG _unionPolygonBoundsOpt(CSG csg) { } /** - * Optimizes for intersection. If csgs do not intersect create a new csg - * that consists of the polygon lists of this csg and the specified csg. In - * this case no further space partitioning is performed. + * Optimizes for intersection. If csgs do not intersect create a new csg that consists of the polygon lists of this + * csg and the specified csg. In this case no further space partitioning is performed. * * @param csg csg * @return the union of this csg and the specified csg @@ -456,8 +446,7 @@ private CSG _unionNoOpt(CSG csg) { } /** - * Return a new CSG solid representing the difference of this csg and the - * specified csgs. + * Return a new CSG solid representing the difference of this csg and the specified csgs. * * Note: Neither this csg nor the specified csgs are weighted. * @@ -493,8 +482,7 @@ public CSG difference(List csgs) { } /** - * Return a new CSG solid representing the difference of this csg and the - * specified csgs. + * Return a new CSG solid representing the difference of this csg and the specified csgs. * * Note: Neither this csg nor the specified csgs are weighted. * @@ -520,8 +508,7 @@ public CSG difference(CSG... csgs) { } /** - * Return a new CSG solid representing the difference of this csg and the - * specified csg. + * Return a new CSG solid representing the difference of this csg and the specified csg. * * Note: Neither this csg nor the specified csg are weighted. * @@ -604,8 +591,7 @@ private CSG _differenceNoOpt(CSG csg) { } /** - * Return a new CSG solid representing the intersection of this csg and the - * specified csg. + * Return a new CSG solid representing the intersection of this csg and the specified csg. * * Note: Neither this csg nor the specified csg are weighted. * @@ -641,8 +627,7 @@ public CSG intersect(CSG csg) { } /** - * Return a new CSG solid representing the intersection of this csg and the - * specified csgs. + * Return a new CSG solid representing the intersection of this csg and the specified csgs. * * Note: Neither this csg nor the specified csgs are weighted. * @@ -679,8 +664,7 @@ public CSG intersect(List csgs) { } /** - * Return a new CSG solid representing the intersection of this csg and the - * specified csgs. + * Return a new CSG solid representing the intersection of this csg and the specified csgs. * * Note: Neither this csg nor the specified csgs are weighted. * @@ -717,6 +701,7 @@ public String toStlString() { return sb.toString(); } + /** * Returns this csg in STL string format. * @@ -747,6 +732,17 @@ public CSG color(Color c) { } public ObjFile toObj() { + // we triangulate the polygon to ensure + // compatibility with 3d printer software + return toObj(3); + } + + public ObjFile toObj(int maxNumberOfVerts) { + + if (maxNumberOfVerts != 3) { + throw new UnsupportedOperationException( + "maxNumberOfVerts > 3 not supported yet"); + } StringBuilder objSb = new StringBuilder(); @@ -822,6 +818,12 @@ public PolygonStruct(PropertyStorage storage, List indices, String mate append(index2).append(" "). append(index3).append("\n"); } +// +// objSb.append("f "); +// for (int i = 0; i < pVerts.size(); i++) { +// objSb.append(pVerts.get(i)).append(" "); +// } + objSb.append("\n"); } objSb.append("\n# End Group v3d.csg").append("\n"); @@ -941,7 +943,6 @@ public CSG transformed(Transform transform) { return result; } - // TODO finish experiment (20.7.2014) public MeshContainer toJavaFXMesh() { @@ -1109,13 +1110,15 @@ public Bounds getBounds() { return new Bounds(Vector3d.ZERO, Vector3d.ZERO); } - double minX = Double.POSITIVE_INFINITY; - double minY = Double.POSITIVE_INFINITY; - double minZ = Double.POSITIVE_INFINITY; + Vector3d initial = polygons.get(0).vertices.get(0).pos; - double maxX = Double.NEGATIVE_INFINITY; - double maxY = Double.NEGATIVE_INFINITY; - double maxZ = Double.NEGATIVE_INFINITY; + double minX = initial.x(); + double minY = initial.y(); + double minZ = initial.z(); + + double maxX = initial.x(); + double maxY = initial.y(); + double maxZ = initial.z(); for (Polygon p : getPolygons()) { @@ -1180,4 +1183,46 @@ public static enum OptType { NONE } + /** + * Computes and returns the volume of this CSG based on a triangulated version + * of the internal mesh. + * @return volume of this csg + */ + public double computeVolume() { + if(getPolygons().isEmpty()) return 0; + + // triangulate polygons (parallel for larger meshes) + Stream polyStream; + if(getPolygons().size()>200) { + polyStream = getPolygons().parallelStream(); + } else { + polyStream = getPolygons().stream(); + } + List triangles = polyStream. + flatMap(poly->poly.toTriangles().stream()). + collect(Collectors.toList()); + + // compute sum over signed volumes of triangles + // we use parallel streams for larger meshes + // see http://chenlab.ece.cornell.edu/Publication/Cha/icip01_Cha.pdf + Stream triangleStream; + if(triangles.size()>200) { + triangleStream = triangles.parallelStream(); + } else { + triangleStream = triangles.stream(); + } + + double volume = triangleStream.mapToDouble(tri-> { + Vector3d p1 = tri.vertices.get(0).pos; + Vector3d p2 = tri.vertices.get(1).pos; + Vector3d p3 = tri.vertices.get(2).pos; + + return p1.dot(p2.crossed(p3)) / 6.0; + }).sum(); + + volume = Math.abs(volume); + + return volume; + } + } diff --git a/src/main/java/eu/mihosoft/jcsg/Cube.java b/src/main/java/eu/mihosoft/jcsg/Cube.java index 3d314df4..53e755a1 100644 --- a/src/main/java/eu/mihosoft/jcsg/Cube.java +++ b/src/main/java/eu/mihosoft/jcsg/Cube.java @@ -36,6 +36,7 @@ import eu.mihosoft.vvecmath.Vector3d; import eu.mihosoft.vvecmath.Transform; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -103,7 +104,66 @@ public Cube(double w, double h, double d) { this(Vector3d.ZERO, Vector3d.xyz(w, h, d)); } - @Override +// public List toPolygons() { +// List result = new ArrayList<>(6); +// +// Vector3d centerOffset = dimensions.times(0.5); +// +// result.addAll(Arrays.asList(new Polygon[]{ +// Polygon.fromPoints( +// centerOffset.times(-1, -1, -1), +// centerOffset.times(1, -1, -1), +// centerOffset.times(1, -1, 1), +// centerOffset.times(-1, -1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(1, -1, -1), +// centerOffset.times(1, 1, -1), +// centerOffset.times(1, 1, 1), +// centerOffset.times(1, -1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(1, 1, -1), +// centerOffset.times(-1, 1, -1), +// centerOffset.times(-1, 1, 1), +// centerOffset.times(1, 1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(1, 1, 1), +// centerOffset.times(-1, 1, 1), +// centerOffset.times(-1, -1, 1), +// centerOffset.times(1, -1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(-1, 1, 1), +// centerOffset.times(-1, 1, -1), +// centerOffset.times(-1, -1, -1), +// centerOffset.times(-1, -1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(-1, 1, -1), +// centerOffset.times(1, 1, -1), +// centerOffset.times(1, -1, -1), +// centerOffset.times(-1, -1, -1) +// ) +// } +// )); +// +// if(!centered) { +// Transform centerTransform = Transform.unity(). +// translate(dimensions.x() / 2.0, +// dimensions.y() / 2.0, +// dimensions.z() / 2.0); +// +// for (Polygon p : result) { +// p.transform(centerTransform); +// } +// } +// +// return result; +// } + + public List toPolygons() { int[][][] a = { @@ -183,6 +243,7 @@ public PropertyStorage getProperties() { /** * Defines that this cube will not be centered. + * * @return this cube */ public Cube noCenter() { diff --git a/src/main/java/eu/mihosoft/jcsg/Cylinder.java b/src/main/java/eu/mihosoft/jcsg/Cylinder.java index 80ebfad7..122a12da 100644 --- a/src/main/java/eu/mihosoft/jcsg/Cylinder.java +++ b/src/main/java/eu/mihosoft/jcsg/Cylinder.java @@ -150,8 +150,8 @@ public List toPolygons() { final Vector3d axisZ = ray.normalized(); boolean isY = (Math.abs(axisZ.y()) > 0.5); final Vector3d axisX = Vector3d.xyz(isY ? 1 : 0, !isY ? 1 : 0, 0). - cross(axisZ).normalized(); - final Vector3d axisY = axisX.cross(axisZ).normalized(); + crossed(axisZ).normalized(); + final Vector3d axisY = axisX.crossed(axisZ).normalized(); Vertex startV = new Vertex(s, axisZ.negated()); Vertex endV = new Vertex(e, axisZ.normalized()); List polygons = new ArrayList<>(); diff --git a/src/main/java/eu/mihosoft/jcsg/Edge.java b/src/main/java/eu/mihosoft/jcsg/Edge.java index ac3d72ad..86dd106a 100644 --- a/src/main/java/eu/mihosoft/jcsg/Edge.java +++ b/src/main/java/eu/mihosoft/jcsg/Edge.java @@ -599,11 +599,11 @@ private static List boundaryEdgesOfPlaneGroup(List planeGroup) { Stream pStream; - if (planeGroup.size() > 200) { - pStream = planeGroup.parallelStream(); - } else { + // if (planeGroup.size() > 200) { + // pStream = planeGroup.parallelStream(); + // } else { pStream = planeGroup.stream(); - } + // } pStream.map((p) -> Edge.fromPolygon(p)).forEach((pEdges) -> { edges.addAll(pEdges); @@ -611,11 +611,11 @@ private static List boundaryEdgesOfPlaneGroup(List planeGroup) { Stream edgeStream; - if (edges.size() > 200) { - edgeStream = edges.parallelStream(); - } else { + // if (edges.size() > 200) { + // edgeStream = edges.parallelStream(); + // } else { edgeStream = edges.stream(); - } + // } // find potential boundary edges, i.e., edges that occur once (freq=1) List potentialBoundaryEdges = new ArrayList<>(); @@ -716,8 +716,8 @@ private static List> searchPlaneGroups(List polygons) { continue; } - Vector3d nOuter = pOuter.plane.normal; - Vector3d nInner = pInner.plane.normal; + Vector3d nOuter = pOuter._csg_plane.normal; + Vector3d nInner = pInner._csg_plane.normal; // TODO do we need radians or degrees? double angle = nOuter.angle(nInner); diff --git a/src/main/java/eu/mihosoft/jcsg/Extrude.java b/src/main/java/eu/mihosoft/jcsg/Extrude.java index 1b0a32ba..c0072b03 100644 --- a/src/main/java/eu/mihosoft/jcsg/Extrude.java +++ b/src/main/java/eu/mihosoft/jcsg/Extrude.java @@ -1,7 +1,7 @@ /** * Extrude.java * - * Copyright 2014-2014 Michael Hoffer . All rights + * Copyright 2014-2017 Michael Hoffer . All rights * reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,6 +33,7 @@ */ package eu.mihosoft.jcsg; +import eu.mihosoft.vvecmath.Transform; import eu.mihosoft.vvecmath.Vector3d; import eu.mihosoft.jcsg.ext.org.poly2tri.PolygonUtil; import java.util.ArrayList; @@ -82,6 +83,105 @@ public static CSG points(Vector3d dir, List points) { return extrude(dir, Polygon.fromPoints(toCCW(newList))); } + + /** + * Extrudes the specified path (convex or concave polygon without holes or + * intersections, specified in CCW) into the specified direction. + * + * @param dir direction + * @param points path (convex or concave polygon without holes or + * intersections) + * + * @return a list containing the extruded polygon + */ + public static List points(Vector3d dir, boolean top, boolean bottom, Vector3d... points) { + + return extrude(dir, Polygon.fromPoints(toCCW(Arrays.asList(points))), top, bottom); + } + + /** + * Extrudes the specified path (convex or concave polygon without holes or + * intersections, specified in CCW) into the specified direction. + * + * @param dir direction + * @param points1 path (convex or concave polygon without holes or + * intersections) + * @param points1 path (convex or concave polygon without holes or + * intersections) + * + * @return a list containing the extruded polygon + */ + public static List points(Vector3d dir, boolean top, boolean bottom, List points1) { + + List newList1 = new ArrayList<>(points1); + + return extrude(dir, Polygon.fromPoints(toCCW(newList1)), top, bottom); + } + + /** + * Combines two polygons into one CSG object. Polygons p1 and p2 are treated as top and + * bottom of a tube segment with p1 and p2 as the profile. Note: both polygons must have the + * same number of vertices. This method does not guarantee intersection-free CSGs. It is in the + * responsibility of the caller to ensure that the orientation of p1 and p2 allow for + * intersection-free combination of both. + * + * @param p1 first polygon + * @param p2 second polygon + * @return List of polygons + */ + public static CSG combine(Polygon p1, Polygon p2) { + return CSG.fromPolygons(combine(p1,p2,true,true)); + } + + /** + * Combines two polygons into one CSG object. Polygons p1 and p2 are treated as top and + * bottom of a tube segment with p1 and p2 as the profile. Note: both polygons must have the + * same number of vertices. This method does not guarantee intersection-free CSGs. It is in the + * responsibility of the caller to ensure that the orientation of p1 and p2 allow for + * intersection-free combination of both. + * + * @param p1 first polygon + * @param p2 second polygon + * @param bottom defines whether to close the bottom of the tube + * @param top defines whether to close the top of the tube + * @return List of polygons + */ + public static List combine(Polygon p1, Polygon p2, boolean bottom, boolean top) { + List newPolygons = new ArrayList<>(); + + if (p1.vertices.size() != p2.vertices.size()) { + throw new RuntimeException("Polygons must have the same number of vertices"); + } + + int numVertices = p1.vertices.size(); + + if (bottom) { + newPolygons.add(p1.flipped()); + } + + for (int i = 0; i < numVertices; i++) { + + int nexti = (i + 1) % numVertices; + + Vector3d bottomV1 = p1.vertices.get(i).pos; + Vector3d topV1 = p2.vertices.get(i).pos; + Vector3d bottomV2 = p1.vertices.get(nexti).pos; + Vector3d topV2 = p2.vertices.get(nexti).pos; + + List pPoints; + + pPoints = Arrays.asList(bottomV2, topV2, topV1); + newPolygons.add(Polygon.fromPoints(pPoints, p1.getStorage())); + pPoints = Arrays.asList(bottomV2, topV1, bottomV1); + newPolygons.add(Polygon.fromPoints(pPoints, p1.getStorage())); + } + + if (top) { + newPolygons.add(p2); + } + + return newPolygons; + } private static CSG extrude(Vector3d dir, Polygon polygon1) { List newPolygons = new ArrayList<>(); @@ -118,6 +218,78 @@ private static CSG extrude(Vector3d dir, Polygon polygon1) { } + + private static List extrude(Vector3d dir, Polygon polygon1, boolean top, boolean bottom) { + List newPolygons = new ArrayList<>(); + + + if (bottom) { + newPolygons.addAll(PolygonUtil.concaveToConvex(polygon1)); + } + + Polygon polygon2 = polygon1.translated(dir); + + Transform rot = Transform.unity(); + + Vector3d a = polygon2.getPlane().getNormal().normalized(); + Vector3d b = dir.normalized(); + + Vector3d c = a.crossed(b); + + double l = c.magnitude(); // sine of angle + + if (l > 1e-9) { + + Vector3d axis = c.times(1.0 / l); + double angle = a.angle(b); + + double sx = 0; + double sy = 0; + double sz = 0; + + int n = polygon2.vertices.size(); + + for (Vertex v : polygon2.vertices) { + sx += v.pos.x(); + sy += v.pos.y(); + sz += v.pos.z(); + } + + Vector3d center = Vector3d.xyz(sx / n, sy / n, sz / n); + + rot = rot.rot(center, axis, angle * Math.PI / 180.0); + + for (Vertex v : polygon2.vertices) { + v.pos = rot.transform(v.pos); + } + } + + int numvertices = polygon1.vertices.size(); + for (int i = 0; i < numvertices; i++) { + + int nexti = (i + 1) % numvertices; + + Vector3d bottomV1 = polygon1.vertices.get(i).pos; + Vector3d topV1 = polygon2.vertices.get(i).pos; + Vector3d bottomV2 = polygon1.vertices.get(nexti).pos; + Vector3d topV2 = polygon2.vertices.get(nexti).pos; + + List pPoints = Arrays.asList(bottomV2, topV2, topV1, bottomV1); + + newPolygons.add(Polygon.fromPoints(pPoints, polygon1.getStorage())); + } + + polygon2 = polygon2.flipped(); + List topPolygons = PolygonUtil.concaveToConvex(polygon2); + if (top) { + newPolygons.addAll(topPolygons); + } + + return newPolygons; + + } + + static List toCCW(List points) { List result = new ArrayList<>(points); @@ -140,6 +312,12 @@ static List toCW(List points) { return result; } + /** + * Indicates whether the specified polygon is defined counter-clockwise. + * @param polygon polygon + * @return {@code true} if the specified polygon is defined counter-clockwise; + * {@code false} otherwise + */ public static boolean isCCW(Polygon polygon) { // thanks to Sepp Reiter for explaining me the algorithm! @@ -205,7 +383,7 @@ public static boolean isCCW(Polygon polygon) { private static double normalizedX(Vector3d v1, Vector3d v2) { Vector3d v2MinusV1 = v2.minus(v1); - return v2MinusV1.dividedBy(v2MinusV1.magnitude()).times(Vector3d.X_ONE).x(); + return v2MinusV1.divided(v2MinusV1.magnitude()).times(Vector3d.X_ONE).x(); } // public static void main(String[] args) { diff --git a/src/main/java/eu/mihosoft/jcsg/FileUtil.java b/src/main/java/eu/mihosoft/jcsg/FileUtil.java index 44c180d9..e3c00490 100644 --- a/src/main/java/eu/mihosoft/jcsg/FileUtil.java +++ b/src/main/java/eu/mihosoft/jcsg/FileUtil.java @@ -36,6 +36,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.logging.Level; +import java.util.logging.Logger; /** * File util class. @@ -74,4 +76,30 @@ public static void write(Path p, String s) throws IOException { public static String read(Path p) throws IOException { return new String(Files.readAllBytes(p), Charset.forName("UTF-8")); } + + + /** + * Saves the specified csg using STL ASCII format. + * + * @param path destination path + * @param csg csg to save + * @throws java.io.IOException + */ + public static void toStlFile(Path path, CSG csg) throws IOException { + try (BufferedWriter out = Files.newBufferedWriter(path, Charset.forName("UTF-8"), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + + out.append("solid v3d.csg\n"); + csg.getPolygons().stream().forEach( + (Polygon p) -> { + try { + out.append(p.toStlString()); + } catch (IOException ex) { + Logger.getLogger(CSG.class.getName()).log(Level.SEVERE, null, ex); + throw new RuntimeException(ex); + } + }); + out.append("endsolid v3d.csg\n"); + } + } } diff --git a/src/main/java/eu/mihosoft/jcsg/Node.java b/src/main/java/eu/mihosoft/jcsg/Node.java index c88f2862..c5d91cb1 100644 --- a/src/main/java/eu/mihosoft/jcsg/Node.java +++ b/src/main/java/eu/mihosoft/jcsg/Node.java @@ -128,7 +128,7 @@ public void invert() { }); if (this.plane == null && !polygons.isEmpty()) { - this.plane = polygons.get(0).plane.clone(); + this.plane = polygons.get(0)._csg_plane.clone(); } else if (this.plane == null && polygons.isEmpty()) { System.err.println("Please fix me! I don't know what to do?"); @@ -238,7 +238,7 @@ public final void build(List polygons) { if (polygons.isEmpty()) return; if (this.plane == null) { - this.plane = polygons.get(0).plane.clone(); + this.plane = polygons.get(0)._csg_plane.clone(); } polygons = polygons.stream().filter(p->p.isValid()).distinct().collect(Collectors.toList()); diff --git a/src/main/java/eu/mihosoft/jcsg/Plane.java b/src/main/java/eu/mihosoft/jcsg/Plane.java index 3ef82165..37f1407c 100644 --- a/src/main/java/eu/mihosoft/jcsg/Plane.java +++ b/src/main/java/eu/mihosoft/jcsg/Plane.java @@ -94,7 +94,7 @@ public Plane(Vector3d normal, double dist) { * @return a plane */ public static Plane createFromPoints(Vector3d a, Vector3d b, Vector3d c) { - Vector3d n = b.minus(a).cross(c.minus(a)).normalized(); + Vector3d n = b.minus(a).crossed(c.minus(a)).normalized(); return new Plane(n, n.dot(a)); } @@ -152,7 +152,7 @@ public void splitPolygon( switch (polygonType) { case COPLANAR: //System.out.println(" -> coplanar"); - (this.normal.dot(polygon.plane.normal) > 0 ? coplanarFront : coplanarBack).add(polygon); + (this.normal.dot(polygon._csg_plane.normal) > 0 ? coplanarFront : coplanarBack).add(polygon); break; case FRONT: //System.out.println(" -> front"); diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index e820a0db..faf8f56c 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -64,7 +64,17 @@ public final class Polygon { * * Note: uses first three vertices to define the plane. */ - public final Plane plane; + public final Plane _csg_plane; + private eu.mihosoft.vvecmath.Plane plane; + + /** + * Returns the plane defined by this triangle. + * + * @return plane + */ + public eu.mihosoft.vvecmath.Plane getPlane() { + return plane; + } void setStorage(PropertyStorage storage) { this.shared = storage; @@ -95,15 +105,16 @@ public static List fromConcavePoints(List points) { } /** - * Indicates whether this polyon is valid, i.e., if it - * @return + * Indicates whether this polyon is valid, i.e., if it + * + * @return */ public boolean isValid() { return valid; } - + private boolean valid = true; - + /** * Constructor. Creates a new polygon that consists of the specified * vertices. @@ -117,30 +128,32 @@ public boolean isValid() { public Polygon(List vertices, PropertyStorage shared) { this.vertices = vertices; this.shared = shared; - this.plane = Plane.createFromPoints( + this._csg_plane = Plane.createFromPoints( vertices.get(0).pos, vertices.get(1).pos, vertices.get(2).pos); - + this.plane = eu.mihosoft.vvecmath.Plane. + fromPointAndNormal(centroid(), _csg_plane.normal); + validateAndInit(vertices); } private void validateAndInit(List vertices1) { for (Vertex v : vertices1) { - v.normal = plane.normal; + v.normal = _csg_plane.normal; } - if (Vector3d.ZERO.equals(plane.normal)) { + if (Vector3d.ZERO.equals(_csg_plane.normal)) { valid = false; System.err.println( - "Normal is zero! Probably, duplicate points have been specified!\n\n"+toStlString()); + "Normal is zero! Probably, duplicate points have been specified!\n\n" + toStlString()); // throw new RuntimeException( // "Normal is zero! Probably, duplicate points have been specified!\n\n"+toStlString()); - } - - if(vertices.size()<3) { + } + + if (vertices.size() < 3) { throw new RuntimeException( "Invalid polygon: at least 3 vertices expected, got: " - + vertices.size()); + + vertices.size()); } } @@ -155,11 +168,14 @@ private void validateAndInit(List vertices1) { */ public Polygon(List vertices) { this.vertices = vertices; - this.plane = Plane.createFromPoints( + this._csg_plane = Plane.createFromPoints( vertices.get(0).pos, vertices.get(1).pos, vertices.get(2).pos); - + + this.plane = eu.mihosoft.vvecmath.Plane. + fromPointAndNormal(centroid(), _csg_plane.normal); + validateAndInit(vertices); } @@ -197,7 +213,8 @@ public Polygon flip() { }); Collections.reverse(vertices); - plane.flip(); + _csg_plane.flip(); + this.plane = plane.flipped(); return this; } @@ -241,8 +258,7 @@ public StringBuilder toStlString(StringBuilder sb) { String firstVertexStl = this.vertices.get(0).toStlString(); for (int i = 0; i < this.vertices.size() - 2; i++) { sb. - append(" facet normal ").append( - this.plane.normal.toStlString()).append("\n"). + append(" facet normal ").append(this._csg_plane.normal.toStlString()).append("\n"). append(" outer loop\n"). append(" ").append(firstVertexStl).append("\n"). append(" "); @@ -257,6 +273,38 @@ public StringBuilder toStlString(StringBuilder sb) { return sb; } + /** + * Returns a triangulated version of this polygon. + * + * @return triangles + */ + public List toTriangles() { + + List result = new ArrayList<>(); + + if (this.vertices.size() >= 3) { + + // TODO: improve the triangulation? + // + // If our polygon has more vertices, create + // multiple triangles: + Vertex firstVertexStl = this.vertices.get(0); + for (int i = 0; i < this.vertices.size() - 2; i++) { + + // create triangle + Polygon polygon = Polygon.fromPoints( + firstVertexStl.pos, + this.vertices.get(i + 1).pos, + this.vertices.get(i + 2).pos + ); + + result.add(polygon); + } + } + + return result; + } + /** * Translates this polygon. * @@ -272,7 +320,11 @@ public Polygon translate(Vector3d v) { Vector3d b = this.vertices.get(1).pos; Vector3d c = this.vertices.get(2).pos; - this.plane.normal = b.minus(a).cross(c.minus(a)); + // TODO plane update correct? + this._csg_plane.normal = b.minus(a).crossed(c.minus(a)); + + this.plane = eu.mihosoft.vvecmath.Plane. + fromPointAndNormal(centroid(), _csg_plane.normal); return this; } @@ -312,8 +364,15 @@ public Polygon transform(Transform transform) { Vector3d b = this.vertices.get(1).pos; Vector3d c = this.vertices.get(2).pos; - this.plane.normal = b.minus(a).cross(c.minus(a)).normalized(); - this.plane.dist = this.plane.normal.dot(a); + this._csg_plane.normal = b.minus(a).crossed(c.minus(a)).normalized(); + this._csg_plane.dist = this._csg_plane.normal.dot(a); + + this.plane = eu.mihosoft.vvecmath.Plane. + fromPointAndNormal(centroid(), _csg_plane.normal); + + vertices.forEach((vertex) -> { + vertex.normal = plane.getNormal(); + }); if (transform.isMirror()) { // the transformation includes mirroring. flip polygon @@ -447,27 +506,119 @@ public Bounds getBounds() { Vector3d.xyz(maxX, maxY, maxZ)); } + public Vector3d centroid() { + Vector3d sum = Vector3d.zero(); + + for (Vertex v : vertices) { + sum = sum.plus(v.pos); + } + + return sum.times(1.0 / vertices.size()); + } + + /** + * Indicates whether the specified point is contained within this polygon. + * + * @param p point + * @return {@code true} if the point is inside the polygon or on one of the + * edges; {@code false} otherwise + */ public boolean contains(Vector3d p) { - // taken from http://www.java-gaming.org/index.php?topic=26013.0 - // and http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html - double px = p.x(); - double py = p.y(); + + // P not on the plane + if (plane.distance(p) > Plane.EPSILON) { + return false; + } + + // if P is on one of the vertices, return true + for (int i = 0; i < vertices.size() - 1; i++) { + if (p.minus(vertices.get(i).pos).magnitude() < Plane.EPSILON) { + return true; + } + } + + // if P is on the plane, we proceed with projection to XY plane + // + // P1--P------P2 + // ^ + // | + // P is on the segment if( dist(P1,P) + dist(P2,P) - dist(P1,P2) < TOL) + for (int i = 0; i < vertices.size() - 1; i++) { + + Vector3d p1 = vertices.get(i).pos; + Vector3d p2 = vertices.get(i + 1).pos; + + boolean onASegment = p1.minus(p).magnitude() + p2.minus(p).magnitude() + - p1.minus(p2).magnitude() < Plane.EPSILON; + + if (onASegment) { + return true; + } + } + + // find projection plane + // we start with XY plane + int coordIndex1 = 0; + int coordIndex2 = 1; + + boolean orthogonalToXY = Math.abs(eu.mihosoft.vvecmath.Plane.XY_PLANE.getNormal() + .dot(plane.getNormal())) < Plane.EPSILON; + + boolean foundProjectionPlane = false; + if (!orthogonalToXY && !foundProjectionPlane) { + coordIndex1 = 0; + coordIndex2 = 1; + foundProjectionPlane = true; + } + + boolean orthogonalToXZ = Math.abs(eu.mihosoft.vvecmath.Plane.XZ_PLANE.getNormal() + .dot(plane.getNormal())) < Plane.EPSILON; + + if (!orthogonalToXZ && !foundProjectionPlane) { + coordIndex1 = 0; + coordIndex2 = 2; + foundProjectionPlane = true; + } + + boolean orthogonalToYZ = Math.abs(eu.mihosoft.vvecmath.Plane.YZ_PLANE.getNormal() + .dot(plane.getNormal())) < Plane.EPSILON; + + if (!orthogonalToYZ && !foundProjectionPlane) { + coordIndex1 = 1; + coordIndex2 = 2; + foundProjectionPlane = true; + } + + // see from http://www.java-gaming.org/index.php?topic=26013.0 + // see http://alienryderflex.com/polygon/ + // see http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + int i, j = vertices.size() - 1; boolean oddNodes = false; - double x2 = vertices.get(vertices.size() - 1).pos.x(); - double y2 = vertices.get(vertices.size() - 1).pos.y(); - double x1, y1; - for (int i = 0; i < vertices.size(); x2 = x1, y2 = y1, ++i) { - x1 = vertices.get(i).pos.x(); - y1 = vertices.get(i).pos.y(); - if (((y1 < py) && (y2 >= py)) - || (y1 >= py) && (y2 < py)) { - if ((py - y1) / (y2 - y1) - * (x2 - x1) < (px - x1)) { - oddNodes = !oddNodes; - } + double x = p.get(coordIndex1); + double y = p.get(coordIndex2); + for (i = 0; i < vertices.size(); i++) { + double xi = vertices.get(i).pos.get(coordIndex1); + double yi = vertices.get(i).pos.get(coordIndex2); + double xj = vertices.get(j).pos.get(coordIndex1); + double yj = vertices.get(j).pos.get(coordIndex2); + if ((yi < y && yj >= y + || yj < y && yi >= y) + && (xi <= x || xj <= x)) { + oddNodes ^= (xi + (y - yi) / (yj - yi) * (xj - xi) < x); } + j = i; } return oddNodes; + + } + + @Deprecated + public boolean intersects(Polygon p) { + if (!getBounds().intersects(p.getBounds())) { + return false; + } + + throw new UnsupportedOperationException("Not implemented"); } public boolean contains(Polygon p) { diff --git a/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/SmoothingGroups.java b/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/SmoothingGroups.java index 827227da..a0a0dbf6 100644 --- a/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/SmoothingGroups.java +++ b/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/SmoothingGroups.java @@ -40,7 +40,6 @@ import java.util.List; import java.util.Map; import java.util.Queue; -import com.sun.javafx.geom.Vec3f; import javafx.scene.shape.TriangleMesh; /** Util for converting Normals to Smoothing Groups */ diff --git a/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/Vec3f.java b/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/Vec3f.java new file mode 100644 index 00000000..8de9acb9 --- /dev/null +++ b/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/Vec3f.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2014, Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.mihosoft.jcsg.ext.openjfx.importers; + +/** + * A 3-dimensional, single-precision, floating-point vector. + * + */ +public class Vec3f { + /** + * The x coordinate. + */ + public float x; + + /** + * The y coordinate. + */ + public float y; + + /** + * The z coordinate. + */ + public float z; + + public Vec3f() { } + + public Vec3f(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vec3f(Vec3f v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + } + + public void set(Vec3f v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + } + + public void set(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public final void mul(float s) { + this.x *= s; + this.y *= s; + this.z *= s; + } + + /** + * Sets the value of this vector to the difference + * of vectors t1 and t2 (this = t1 - t2). + * @param t1 the first vector + * @param t2 the second vector + */ + public void sub(Vec3f t1, Vec3f t2) { + this.x = t1.x - t2.x; + this.y = t1.y - t2.y; + this.z = t1.z - t2.z; + } + + /** + * Sets the value of this vector to the difference of + * itself and vector t1 (this = this - t1) . + * @param t1 the other vector + */ + public void sub(Vec3f t1) { + this.x -= t1.x; + this.y -= t1.y; + this.z -= t1.z; + } + + /** + * Sets the value of this vector to the sum + * of vectors t1 and t2 (this = t1 + t2). + * @param t1 the first vector + * @param t2 the second vector + */ + public void add(Vec3f t1, Vec3f t2) { + this.x = t1.x + t2.x; + this.y = t1.y + t2.y; + this.z = t1.z + t2.z; + } + + /** + * Sets the value of this vector to the sum of + * itself and vector t1 (this = this + t1) . + * @param t1 the other vector + */ + public void add(Vec3f t1) { + this.x += t1.x; + this.y += t1.y; + this.z += t1.z; + } + + /** + * Returns the length of this vector. + * @return the length of this vector + */ + public float length() { + return (float) Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z); + } + + /** + * Normalize this vector. + */ + public void normalize() { + float norm = 1.0f / length(); + this.x = this.x * norm; + this.y = this.y * norm; + this.z = this.z * norm; + } + + /** + * Sets this vector to be the vector cross product of vectors v1 and v2. + * @param v1 the first vector + * @param v2 the second vector + */ + public void cross(Vec3f v1, Vec3f v2) { + float tmpX; + float tmpY; + + tmpX = v1.y * v2.z - v1.z * v2.y; + tmpY = v2.x * v1.z - v2.z * v1.x; + this.z = v1.x * v2.y - v1.y * v2.x; + this.x = tmpX; + this.y = tmpY; + } + + /** + * Computes the dot product of this vector and vector v1. + * @param v1 the other vector + * @return the dot product of this vector and v1 + */ + public float dot(Vec3f v1) { + return this.x * v1.x + this.y * v1.y + this.z * v1.z; + } + + /** + * Returns the hashcode for this Vec3f. + * @return a hash code for this Vec3f. + */ + @Override + public int hashCode() { + int bits = 7; + bits = 31 * bits + Float.floatToIntBits(x); + bits = 31 * bits + Float.floatToIntBits(y); + bits = 31 * bits + Float.floatToIntBits(z); + return bits; + } + + /** + * Determines whether or not two 3D points or vectors are equal. + * Two instances of Vec3f are equal if the values of their + * x, y and z member fields, + * representing their position in the coordinate space, are the same. + * @param obj an object to be compared with this Vec3f + * @return true if the object to be compared is + * an instance of Vec3f and has + * the same values; false otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Vec3f) { + Vec3f v = (Vec3f) obj; + return (x == v.x) && (y == v.y) && (z == v.z); + } + return false; + } + + /** + * Returns a String that represents the value + * of this Vec3f. + * @return a string representation of this Vec3f. + */ + @Override + public String toString() { + return "Vec3f[" + x + ", " + y + ", " + z + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java new file mode 100644 index 00000000..f67b4e5f --- /dev/null +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -0,0 +1,736 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package eu.mihosoft.jcsg.playground; + +import eu.mihosoft.jcsg.Bounds; +import eu.mihosoft.jcsg.CSG; +import eu.mihosoft.jcsg.Cube; +import eu.mihosoft.jcsg.ObjFile; +import eu.mihosoft.jcsg.Polygon; +import eu.mihosoft.jcsg.STL; +import eu.mihosoft.jcsg.Sphere; +import eu.mihosoft.jcsg.Vertex; +import eu.mihosoft.vvecmath.ModifiableVector3d; +import eu.mihosoft.vvecmath.Plane; +import eu.mihosoft.vvecmath.Transform; +import eu.mihosoft.vvecmath.Vector3d; + +import javax.lang.model.type.IntersectionType; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Michael Hoffer (info@michaelhoffer.de) + */ +public class Main { + + public static final double EPS = 1e-8; + + public static void main(String[] args) throws IOException { + + testCut(); + + CSG c1 = new Cube(Vector3d.zero(), Vector3d.xyz(1, 1, 1)).toCSG(); + + CSG c2 = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG() + .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 78)); + +// Files.write(Paths.get("c1.stl"), c1.toStlString().getBytes()); +// Files.write(Paths.get("c2.stl"), c2.toStlString().getBytes()); +// c1 = STL.file(Paths.get("c1.stl")); +// c2 = STL.file(Paths.get("c2.stl")); +// c1 = new Sphere(Vector3d.x(0.), 0.5, 16, 16).toCSG(); +// c2 = new Sphere(Vector3d.x(0.6), 0.5, 16, 16).toCSG(); + c2 = new Sphere(Vector3d.x(0.0), 0.65, 16, 16).toCSG(); + List result1 = splitPolygons( + c1.getPolygons(), c2.getPolygons(), + c1.getBounds(), c2.getBounds() + ); + + List result2 = splitPolygons( + c2.getPolygons(), c1.getPolygons(), + c2.getBounds(), c1.getBounds() + ); + + /* result1 = splitPolygons( + result2, c2.getPolygons(), + c1.getBounds(), c2.getBounds());*/ + + List splitted = new ArrayList<>(); + splitted.addAll(result1); + splitted.addAll(result2); + +// CSG.fromPolygons(splitted).toObj(100).toFiles(Paths.get("test-split1.obj")); +// + Files.write(Paths.get("test-split1.stl"), + CSG.fromPolygons(splitted).toStlString().getBytes()); + List inC2 = new ArrayList<>(); + List outC2 = new ArrayList<>(); + List sameC2 = new ArrayList<>(); + List oppositeC2 = new ArrayList<>(); + + List unknownOfC1 = new ArrayList<>(); + + for (Polygon p : result2) { + PolygonType pT = classifyPolygon(p, c2.getPolygons(), c2.getBounds()); + + if (pT == PolygonType.INSIDE) { + inC2.add(p); + } + + if (pT == PolygonType.SAME) { + sameC2.add(p); + } + + if (pT == PolygonType.OPPOSITE) { + oppositeC2.add(p); + } + + if (pT == PolygonType.OUTSIDE) { + outC2.add(p); + } + + if (pT == PolygonType.UNKNOWN) { + unknownOfC1.add(p); + } + } + + List inC1 = new ArrayList<>(); + List outC1 = new ArrayList<>(); + List sameC1 = new ArrayList<>(); + List oppositeC1 = new ArrayList<>(); + + List unknownOfC2 = new ArrayList<>(); + + for (Polygon p : result1) { + PolygonType pT = classifyPolygon(p, c1.getPolygons(), c1.getBounds()); + + if (pT == PolygonType.INSIDE) { + inC1.add(p); + } + + if (pT == PolygonType.OUTSIDE) { + outC1.add(p); + } + + if (pT == PolygonType.SAME) { + sameC1.add(p); + } + + if (pT == PolygonType.OPPOSITE) { + oppositeC1.add(p); + } + + if (pT == PolygonType.UNKNOWN) { + unknownOfC2.add(p); + } + } + + List difference = new ArrayList<>(); + difference.addAll(outC2); + difference.addAll(oppositeC2); + for (Polygon p : inC1) { + p.flip(); + } + for (Polygon p : inC2) { + p.flip(); + } + + difference.addAll(inC1); + + System.err.println(">> creating CSG"); + + CSG result = CSG.fromPolygons(difference); + + System.err.println(">> unknown polygons in C1: " + unknownOfC1.size()); + System.err.println(">> unknown polygons in C2: " + unknownOfC2.size()); + System.err.println(">> opposite polygons in C1: " + oppositeC1.size()); + System.err.println(">> opposite polygons in C2: " + oppositeC2.size()); + System.err.println(">> inside polygons in C1: " + inC1.size()); + System.err.println(">> inside polygons in C2: " + inC2.size()); + + Files.write(Paths.get("test.stl"), result.toStlString().getBytes()); + + } + + public static PolygonType classifyPolygon(Polygon p1, List polygons, Bounds b) { + + double TOL = 1e-10; + + // we are definitely outside if bounding boxes don't intersect + if (!p1.getBounds().intersects(b)) { + return PolygonType.OUTSIDE; + } + + Vector3d rayCenter = p1.centroid(); + Vector3d rayDirection = p1.getPlane().getNormal(); + + List intersections = getPolygonsThatIntersectWithRay( + rayCenter, rayDirection, polygons, TOL); + + if (intersections.isEmpty()) { + return PolygonType.OUTSIDE; + } + + // find the closest polygon to the centroid of p1 which intersects the + // ray + RayIntersection min = null; //intersections.get(0); + double dist = 0; + double prevDist = Double.MAX_VALUE; // min.polygon.centroid().minus(rayCenter).magnitude(); + int i = 0; + for (RayIntersection ri : intersections) { + + int frontOrBack = p1.getPlane().compare(ri.intersectionPoint, TOL); + + if (frontOrBack < 0) { + // System.out.println(" -> skipping intersection behind ray " + i); + continue; + } + + //try { + // ObjFile objF = CSG.fromPolygons(ri.polygon).toObj(3); + // objF.toFiles(Paths.get("test-intersection-" + i + ".obj")); + //} catch (IOException ex) { + // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + //} + + dist = ri.polygon.centroid().minus(rayCenter).magnitude(); + + //System.out.println("dist-"+i+": " + dist); + + if (dist < TOL && ri.polygon.getPlane().getNormal().dot(rayDirection) < TOL) { + // System.out.println(" -> skipping intersection " + i); + continue; + } + + if (dist < prevDist) { + prevDist = dist; + min = ri; + } + + i++; + } + + if (min == null) { + return PolygonType.OUTSIDE; + } + + // try { + // ObjFile objF = CSG.fromPolygons(min.polygon).toObj(); + // objF.toFiles(Paths.get("test-intersection-min.obj")); + //} catch (IOException ex) { + // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + //} + + int frontOrBack = p1.getPlane().compare(min.intersectionPoint, TOL); + + Vector3d planePoint = p1.getPlane().getAnchor(); + + int sameOrOpposite = p1.getPlane().compare( + planePoint.plus(min.polygon.getPlane().getNormal()), TOL + ); + + if (frontOrBack > 0 && sameOrOpposite > 0) { + return PolygonType.INSIDE; + } + + if (frontOrBack > 0 && sameOrOpposite < 0) { + return PolygonType.OUTSIDE; + } + + if (frontOrBack < 0 && sameOrOpposite < 0) { + return PolygonType.INSIDE; + } + + if (frontOrBack < 0 && sameOrOpposite > 0) { + return PolygonType.OUTSIDE; + } + + if (frontOrBack == 0 && sameOrOpposite > 0) { + return PolygonType.SAME; + } + + if (frontOrBack == 0 && sameOrOpposite < 0) { + return PolygonType.OPPOSITE; + } + + System.err.println("I need help (2) !"); + + return PolygonType.UNKNOWN; + } + + public static final class PlaneIntersection { + + public final IntersectionType type; + public Optional point; + + public PlaneIntersection( + IntersectionType type, Optional point) { + this.type = type; + this.point = point; + } + + public static enum IntersectionType { + ON, + PARALLEL, + NON_PARALLEL + } + } + + public static final class RayIntersection { + + public final Vector3d intersectionPoint; + public final Polygon polygon; + public final PlaneIntersection.IntersectionType type; + + public RayIntersection(Vector3d intersectionPoint, + Polygon polygon, PlaneIntersection.IntersectionType type) { + this.intersectionPoint = intersectionPoint; + this.polygon = polygon; + this.type = type; + } + + + @Override + public String toString() { + return "" + + "[\n" + + " -> point: " + intersectionPoint + "\n" + + " -> polygon-normal: " + polygon.getPlane().getNormal() + "\n" + + " -> type: " + type + "\n" + + "]"; + } + + } + + public static List getPolygonsThatIntersectWithRay( + Vector3d point, Vector3d direction, List polygons, double TOL) { + List intersection = new ArrayList<>(); + for (Polygon p : polygons) { + PlaneIntersection res = computePlaneIntersection(p.getPlane(), point, direction, TOL); + if (res.point.isPresent()) { + if (p.contains(res.point.get())) { + intersection.add(new RayIntersection(res.point.get(), p, res.type)); + } + } + } + + return intersection; + } + + public static PlaneIntersection computePlaneIntersection( + Plane plane, Vector3d point, Vector3d direction, double TOL) { + + //Ax + By + Cz + D = 0 + //x = x0 + t(x1 x0) + //y = y0 + t(y1 y0) + //z = z0 + t(z1 z0) + //(x1 - x0) = dx, (y1 - y0) = dy, (z1 - z0) = dz + //t = -(A*x0 + B*y0 + C*z0 )/(A*dx + B*dy + C*dz) + Vector3d normal = plane.getNormal(); + Vector3d planePoint = plane.getAnchor(); + + double A = normal.x(); + double B = normal.y(); + double C = normal.z(); + double D = -(normal.x() * planePoint.x() + normal.y() * planePoint.y() + normal.z() * planePoint.z()); + + double numerator = A * point.x() + B * point.y() + C * point.z() + D; + double denominator = A * direction.x() + B * direction.y() + C * direction.z(); + + //if line is paralel to the plane... + if (Math.abs(denominator) < TOL) { + //if line is contained in the plane... + if (Math.abs(numerator) < TOL) { + return new PlaneIntersection( + PlaneIntersection.IntersectionType.ON, + Optional.of(point)); + } else { + return new PlaneIntersection( + PlaneIntersection.IntersectionType.PARALLEL, + Optional.empty()); + } + } //if line intercepts the plane... + else { + double t = -numerator / denominator; + Vector3d resultPoint = Vector3d.xyz( + point.x() + t * direction.x(), + point.y() + t * direction.y(), + point.z() + t * direction.z()); + + return new PlaneIntersection( + PlaneIntersection.IntersectionType.NON_PARALLEL, + Optional.of(resultPoint)); + } + } + + /** + * Splits polygons ps2 with planes from polygons ps1. + * + * @param ps1 + * @param ps2 + * @param b1 + * @param b2 + * @return + */ + public static List splitPolygons( + List ps1, + List ps2, + Bounds b1, Bounds b2) { + + System.out.println("#ps1: " + ps1.size() + ", #ps2: " + ps2.size()); + + if (ps1.isEmpty() || ps2.isEmpty()) return Collections.EMPTY_LIST; + + List ps2WithCuts = new ArrayList<>(ps2); + + for (Polygon p1 : ps1) { + + // return early if polygon bounds do not intersect object bounds + if (!p1.getBounds().intersects(b2)) { + continue; + } + + List cutsWithP1 = new ArrayList<>(); + List p2ToDelete = new ArrayList<>(); + for (Polygon p2 : ps2WithCuts) { + + // return early if polygon bounds do not intersect other polygon bound + if (!p1.getBounds().intersects(p2.getBounds())) { + continue; + } + + List cutsOfP2WithP1 = cutPolygonWithPlaneIf(p2, p1.getPlane(), + (Predicate>) segments -> { + + //if(true)return true; + if(segments.size()!=2) return true; + + Vector3d s1 = segments.get(0); + Vector3d s2 = segments.get(1); + + int numIntersectionsPoly1 = 0; + for(int i = 0; i< p1.vertices.size()-1;i++) { + //System.out.println("i,j : " + i + ", " + (i+1%p1.vertices.size())); + Vector3d e1 = p1.vertices.get(i).pos; + Vector3d e2 = p1.vertices.get(i+1%p1.vertices.size()).pos; + LineIntersectionResult iRes = calculateLineLineIntersection(e1,e2,s1,s2); + if(iRes.type == LineIntersectionResult.IntersectionType.INTERSECTING && + p1.contains(iRes.segmentPoint1.get())) { + numIntersectionsPoly1++; + } + } + + int numIntersectionsPoly2 = 0; + for(int i = 0; i< p2.vertices.size()-1;i++) { + Vector3d e1 = p2.vertices.get(i).pos; + Vector3d e2 = p2.vertices.get(i+1%p2.vertices.size()).pos; + LineIntersectionResult iRes = calculateLineLineIntersection(e1,e2,s1,s2); + if(iRes.type == LineIntersectionResult.IntersectionType.INTERSECTING && + p2.contains(iRes.segmentPoint1.get())) { + numIntersectionsPoly2++; + } + } + + return numIntersectionsPoly1 > 0 && numIntersectionsPoly2 > 0; + }); + + if (!cutsOfP2WithP1.isEmpty()) { + cutsWithP1.addAll(cutsOfP2WithP1); + p2ToDelete.add(p2); + } + } + ps2WithCuts.addAll(cutsWithP1); + ps2WithCuts.removeAll(p2ToDelete); + } + + return ps2WithCuts; + } + + + private static void cutPolygonWithPlaneAndTypes(Polygon polygon, Plane cutPlane, + int[] vertexTypes, List frontPolygon, + List backPolygon, List onPlane) { + +// System.out.println("polygon: \n" + polygon.toStlString()); +// System.out.println("--------------------"); +// System.out.println("plane: \n -> p: " + cutPlane.getAnchor() + "\n -> n: " + cutPlane.getNormal()); +// System.out.println("--------------------"); + for (int i = 0; i < polygon.vertices.size(); i++) { + int j = (i + 1) % polygon.vertices.size(); + int ti = vertexTypes[i]; + int tj = vertexTypes[j]; + Vertex vi = polygon.vertices.get(i); + Vertex vj = polygon.vertices.get(j); + if (ti == 1 /*front*/) { + frontPolygon.add(vi.pos); + } + if (ti == -1 /*back*/) { + backPolygon.add(vi.pos); + } + + if (ti == 0) { + frontPolygon.add(vi.pos); + backPolygon.add(vi.pos); +// segmentPoints.add(vi.pos); + } + + if (ti != tj && (ti != 0 && tj != 0)/*spanning*/) { + PlaneIntersection pI = computePlaneIntersection(cutPlane, vi.pos, vj.pos.minus(vi.pos), EPS); + + if (pI.type != PlaneIntersection.IntersectionType.NON_PARALLEL) { + throw new RuntimeException("I need help (3)!"); + } + + Vector3d intersectionPoint = pI.point.get(); + + frontPolygon.add(intersectionPoint); + backPolygon.add(intersectionPoint); + onPlane.add(intersectionPoint); + } + } + } + + public static void testCut() { + + Polygon p = Polygon.fromPoints( + Vector3d.xyz(0, 0, 0), + Vector3d.xyz(1, 0, 0), + Vector3d.xyz(1, 0, 1), + Vector3d.xyz(0, 0, 1) + ); + + try { + CSG pCSG = STL.file(Paths.get("sphere-test-01.stl")); + + p = pCSG.getPolygons().get(0); + } catch (Exception ex) { + ex.printStackTrace(); + } + + CSG cube = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG() + .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 17)); + + cube = new Sphere(Vector3d.x(0.), 0.5, 16, 16).toCSG(); + +// CSG cube = new Cube(1).toCSG().transformed( +// Transform.unity().translate(0.5,-0.55,0.5).rot(Vector3d.ZERO, Vector3d.UNITY, 0) +// ); + + int cubePolyFrom = 0; + int cubePolyTo = 6; + + List cubePolys = cube.getPolygons();//.subList(cubePolyFrom, cubePolyTo); + + System.out.println("p: " + p.toStlString()); + System.out.println("p-centroid: " + p.centroid()); + + List intersections = + getPolygonsThatIntersectWithRay( + p.centroid(), + p.getPlane().getNormal(), + cubePolys, EPS); + + System.out.println("my normal: " + p.getPlane().getNormal()); + + System.out.println("#intersections: " + intersections.size()); + for (RayIntersection ri : intersections) { + System.out.println(ri); + } + + PolygonType pType = classifyPolygon(p, cubePolys, cube.getBounds()); + + System.out.println("#pType:"); + System.out.println(" -> " + pType); + + List cutsWithCube = splitPolygons(cubePolys, + Arrays.asList(p), p.getBounds(), cube.getBounds()); + + cutsWithCube.addAll(cube.getPolygons()/*.subList(cubePolyFrom, cubePolyTo)*/); + + try { + ObjFile objF = CSG.fromPolygons(cutsWithCube).toObj(3); + objF.toFiles(Paths.get("test-split1.obj")); +// Files.write(Paths.get("test-split1.stl"), +// CSG.fromPolygons(cutsWithP1).toStlString().getBytes()); + } catch (IOException ex) { + Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + } + + ModifiableVector3d segmentP1 = Vector3d.zero().asModifiable(); + ModifiableVector3d segmentP2 = Vector3d.zero().asModifiable(); + LineIntersectionResult lineRes = calculateLineLineIntersection( + Vector3d.xyz(-1, 0, 0), Vector3d.xyz(1, 0, 0), + Vector3d.xyz(0, -1, 0), Vector3d.xyz(0, 1, 0)); + + System.out.println("l1 intersect l2: "); + System.out.println(lineRes); + + // System.exit(0); + } + + private static List cutPolygonWithPlaneIf(Polygon p, Plane plane, Predicate> check) { + + boolean typesEqual = true; + int types[] = new int[p.vertices.size()]; + for (int i = 0; i < p.vertices.size(); i++) { + types[i] = plane.compare(p.vertices.get(i).pos, EPS); +// System.out.println("type " + i + ": " + types[i]); + + if (i > 0 && typesEqual) { + typesEqual = typesEqual && (types[i] == types[i - 1]); + } + } + + // planes are parallel, thus polygons do not intersect + if (typesEqual) { + return Collections.EMPTY_LIST; + } + + List front = new ArrayList<>(); + List back = new ArrayList<>(); + List on = new ArrayList<>(); + cutPolygonWithPlaneAndTypes(p, plane, types, front, back, on); + + boolean checkResult = check == null; + + if (check != null) { + checkResult = check.test(on); + } + + if (!checkResult) return Collections.EMPTY_LIST; + + List cutsWithP1 = new ArrayList<>(); + if (front.size() > 2) { + Polygon frontCut = Polygon.fromPoints( + front); + if (frontCut.isValid()) { + cutsWithP1.add(frontCut); + } + } + if (back.size() > 2) { + Polygon backCut = Polygon.fromPoints( + back); + if (backCut.isValid()) { + cutsWithP1.add(backCut); + } + } + return cutsWithP1; + } + + enum PolygonType { + UNKNOWN, + INSIDE, + OUTSIDE, + OPPOSITE, + SAME + } + + + static class LineIntersectionResult { + + public final IntersectionType type; + + public final Optional segmentPoint1; + public final Optional segmentPoint2; + + LineIntersectionResult(IntersectionType type, Vector3d segmentPoint1, Vector3d segmentPoint2) { + this.type = type; + this.segmentPoint1 = Optional.ofNullable(segmentPoint1); + this.segmentPoint2 = Optional.ofNullable(segmentPoint2); + } + + static enum IntersectionType { + PARALLEL, + NON_PARALLEL, + INTERSECTING + } + + static final LineIntersectionResult PARALLEL = + new LineIntersectionResult(IntersectionType.PARALLEL, null, null); + + @Override + public String toString() { + return "[\n -> type: " + type + + "\n -> segmentP1: " + (segmentPoint1.isPresent() ? segmentPoint1.get() : "none") + + "\n -> segmentP2: " + (segmentPoint2.isPresent() ? segmentPoint2.get() : "none") + + "\n]"; + } + } + + /** + * Calculates the intersection line segment between two lines. + * + * @param line1Point1 + * @param line1Point2 + * @param line2Point1 + * @param line2Point2 + * @return {@code true} if the intersection line segment exists; {@code false} otherwise + */ + public static LineIntersectionResult calculateLineLineIntersection(Vector3d line1Point1, Vector3d line1Point2, + Vector3d line2Point1, Vector3d line2Point2) { + // Algorithm is ported from the C algorithm of + // Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/ + + Vector3d p1 = line1Point1; + Vector3d p2 = line1Point2; + Vector3d p3 = line2Point1; + Vector3d p4 = line2Point2; + Vector3d p13 = p1.minus(p3); + Vector3d p43 = p4.minus(p3); + + if (p43.magnitudeSq() < EPS) { + return LineIntersectionResult.PARALLEL; + } + Vector3d p21 = p2.minus(p1); + if (p21.magnitudeSq() < EPS) { + return LineIntersectionResult.PARALLEL; + } + + double d1343 = p13.x() * (double) p43.x() + (double) p13.y() * p43.y() + (double) p13.z() * p43.z(); + double d4321 = p43.x() * (double) p21.x() + (double) p43.y() * p21.y() + (double) p43.z() * p21.z(); + double d1321 = p13.x() * (double) p21.x() + (double) p13.y() * p21.y() + (double) p13.z() * p21.z(); + double d4343 = p43.x() * (double) p43.x() + (double) p43.y() * p43.y() + (double) p43.z() * p43.z(); + double d2121 = p21.x() * (double) p21.x() + (double) p21.y() * p21.y() + (double) p21.z() * p21.z(); + + double denom = d2121 * d4343 - d4321 * d4321; + if (Math.abs(denom) < EPS) { + return LineIntersectionResult.PARALLEL; + } + double numer = d1343 * d4321 - d1321 * d4343; + + double mua = numer / denom; + double mub = (d1343 + d4321 * (mua)) / d4343; + + ModifiableVector3d resultSegmentPoint1 = Vector3d.zero().asModifiable(); + ModifiableVector3d resultSegmentPoint2 = Vector3d.zero().asModifiable(); + + resultSegmentPoint1.setX(p1.x() + mua * p21.x()); + resultSegmentPoint1.setY(p1.y() + mua * p21.y()); + resultSegmentPoint1.setZ(p1.z() + mua * p21.z()); + resultSegmentPoint2.setX(p3.x() + mub * p43.x()); + resultSegmentPoint2.setY(p3.y() + mub * p43.y()); + resultSegmentPoint2.setZ(p3.z() + mub * p43.z()); + + if (resultSegmentPoint1.equals(resultSegmentPoint2)) { + return new LineIntersectionResult(LineIntersectionResult.IntersectionType.INTERSECTING, + resultSegmentPoint1, resultSegmentPoint2); + } else { + return new LineIntersectionResult(LineIntersectionResult.IntersectionType.NON_PARALLEL, + resultSegmentPoint1, resultSegmentPoint2); + } + } + +} diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main2.java b/src/main/java/eu/mihosoft/jcsg/playground/Main2.java new file mode 100644 index 00000000..a65958b6 --- /dev/null +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main2.java @@ -0,0 +1,723 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package eu.mihosoft.jcsg.playground; + +import eu.mihosoft.jcsg.*; +import eu.mihosoft.vvecmath.ModifiableVector3d; +import eu.mihosoft.vvecmath.Plane; +import eu.mihosoft.vvecmath.Transform; +import eu.mihosoft.vvecmath.Vector3d; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * @author Michael Hoffer (info@michaelhoffer.de) + */ +public class Main2 { + + public static final double EPS = 1e-8; + // s3 = s1 - s2 + + // # Step 1: + // + // Take all polygons of s2 that have vertices inside the bounding box b 1of s1 + // (polygons with only some vertices inside of b are cut along the planes of b1) + // All polygons outside of b1 are ignored from now on. + // + // Remark: collinear polygons are considered as being outside of b1. + // + // # Step 2: + // + // Cut all remaining polygons of s2 with the polygons of s1. Only keep the polygons with all vertices + // inside of b1. + // + // # Step 3: + // + // a) For each remaining polygon p of s2: cast an orthogonal ray from the center of p (normal) and + // count the number of intersecting polygons of s1. If the ray hits a vertex of p or cuts the + // boundary, it counts as being intersected by the ray. + // + // b) Classify the resulting polygon by whether the number of intersections is even or uneven number + // of intersections. An uneven number of intersections indicates that the polygon is inside of s1; + // An even number of intersections indicates that the polygon is outside of s2. + // + // + // # Step 5: + // + // Repeat these steps (1-4) with s1 and s2 reversed. + // + // # Step 6: + // + // s3 consists of all polygons of s1 classified as being outside of s2 and all polygons of s2 being + // classified as being inside of s1. + // + // _________ + // / /*\ \ + // | |*| | * = intersection + // \___\*/___/ + // + // _________ ____ + // / / \ \ / / + // | | | | === | | + // \___\ /___/ \___\ + public static void main(String[] args) throws IOException { + + CSG s1 = new Cube(2).toCSG(); + CSG s2 = new Sphere(Vector3d.x(0.0), 1.25, 32, 32).toCSG(); + + Files.write(Paths.get("diff-orig.stl"), s2.difference(s1).toStlString().getBytes()); + + Classification classification = classify(s1,s2); + + List polygons = new ArrayList<>(); + + polygons.addAll(classification.outsideS1); +// polygons.addAll(classification.outsideS2); +// polygons.addAll(classification.insideS1); + polygons.addAll(classification.insideS2); + + CSG difference = CSG.fromPolygons(polygons); + + Files.write(Paths.get("diff.stl"), difference.toStlString().getBytes()); + } + + static class Classification { + public List insideS1; + public List outsideS1; + public List insideS2; + public List outsideS2; + } + + static class Classification1 { + public List inside; + public List outside; + } + + public static Classification classify(CSG s1, CSG s2) { + Classification result = new Classification(); + + Classification1 r1 = classify1(s1,s2); + Classification1 r2 = classify1(s2,s1); + + result.insideS1 = r1.inside; + result.outsideS1 = r1.outside; + result.insideS2 = r2.inside; + result.outsideS2 = r2.outside; + + return result; + } + + public static Classification1 classify1(CSG s1, CSG s2) { + + // step 1 + + // get polygons inside of b + Bounds b1 = s1.getBounds(); + Bounds b2 = s2.getBounds(); + List ps1 = s1.getPolygons(); + List ps2 = s2.getPolygons();//.stream().filter(p->b1.intersects(p)).collect(Collectors.toList()); + + // step 2 + + // cut polygons + ps2 = splitPolygons(ps1, ps2, b1, b2);//.stream().filter(p->p.vertices.stream().filter(v->b1.contains(v)).count()==p.vertices.size()).collect(Collectors.toList()); + + // step 3 + + double TOL = 1e-10; + + Map> polygons = ps2.parallelStream().collect(Collectors.partitioningBy(p-> { + return classifyPolygon(p, ps1, b1) == PolygonType.OUTSIDE; + })); + + List inside = polygons.get(false); + List outside = polygons.get(true); + + Classification1 result = new Classification1(); + result.inside = inside; + result.outside = outside; + + return result; + } + + + + public static PolygonType classifyPolygon(Polygon p1, List polygons, Bounds b) { + + double TOL = 1e-10; + + // we are definitely outside if bounding boxes don't intersect + if (!p1.getBounds().intersects(b)) { + return PolygonType.OUTSIDE; + } + + Vector3d rayCenter = p1.centroid(); + Vector3d rayDirection = p1.getPlane().getNormal(); + + List intersections = getPolygonsThatIntersectWithRay( + rayCenter, rayDirection, polygons, TOL); + + if (intersections.isEmpty()) { + return PolygonType.OUTSIDE; + } + + // find the closest polygon to the centroid of p1 which intersects the + // ray + RayIntersection min = null; //intersections.get(0); + double dist = 0; + double prevDist = Double.MAX_VALUE; // min.polygon.centroid().minus(rayCenter).magnitude(); + int i = 0; + for (RayIntersection ri : intersections) { + + int frontOrBack = p1.getPlane().compare(ri.intersectionPoint, TOL); + + if (frontOrBack < 0) { + // System.out.println(" -> skipping intersection behind ray " + i); + continue; + } + + //try { + // ObjFile objF = CSG.fromPolygons(ri.polygon).toObj(3); + // objF.toFiles(Paths.get("test-intersection-" + i + ".obj")); + //} catch (IOException ex) { + // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + //} + + dist = ri.polygon.centroid().minus(rayCenter).magnitude(); + + //System.out.println("dist-"+i+": " + dist); + + if (dist < TOL && ri.polygon.getPlane().getNormal().dot(rayDirection) < TOL) { + // System.out.println(" -> skipping intersection " + i); + continue; + } + + if (dist < prevDist) { + prevDist = dist; + min = ri; + } + + i++; + } + + if (min == null) { + return PolygonType.OUTSIDE; + } + + // try { + // ObjFile objF = CSG.fromPolygons(min.polygon).toObj(); + // objF.toFiles(Paths.get("test-intersection-min.obj")); + //} catch (IOException ex) { + // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + //} + + int frontOrBack = p1.getPlane().compare(min.intersectionPoint, TOL); + + Vector3d planePoint = p1.getPlane().getAnchor(); + + int sameOrOpposite = p1.getPlane().compare( + planePoint.plus(min.polygon.getPlane().getNormal()), TOL + ); + + if (frontOrBack > 0 && sameOrOpposite > 0) { + return PolygonType.INSIDE; + } + + if (frontOrBack > 0 && sameOrOpposite < 0) { + return PolygonType.OUTSIDE; + } + + if (frontOrBack < 0 && sameOrOpposite < 0) { + return PolygonType.INSIDE; + } + + if (frontOrBack < 0 && sameOrOpposite > 0) { + return PolygonType.OUTSIDE; + } + + if (frontOrBack == 0 && sameOrOpposite > 0) { + return PolygonType.SAME; + } + + if (frontOrBack == 0 && sameOrOpposite < 0) { + return PolygonType.OPPOSITE; + } + + System.err.println("I need help (2) !"); + + return PolygonType.UNKNOWN; + } + + public static final class PlaneIntersection { + + public final IntersectionType type; + public Optional point; + + public PlaneIntersection( + IntersectionType type, Optional point) { + this.type = type; + this.point = point; + } + + public static enum IntersectionType { + ON, + PARALLEL, + NON_PARALLEL + } + } + + public static final class RayIntersection { + + public final Vector3d intersectionPoint; + public final Polygon polygon; + public final PlaneIntersection.IntersectionType type; + + public RayIntersection(Vector3d intersectionPoint, + Polygon polygon, PlaneIntersection.IntersectionType type) { + this.intersectionPoint = intersectionPoint; + this.polygon = polygon; + this.type = type; + } + + + @Override + public String toString() { + return "" + + "[\n" + + " -> point: " + intersectionPoint + "\n" + + " -> polygon-normal: " + polygon.getPlane().getNormal() + "\n" + + " -> type: " + type + "\n" + + "]"; + } + + } + + public static List getPolygonsThatIntersectWithRay( + Vector3d point, Vector3d direction, List polygons, double TOL) { + List intersection = new ArrayList<>(); + for (Polygon p : polygons) { + PlaneIntersection res = computePlaneIntersection(p.getPlane(), point, direction, TOL); + if (res.point.isPresent()) { + if (p.contains(res.point.get())) { + intersection.add(new RayIntersection(res.point.get(), p, res.type)); + } + } + } + + return intersection; + } + + public static PlaneIntersection computePlaneIntersection( + Plane plane, Vector3d point, Vector3d direction, double TOL) { + + //Ax + By + Cz + D = 0 + //x = x0 + t(x1 x0) + //y = y0 + t(y1 y0) + //z = z0 + t(z1 z0) + //(x1 - x0) = dx, (y1 - y0) = dy, (z1 - z0) = dz + //t = -(A*x0 + B*y0 + C*z0 )/(A*dx + B*dy + C*dz) + Vector3d normal = plane.getNormal(); + Vector3d planePoint = plane.getAnchor(); + + double A = normal.x(); + double B = normal.y(); + double C = normal.z(); + double D = -(normal.x() * planePoint.x() + normal.y() * planePoint.y() + normal.z() * planePoint.z()); + + double numerator = A * point.x() + B * point.y() + C * point.z() + D; + double denominator = A * direction.x() + B * direction.y() + C * direction.z(); + + //if line is parallel to the plane... + if (Math.abs(denominator) < TOL) { + //if line is contained in the plane... + if (Math.abs(numerator) < TOL) { + return new PlaneIntersection( + PlaneIntersection.IntersectionType.ON, + Optional.of(point)); + } else { + return new PlaneIntersection( + PlaneIntersection.IntersectionType.PARALLEL, + Optional.empty()); + } + } //if line intercepts the plane... + else { + double t = -numerator / denominator; + Vector3d resultPoint = Vector3d.xyz( + point.x() + t * direction.x(), + point.y() + t * direction.y(), + point.z() + t * direction.z()); + + return new PlaneIntersection( + PlaneIntersection.IntersectionType.NON_PARALLEL, + Optional.of(resultPoint)); + } + } + + /** + * Splits polygons ps2 with planes from polygons ps1. + * + * @param ps1 + * @param ps2 + * @param b1 + * @param b2 + * @return + */ + public static List splitPolygons( + List ps1, + List ps2, + Bounds b1, Bounds b2) { + + System.out.println("#ps1: " + ps1.size() + ", #ps2: " + ps2.size()); + + if (ps1.isEmpty() || ps2.isEmpty()) return Collections.EMPTY_LIST; + + List ps2WithCuts = new ArrayList<>(ps2); + + for (Polygon p1 : ps1) { + + // return early if polygon bounds do not intersect object bounds + if (!p1.getBounds().intersects(b2)) { + continue; + } + + List cutsWithP1 = new ArrayList<>(); + List p2ToDelete = new ArrayList<>(); + for (Polygon p2 : ps2WithCuts) { + + // return early if polygon bounds do not intersect other polygon bound + if (!p1.getBounds().intersects(p2.getBounds())) { + continue; + } + + List cutsOfP2WithP1 = cutPolygonWithPlaneIf(p2, p1.getPlane(), + (Predicate>) segments -> { + + //if(true)return true; + if(segments.size()!=2) return true; + + Vector3d s1 = segments.get(0); + Vector3d s2 = segments.get(1); + + int numIntersectionsPoly1 = 0; + for(int i = 0; i< p1.vertices.size()-1;i++) { + //System.out.println("i,j : " + i + ", " + (i+1%p1.vertices.size())); + Vector3d e1 = p1.vertices.get(i).pos; + Vector3d e2 = p1.vertices.get(i+1%p1.vertices.size()).pos; + LineIntersectionResult iRes = calculateLineLineIntersection(e1,e2,s1,s2); + if(iRes.type == LineIntersectionResult.IntersectionType.INTERSECTING && + p1.contains(iRes.segmentPoint1.get())) { + numIntersectionsPoly1++; + } + } + + int numIntersectionsPoly2 = 0; + for(int i = 0; i< p2.vertices.size()-1;i++) { + Vector3d e1 = p2.vertices.get(i).pos; + Vector3d e2 = p2.vertices.get(i+1%p2.vertices.size()).pos; + LineIntersectionResult iRes = calculateLineLineIntersection(e1,e2,s1,s2); + if(iRes.type == LineIntersectionResult.IntersectionType.INTERSECTING && + p2.contains(iRes.segmentPoint1.get())) { + numIntersectionsPoly2++; + } + } + + return numIntersectionsPoly1 > 0 && numIntersectionsPoly2 > 0; + }); + + if (!cutsOfP2WithP1.isEmpty()) { + cutsWithP1.addAll(cutsOfP2WithP1); + p2ToDelete.add(p2); + } + } + ps2WithCuts.addAll(cutsWithP1); + ps2WithCuts.removeAll(p2ToDelete); + } + + return ps2WithCuts; + } + + + private static void cutPolygonWithPlaneAndTypes(Polygon polygon, Plane cutPlane, + int[] vertexTypes, List frontPolygon, + List backPolygon, List onPlane) { + +// System.out.println("polygon: \n" + polygon.toStlString()); +// System.out.println("--------------------"); +// System.out.println("plane: \n -> p: " + cutPlane.getAnchor() + "\n -> n: " + cutPlane.getNormal()); +// System.out.println("--------------------"); + for (int i = 0; i < polygon.vertices.size(); i++) { + int j = (i + 1) % polygon.vertices.size(); + int ti = vertexTypes[i]; + int tj = vertexTypes[j]; + Vertex vi = polygon.vertices.get(i); + Vertex vj = polygon.vertices.get(j); + if (ti == 1 /*front*/) { + frontPolygon.add(vi.pos); + } + if (ti == -1 /*back*/) { + backPolygon.add(vi.pos); + } + + if (ti == 0) { + frontPolygon.add(vi.pos); + backPolygon.add(vi.pos); +// segmentPoints.add(vi.pos); + } + + if (ti != tj && (ti != 0 && tj != 0)/*spanning*/) { + PlaneIntersection pI = computePlaneIntersection(cutPlane, vi.pos, vj.pos.minus(vi.pos), EPS); + + if (pI.type != PlaneIntersection.IntersectionType.NON_PARALLEL) { + throw new RuntimeException("I need help (3)!"); + } + + Vector3d intersectionPoint = pI.point.get(); + + frontPolygon.add(intersectionPoint); + backPolygon.add(intersectionPoint); + onPlane.add(intersectionPoint); + } + } + } + + public static void testCut() { + + Polygon p = Polygon.fromPoints( + Vector3d.xyz(0, 0, 0), + Vector3d.xyz(1, 0, 0), + Vector3d.xyz(1, 0, 1), + Vector3d.xyz(0, 0, 1) + ); + + try { + CSG pCSG = STL.file(Paths.get("sphere-test-01.stl")); + + p = pCSG.getPolygons().get(0); + } catch (Exception ex) { + ex.printStackTrace(); + } + + CSG cube = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG() + .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 17)); + + cube = new Sphere(Vector3d.x(0.), 0.5, 16, 16).toCSG(); + +// CSG cube = new Cube(1).toCSG().transformed( +// Transform.unity().translate(0.5,-0.55,0.5).rot(Vector3d.ZERO, Vector3d.UNITY, 0) +// ); + + int cubePolyFrom = 0; + int cubePolyTo = 6; + + List cubePolys = cube.getPolygons();//.subList(cubePolyFrom, cubePolyTo); + + System.out.println("p: " + p.toStlString()); + System.out.println("p-centroid: " + p.centroid()); + + List intersections = + getPolygonsThatIntersectWithRay( + p.centroid(), + p.getPlane().getNormal(), + cubePolys, EPS); + + System.out.println("my normal: " + p.getPlane().getNormal()); + + System.out.println("#intersections: " + intersections.size()); + for (RayIntersection ri : intersections) { + System.out.println(ri); + } + + PolygonType pType = classifyPolygon(p, cubePolys, cube.getBounds()); + + System.out.println("#pType:"); + System.out.println(" -> " + pType); + + List cutsWithCube = splitPolygons(cubePolys, + Arrays.asList(p), p.getBounds(), cube.getBounds()); + + cutsWithCube.addAll(cube.getPolygons()/*.subList(cubePolyFrom, cubePolyTo)*/); + + try { + ObjFile objF = CSG.fromPolygons(cutsWithCube).toObj(3); + objF.toFiles(Paths.get("test-split1.obj")); +// Files.write(Paths.get("test-split1.stl"), +// CSG.fromPolygons(cutsWithP1).toStlString().getBytes()); + } catch (IOException ex) { + Logger.getLogger(Main2.class.getName()).log(Level.SEVERE, null, ex); + } + + ModifiableVector3d segmentP1 = Vector3d.zero().asModifiable(); + ModifiableVector3d segmentP2 = Vector3d.zero().asModifiable(); + LineIntersectionResult lineRes = calculateLineLineIntersection( + Vector3d.xyz(-1, 0, 0), Vector3d.xyz(1, 0, 0), + Vector3d.xyz(0, -1, 0), Vector3d.xyz(0, 1, 0)); + + System.out.println("l1 intersect l2: "); + System.out.println(lineRes); + + // System.exit(0); + } + + private static List cutPolygonWithPlaneIf(Polygon p, Plane plane, Predicate> check) { + + boolean typesEqual = true; + int types[] = new int[p.vertices.size()]; + for (int i = 0; i < p.vertices.size(); i++) { + types[i] = plane.compare(p.vertices.get(i).pos, EPS); +// System.out.println("type " + i + ": " + types[i]); + + if (i > 0 && typesEqual) { + typesEqual = typesEqual && (types[i] == types[i - 1]); + } + } + + // planes are parallel, thus polygons do not intersect + if (typesEqual) { + return Collections.EMPTY_LIST; + } + + List front = new ArrayList<>(); + List back = new ArrayList<>(); + List on = new ArrayList<>(); + cutPolygonWithPlaneAndTypes(p, plane, types, front, back, on); + + boolean checkResult = check == null; + + if (check != null) { + checkResult = check.test(on); + } + + if (!checkResult) return Collections.EMPTY_LIST; + + List cutsWithP1 = new ArrayList<>(); + if (front.size() > 2) { + Polygon frontCut = Polygon.fromPoints( + front); + if (frontCut.isValid()) { + cutsWithP1.add(frontCut); + } + } + if (back.size() > 2) { + Polygon backCut = Polygon.fromPoints( + back); + if (backCut.isValid()) { + cutsWithP1.add(backCut); + } + } + return cutsWithP1; + } + + enum PolygonType { + UNKNOWN, + INSIDE, + OUTSIDE, + OPPOSITE, + SAME + } + + + static class LineIntersectionResult { + + public final IntersectionType type; + + public final Optional segmentPoint1; + public final Optional segmentPoint2; + + LineIntersectionResult(IntersectionType type, Vector3d segmentPoint1, Vector3d segmentPoint2) { + this.type = type; + this.segmentPoint1 = Optional.ofNullable(segmentPoint1); + this.segmentPoint2 = Optional.ofNullable(segmentPoint2); + } + + static enum IntersectionType { + PARALLEL, + NON_PARALLEL, + INTERSECTING + } + + static final LineIntersectionResult PARALLEL = + new LineIntersectionResult(IntersectionType.PARALLEL, null, null); + + @Override + public String toString() { + return "[\n -> type: " + type + + "\n -> segmentP1: " + (segmentPoint1.isPresent() ? segmentPoint1.get() : "none") + + "\n -> segmentP2: " + (segmentPoint2.isPresent() ? segmentPoint2.get() : "none") + + "\n]"; + } + } + + /** + * Calculates the intersection line segment between two lines. + * + * @param line1Point1 + * @param line1Point2 + * @param line2Point1 + * @param line2Point2 + * @return {@code true} if the intersection line segment exists; {@code false} otherwise + */ + public static LineIntersectionResult calculateLineLineIntersection(Vector3d line1Point1, Vector3d line1Point2, + Vector3d line2Point1, Vector3d line2Point2) { + // Algorithm is ported from the C algorithm of + // Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/ + + Vector3d p1 = line1Point1; + Vector3d p2 = line1Point2; + Vector3d p3 = line2Point1; + Vector3d p4 = line2Point2; + Vector3d p13 = p1.minus(p3); + Vector3d p43 = p4.minus(p3); + + if (p43.magnitudeSq() < EPS) { + return LineIntersectionResult.PARALLEL; + } + Vector3d p21 = p2.minus(p1); + if (p21.magnitudeSq() < EPS) { + return LineIntersectionResult.PARALLEL; + } + + double d1343 = p13.x() * (double) p43.x() + (double) p13.y() * p43.y() + (double) p13.z() * p43.z(); + double d4321 = p43.x() * (double) p21.x() + (double) p43.y() * p21.y() + (double) p43.z() * p21.z(); + double d1321 = p13.x() * (double) p21.x() + (double) p13.y() * p21.y() + (double) p13.z() * p21.z(); + double d4343 = p43.x() * (double) p43.x() + (double) p43.y() * p43.y() + (double) p43.z() * p43.z(); + double d2121 = p21.x() * (double) p21.x() + (double) p21.y() * p21.y() + (double) p21.z() * p21.z(); + + double denom = d2121 * d4343 - d4321 * d4321; + if (Math.abs(denom) < EPS) { + return LineIntersectionResult.PARALLEL; + } + double numer = d1343 * d4321 - d1321 * d4343; + + double mua = numer / denom; + double mub = (d1343 + d4321 * (mua)) / d4343; + + ModifiableVector3d resultSegmentPoint1 = Vector3d.zero().asModifiable(); + ModifiableVector3d resultSegmentPoint2 = Vector3d.zero().asModifiable(); + + resultSegmentPoint1.setX(p1.x() + mua * p21.x()); + resultSegmentPoint1.setY(p1.y() + mua * p21.y()); + resultSegmentPoint1.setZ(p1.z() + mua * p21.z()); + resultSegmentPoint2.setX(p3.x() + mub * p43.x()); + resultSegmentPoint2.setY(p3.y() + mub * p43.y()); + resultSegmentPoint2.setZ(p3.z() + mub * p43.z()); + + if (resultSegmentPoint1.equals(resultSegmentPoint2)) { + return new LineIntersectionResult(LineIntersectionResult.IntersectionType.INTERSECTING, + resultSegmentPoint1, resultSegmentPoint2); + } else { + return new LineIntersectionResult(LineIntersectionResult.IntersectionType.NON_PARALLEL, + resultSegmentPoint1, resultSegmentPoint2); + } + } + +} diff --git a/src/main/java/eu/mihosoft/jcsg/samples/FractalStructure.java b/src/main/java/eu/mihosoft/jcsg/samples/FractalStructure.java index 6191837c..21fb9d28 100644 --- a/src/main/java/eu/mihosoft/jcsg/samples/FractalStructure.java +++ b/src/main/java/eu/mihosoft/jcsg/samples/FractalStructure.java @@ -148,10 +148,10 @@ public FractalStructure(Vector3d groundCenter, Vector3d topCenter, && Math.abs(orthoVecToRotAxis2.dot(rotationAxis)) < orthoThreshhold) { this.orthoVecToRotAxis2 = orthoVecToRotAxis2.normalized(); } else { - this.orthoVecToRotAxis2 = rotationAxis.cross(this.orthoVecToRotAxis1).normalized(); + this.orthoVecToRotAxis2 = rotationAxis.crossed(this.orthoVecToRotAxis1).normalized(); } } else { - this.orthoVecToRotAxis2 = rotationAxis.cross(this.orthoVecToRotAxis1).normalized(); + this.orthoVecToRotAxis2 = rotationAxis.crossed(this.orthoVecToRotAxis1).normalized(); } // x, y, z @@ -445,7 +445,7 @@ private ArrayList createSubStructures() { helpCenterPoint = connectionLineVectorNormalized.times(j).plus(connectionLineVectorNormalized.times(stepSizeOnConnectionLineHalf)).plus(centerGroundPoint).plus(correctionInRotationAxisDirection); if (secondOrthoVec == null) { - secondOrthoVec = connectionLineVectorNormalized.cross(helpCenterPoint.minus(helpEdgePoint)); + secondOrthoVec = connectionLineVectorNormalized.crossed(helpCenterPoint.minus(helpEdgePoint)); } // prevent that the last cross connactions from bottom left to top right has a to above end point in the top plane diff --git a/src/test/java/eu/mihosoft/jcsg/PolygonFlipTest.java b/src/test/java/eu/mihosoft/jcsg/PolygonFlipTest.java new file mode 100644 index 00000000..67d83838 --- /dev/null +++ b/src/test/java/eu/mihosoft/jcsg/PolygonFlipTest.java @@ -0,0 +1,28 @@ +package eu.mihosoft.jcsg; + +import eu.mihosoft.vvecmath.Vector3d; +import org.junit.Assert; +import org.junit.Test; + +public class PolygonFlipTest { + + private static final double EPSILON = 1e-8; + + @Test + public void flipPolygonTest() { + Polygon polygon = Polygon.fromPoints( + Vector3d.xy(1, 1), + Vector3d.xy(2, 1), + Vector3d.xy(1, 2) + ); + assertEquals(Vector3d.z(1), polygon.getPlane().getNormal()); + polygon.flip(); + assertEquals(Vector3d.z(-1), polygon.getPlane().getNormal()); + } + + private void assertEquals(final Vector3d expected, final Vector3d actual) { + Assert.assertEquals(expected.getX(), actual.getX(), EPSILON); + Assert.assertEquals(expected.getY(), actual.getY(), EPSILON); + Assert.assertEquals(expected.getZ(), actual.getZ(), EPSILON); + } +} diff --git a/src/test/java/eu/mihosoft/jcsg/VolumeTest.java b/src/test/java/eu/mihosoft/jcsg/VolumeTest.java new file mode 100644 index 00000000..c1343d1e --- /dev/null +++ b/src/test/java/eu/mihosoft/jcsg/VolumeTest.java @@ -0,0 +1,61 @@ +package eu.mihosoft.jcsg; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.nio.file.Paths; + +public class VolumeTest { + + @Test + public void vlumeTest() { + + { + // volume of empty CSG object is 0 + double emptyVolume = CSG.fromPolygons(new Polygon[0]).computeVolume(); + assertEquals(emptyVolume, 0, 1e-16); + } + + { + // volume of unit cube is 1 unit^3 + double volumeUnitCube = new Cube(1.0).toCSG().computeVolume(); + assertEquals(1.0, volumeUnitCube, 1e-16); + } + + { + // volume of cube is w*h*d unit^3 + double w = 30.65; + double h = 24.17; + double d = 75.3; + double volumeBox = new Cube(w,h,d).toCSG().computeVolume(); + assertEquals(w*h*d, volumeBox, 1e-16); + } + + { + // volume of sphere is (4*PI*r^3)/3.0 unit^3 + double r = 3.4; + + // bad approximation + double volumeSphere1 = new Sphere(r, 32,16).toCSG().computeVolume(); + assertEquals((4.0*Math.PI*r*r*r)/3.0, volumeSphere1, 10.0); + + // better approximation + double volumeSphere2 = new Sphere(r, 1024, 512).toCSG().computeVolume(); + assertEquals((4.0*Math.PI*r*r*r)/3.0, volumeSphere2, 1e-2); + } + + { + // volume of cylinder is PI*r^2*h unit^3 + double r = 5.9; + double h = 2.1; + + // bad approximation + double volumeCylinder1 = new Cylinder(r, h, 16).toCSG().computeVolume(); + assertEquals(Math.PI*r*r*h, volumeCylinder1, 10); + + // better approximation + double volumeCylinder2 = new Cylinder(r, h, 1024).toCSG().computeVolume(); + assertEquals(Math.PI*r*r*h, volumeCylinder2, 1e-2); + } + + } +} \ No newline at end of file