diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 1721770f..582eb55f 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -42,9 +42,6 @@ jobs: - name: Pull a JavaFX JDK run: wget https://cdn.azul.com/zulu/bin/zulu21.46.19-ca-fx-jdk21.0.9-linux_aarch64.tar.gz - - name: After JDK download, list directory contents - run: pwd; ls -la - - name: Set Java uses: actions/setup-java@v1 with: @@ -119,9 +116,6 @@ jobs: - name: Pull a JavaFX JDK run: wget https://cdn.azul.com/zulu/bin/zulu21.46.19-ca-fx-jdk21.0.9-linux_x64.tar.gz - - name: After JDK download, list directory contnts - run: pwd; ls -la - - name: Set Java uses: actions/setup-java@v1 with: diff --git a/.gitignore b/.gitignore index 2a556fdc..b48f839c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ html /hs_err_*.log /vows.svg.png /flame.svg.png +/Manifold-SVGExportTest.svg diff --git a/box.svg b/box.svg index cbd7f4bd..58c19fa8 100644 --- a/box.svg +++ b/box.svg @@ -1,85 +1,85 @@ - - - -ABox - - -Box - ABox -2024-10-27 21:50:41 -https://boxes.hackerspace-bamberg.de/ABox?FingerJoint_style=rectangular&FingerJoint_surroundingspaces=2.0&FingerJoint_bottom_lip=0.0&FingerJoint_edge_width=1.0&FingerJoint_extra_length=0.0&FingerJoint_finger=2.0&FingerJoint_play=0.0&FingerJoint_space=2.0&FingerJoint_width=1.0&Lid_handle=none&Lid_style=none&Lid_handle_height=8.0&Lid_height=4.0&Lid_play=0.1&x=100.0&y=100.0&h=100.0&outside=0&outside=1&bottom_edge=h&thickness=3.0&format=svg&tabs=0.0&qr_code=0&debug=0&labels=0&labels=1&reference=100.0&inner_corners=loop&burn=0.1&language=en&render=1 -https://boxes.hackerspace-bamberg.de/ABox -A simple Box - -This box is kept simple on purpose. If you need more features have a look at the UniversalBox. - -Created with Boxes.py (https://boxes.hackerspace-bamberg.de/) -Command line: boxes ABox --FingerJoint_style=rectangular --FingerJoint_surroundingspaces=2.0 --FingerJoint_bottom_lip=0.0 --FingerJoint_edge_width=1.0 --FingerJoint_extra_length=0.0 --FingerJoint_finger=2.0 --FingerJoint_play=0.0 --FingerJoint_space=2.0 --FingerJoint_width=1.0 --Lid_handle=none --Lid_style=none --Lid_handle_height=8.0 --Lid_height=4.0 --Lid_play=0.1 --x=100.0 --y=100.0 --h=100.0 --outside=0 --outside=1 --bottom_edge=h --thickness=3.0 --format=svg --tabs=0.0 --qr_code=0 --debug=0 --labels=0 --labels=1 --reference=100.0 --inner_corners=loop --burn=0.1 -Command line short: boxes ABox -Url: https://boxes.hackerspace-bamberg.de/ABox?FingerJoint_style=rectangular&FingerJoint_surroundingspaces=2.0&FingerJoint_bottom_lip=0.0&FingerJoint_edge_width=1.0&FingerJoint_extra_length=0.0&FingerJoint_finger=2.0&FingerJoint_play=0.0&FingerJoint_space=2.0&FingerJoint_width=1.0&Lid_handle=none&Lid_style=none&Lid_handle_height=8.0&Lid_height=4.0&Lid_play=0.1&x=100.0&y=100.0&h=100.0&outside=0&outside=1&bottom_edge=h&thickness=3.0&format=svg&tabs=0.0&qr_code=0&debug=0&labels=0&labels=1&reference=100.0&inner_corners=loop&burn=0.1&language=en&render=1 -Url short: https://boxes.hackerspace-bamberg.de/ABox -SettingsUrl: https://boxes.hackerspace-bamberg.de/ABox?FingerJoint_style=rectangular&FingerJoint_surroundingspaces=2.0&FingerJoint_bottom_lip=0.0&FingerJoint_edge_width=1.0&FingerJoint_extra_length=0.0&FingerJoint_finger=2.0&FingerJoint_play=0.0&FingerJoint_space=2.0&FingerJoint_width=1.0&Lid_handle=none&Lid_style=none&Lid_handle_height=8.0&Lid_height=4.0&Lid_play=0.1&x=100.0&y=100.0&h=100.0&outside=0&outside=1&bottom_edge=h&thickness=3.0&format=svg&tabs=0.0&qr_code=0&debug=0&labels=0&labels=1&reference=100.0&inner_corners=loop&burn=0.1&language=en -SettingsUrl short: https://boxes.hackerspace-bamberg.de/ABox - - - - - 100.0mm, burn:0.10mm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +ABox + + +Box - ABox +2024-10-27 21:50:41 +https://boxes.hackerspace-bamberg.de/ABox?FingerJoint_style=rectangular&FingerJoint_surroundingspaces=2.0&FingerJoint_bottom_lip=0.0&FingerJoint_edge_width=1.0&FingerJoint_extra_length=0.0&FingerJoint_finger=2.0&FingerJoint_play=0.0&FingerJoint_space=2.0&FingerJoint_width=1.0&Lid_handle=none&Lid_style=none&Lid_handle_height=8.0&Lid_height=4.0&Lid_play=0.1&x=100.0&y=100.0&h=100.0&outside=0&outside=1&bottom_edge=h&thickness=3.0&format=svg&tabs=0.0&qr_code=0&debug=0&labels=0&labels=1&reference=100.0&inner_corners=loop&burn=0.1&language=en&render=1 +https://boxes.hackerspace-bamberg.de/ABox +A simple Box + +This box is kept simple on purpose. If you need more features have a look at the UniversalBox. + +Created with Boxes.py (https://boxes.hackerspace-bamberg.de/) +Command line: boxes ABox --FingerJoint_style=rectangular --FingerJoint_surroundingspaces=2.0 --FingerJoint_bottom_lip=0.0 --FingerJoint_edge_width=1.0 --FingerJoint_extra_length=0.0 --FingerJoint_finger=2.0 --FingerJoint_play=0.0 --FingerJoint_space=2.0 --FingerJoint_width=1.0 --Lid_handle=none --Lid_style=none --Lid_handle_height=8.0 --Lid_height=4.0 --Lid_play=0.1 --x=100.0 --y=100.0 --h=100.0 --outside=0 --outside=1 --bottom_edge=h --thickness=3.0 --format=svg --tabs=0.0 --qr_code=0 --debug=0 --labels=0 --labels=1 --reference=100.0 --inner_corners=loop --burn=0.1 +Command line short: boxes ABox +Url: https://boxes.hackerspace-bamberg.de/ABox?FingerJoint_style=rectangular&FingerJoint_surroundingspaces=2.0&FingerJoint_bottom_lip=0.0&FingerJoint_edge_width=1.0&FingerJoint_extra_length=0.0&FingerJoint_finger=2.0&FingerJoint_play=0.0&FingerJoint_space=2.0&FingerJoint_width=1.0&Lid_handle=none&Lid_style=none&Lid_handle_height=8.0&Lid_height=4.0&Lid_play=0.1&x=100.0&y=100.0&h=100.0&outside=0&outside=1&bottom_edge=h&thickness=3.0&format=svg&tabs=0.0&qr_code=0&debug=0&labels=0&labels=1&reference=100.0&inner_corners=loop&burn=0.1&language=en&render=1 +Url short: https://boxes.hackerspace-bamberg.de/ABox +SettingsUrl: https://boxes.hackerspace-bamberg.de/ABox?FingerJoint_style=rectangular&FingerJoint_surroundingspaces=2.0&FingerJoint_bottom_lip=0.0&FingerJoint_edge_width=1.0&FingerJoint_extra_length=0.0&FingerJoint_finger=2.0&FingerJoint_play=0.0&FingerJoint_space=2.0&FingerJoint_width=1.0&Lid_handle=none&Lid_style=none&Lid_handle_height=8.0&Lid_height=4.0&Lid_play=0.1&x=100.0&y=100.0&h=100.0&outside=0&outside=1&bottom_edge=h&thickness=3.0&format=svg&tabs=0.0&qr_code=0&debug=0&labels=0&labels=1&reference=100.0&inner_corners=loop&burn=0.1&language=en +SettingsUrl short: https://boxes.hackerspace-bamberg.de/ABox + + + + + 100.0mm, burn:0.10mm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 13518b8e..46d2442d 100644 --- a/build.gradle +++ b/build.gradle @@ -10,14 +10,17 @@ spotless { java { lineEndings = com.diffplug.spotless.LineEnding.UNIX // Eclipse formatter with your config file - eclipse('4.26') // Uses Eclipse's built-in default profile — no XML needed! + eclipse('4.26') // Uses Eclipse's built-in default profile — no XML needed! // Optional but recommended additions: removeUnusedImports() trimTrailingWhitespace() endWithNewline() } } - +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} File buildDir = file("."); Properties props = new Properties() props.load(new FileInputStream(buildDir.getAbsolutePath()+"/src/main/resources/com/neuronrobotics/javacad/build.properties")) @@ -25,41 +28,7 @@ group = "com.neuronrobotics" archivesBaseName = "JavaCad" version = props."app.version" -// BEGIN AI SLOP - -//nexusStaging { -// serverUrl = "https://oss.sonatype.org/service/local/" -// username = System.getenv("MAVEN_USERNAME") -// password = System.getenv("MAVEN_PASSWORD") -// packageGroup = "com.neuronrobotics" // Replace with your actual package group -//} - -//task closeAndReleaseSeparately { -// dependsOn tasks.releaseRepository -//} - -//tasks.releaseRepository.dependsOn tasks.closeRepository -//tasks.closeRepository.dependsOn tasks.getStagingProfile - -// Optional: Add this if you want to see more information during the execution -//tasks.getStagingProfile.logging.level = LogLevel.INFO -//tasks.closeRepository.logging.level = LogLevel.INFO -//tasks.releaseRepository.logging.level = LogLevel.INFO - -//tasks.getStagingProfile.doFirst { -// println "Executing getStagingProfile task" -//} - -//tasks.closeRepository.doFirst { -// println "Executing closeRepository task" -//} -// -//tasks.releaseRepository.doFirst { -// println "Executing releaseRepository task" -//} - -// END AI SLOP -sourceCompatibility = '1.8' + [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' //apply from: 'http://gradle-plugins.mihosoft.eu/latest/vlicenseheader.gradle' @@ -74,6 +43,7 @@ task sourcesJar(type: Jar) { repositories { mavenCentral() mavenLocal() + maven { url "https://clojars.org/repo" } } // javadoc is way too strict for my taste. @@ -130,8 +100,11 @@ dependencies { implementation 'com.aparapi:aparapi:3.0.2' //SSL for server -implementation 'org.bouncycastle:bcprov-jdk18on:1.80' -implementation 'org.bouncycastle:bcpkix-jdk18on:1.80' + implementation 'org.bouncycastle:bcprov-jdk18on:1.80' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.80' + + //manifold 3d + implementation("com.github.madhephaestus:manifold3d:v3.4.1-9-d40f11b9-SNAPSHOT") } @@ -141,9 +114,19 @@ ext { buildTime = new java.text.SimpleDateFormat('HH:mm:ss.SSSZ').format(buildTimeAndDate) } +tasks.withType(JavaCompile).configureEach { + options.compilerArgs += ['--enable-preview'] +} + +tasks.withType(Test).configureEach { + jvmArgs '--enable-preview' +} +tasks.withType(JavaExec).configureEach { + jvmArgs '--enable-preview' +} test { - dependsOn 'spotlessCheck' + dependsOn 'spotlessApply' testLogging { // Show which test is running events "passed", "skipped", "failed", "standardOut", "standardError" diff --git a/gradlew.bat b/gradlew.bat index 9b42019c..9d21a218 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,94 @@ -@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 -@rem SPDX-License-Identifier: Apache-2.0 -@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=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@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% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -: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 %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 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! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -: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 +@rem SPDX-License-Identifier: Apache-2.0 +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/com/neuronrobotics/manifold3d/CSGManifold3d.java b/src/main/java/com/neuronrobotics/manifold3d/CSGManifold3d.java new file mode 100644 index 00000000..593c286c --- /dev/null +++ b/src/main/java/com/neuronrobotics/manifold3d/CSGManifold3d.java @@ -0,0 +1,398 @@ +package com.neuronrobotics.manifold3d; + +import java.lang.foreign.MemorySegment; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.cadoodlecad.manifold.ManifoldBindings; +import com.cadoodlecad.manifold.ManifoldBindings.ManifoldError; +import com.cadoodlecad.manifold.ManifoldBindings.MeshData64; + +import eu.mihosoft.vrl.v3d.CSG; +import eu.mihosoft.vrl.v3d.Polygon; +import eu.mihosoft.vrl.v3d.Transform; +import eu.mihosoft.vrl.v3d.Vector3d; +import eu.mihosoft.vrl.v3d.Vertex; +import eu.mihosoft.vrl.v3d.ext.org.poly2tri.PolygonUtil; + +public class CSGManifold3d { + private final ManifoldBindings manifold; + // private final Manifold3dExporter exporter; + // private final Manifold3dImporter importer; + + public CSGManifold3d() throws Exception { + this.manifold = new ManifoldBindings(); + // exporter = new Manifold3dExporter(manifold); + // importer = new Manifold3dImporter(manifold); + } + + + /** + * Converts a JCSG {@link CSG} into a native manifold {@link MemorySegment}. + * + *

+ * The caller is responsible for eventually freeing the returned segment via the + * bridge's delete method (e.g. {@code manifold_delete_manifold}). + * + * @param csg + * the solid to export; must not be null + * @return a native manifold segment ready for boolean operations + * @throws Throwable + * if the native import call fails + * @throws IllegalArgumentException + * if {@code csg} is null or has no polygons + */ + public MemorySegment toManifold(CSG csg) throws Throwable { + if (csg == null) + throw new IllegalArgumentException("csg must not be null"); + + List polygons = csg.getPolygons(); + if (polygons == null || polygons.isEmpty()) + throw new IllegalArgumentException("CSG has no polygons"); + + // Build an indexed triangle mesh. + // Use a tolerance-free exact key so we don't merge + // numerically-close-but-distinct verts. + Map vertexIndex = new HashMap<>(); + List vertexList = new ArrayList<>(); + List triList = new ArrayList<>(); + + for (Polygon incoming : polygons) { + for (Polygon poly : PolygonUtil.triangulatePolygon(incoming)) { + List pverts = poly.getVertices(); + if (pverts == null || pverts.size() < 3) + continue; + + // Fan triangulation: (0,1,2), (0,2,3), (0,3,4), ... + int i0 = intern(pverts.get(0), vertexIndex, vertexList); + for (int i = 1; i < pverts.size() - 1; i++) { + int i1 = intern(pverts.get(i), vertexIndex, vertexList); + int i2 = intern(pverts.get(i + 1), vertexIndex, vertexList); + + // Skip degenerate triangles (two or more identical indices). + if (i0 == i1 || i1 == i2 || i0 == i2) + continue; + + triList.add((long) i0); + triList.add((long) i1); + triList.add((long) i2); + } + } + } + + if (triList.isEmpty()) + throw new IllegalArgumentException("CSG produced no valid triangles after triangulation"); + + long nVerts = vertexList.size(); + long nTris = triList.size() / 3; + + // Flatten vertex list into a primitive array. + double[] vertices = new double[(int) (nVerts * 3)]; + for (int i = 0; i < nVerts; i++) { + double[] v = vertexList.get(i); + vertices[i * 3] = v[0]; + vertices[i * 3 + 1] = v[1]; + vertices[i * 3 + 2] = v[2]; + } + + // Flatten triangle index list. + long[] triangles = new long[triList.size()]; + for (int i = 0; i < triList.size(); i++) { + triangles[i] = triList.get(i); + } + + MemorySegment ms = manifold.importMeshGL64(vertices, triangles, nVerts, nTris); + checkResult(ms); + return ms; + } + + /** + * Returns the index of {@code v} in {@code vertexList}, inserting it if not + * already present. The key is an exact string representation of (x, y, z) using + * {@link Double#toHexString} so that only bit-identical positions are merged, + * matching the BSP's behavior. + */ + private static int intern(Vertex v, Map index, List list) { + // 1. Lower the precision slightly. 1e9 is too high for 'double' stability + // after multiple CSG operations. 1e7 (0.1 nanometer) is the "sweet spot". + double precision = 1e7; + + long x = Math.round(v.pos.x * precision); + long y = Math.round(v.pos.y * precision); + long z = Math.round(v.pos.z * precision); + + String key = x + "," + y + "," + z; + + return index.computeIfAbsent(key, k -> { + int idx = list.size(); + // Use the RAW position for the first instance of this vertex. + // This keeps the geometry as true to the BSP as possible. + list.add(new double[] { v.pos.x, v.pos.y, v.pos.z }); + return idx; + }); + } + // ------------------------------------------------------------------------- + // helpers + + + /** + * Converts a native manifold {@link MemorySegment} to a JCSG {@link CSG}. + * + *

+ * The manifold is exported as a triangle soup via the bridge's + * {@code exportMeshGL64} method. Each triangle becomes one JCSG {@link Polygon} + * (three {@link Vertex} objects with positions taken from the flat vertex + * array). Per-vertex normals are computed as the face normal so that JCSG + * downstream tools (BSP, boolean ops) have valid planes. + * + * @param ms + * native manifold segment returned by the bridge import call + * @return a new {@link CSG} representing the same geometry + * @throws Throwable + * if the native export call fails + * @throws IllegalArgumentException + * if {@code manifold} is null + */ + public CSG fromManifold(MemorySegment ms) throws Throwable { + if (ms == null) + throw new IllegalArgumentException("manifold segment must not be null"); + MeshData64 mesh = this.manifold.exportMeshGL64(ms); + + double[] verts = mesh.vertices(); // flat [x0,y0,z0, x1,y1,z1, ...] + long[] tris = mesh.triangles(); // flat [i0,i1,i2, i3,i4,i5, ...] + int triCount = mesh.triCount(); + + if (triCount == 0) + return new CSG(); + + ArrayList polygons = new ArrayList<>(triCount); + + for (int t = 0; t < triCount; t++) { + int base = t * 3; + + Vector3d p0 = vertexAt(verts, (int) tris[base]); + Vector3d p1 = vertexAt(verts, (int) tris[base + 1]); + Vector3d p2 = vertexAt(verts, (int) tris[base + 2]); + + List vertices = Arrays.asList(new Vertex(p0), new Vertex(p1), new Vertex(p2)); + + polygons.add(new Polygon(vertices)); + } + + return CSG.fromPolygons(polygons); + } + + // ------------------------------------------------------------------------- + // helpers + + + private static Vector3d vertexAt(double[] verts, int index) { + int base = index * 3; + return new Vector3d(verts[base], verts[base + 1], verts[base + 2]); + } + + /** + * Slices the given CSG at Z=0 and returns the resulting cross-section as a list + * of JCSG {@link Polygon} objects. + * + *

+ * Each polygon is a flat contour in the Z=0 plane. Outer contours are wound + * counter-clockwise; holes are wound clockwise — matching the winding order + * that manifold_slice produces. + * + *

+ * Contours with fewer than 3 vertices are skipped because they cannot form a + * valid polygon. + * + * @param csg + * the solid to slice + * @return closed polygon contours of the cross-section at Z=0, never + * {@code null}, may be empty if the plane misses the solid + * @throws Throwable + * @throws RuntimeException + * wrapping any native call failure + */ + public ArrayList sliceAtZero(CSG incoming, Transform slicePlane) throws Throwable { + CSG csg = incoming.transformed(slicePlane.inverse()); + MemorySegment csgm = null; + try { + csgm = toManifold(csg); + checkResult(csgm); + List contours = manifold.slice(csgm, 0.0); + + ArrayList result = new ArrayList<>(contours.size()); + + for (double[][] contour : contours) { + // Need at least 3 vertices to define a plane and a valid polygon. + if (contour.length < 3) { + continue; + } + + ArrayList points = new ArrayList<>(contour.length); + for (double[] xy : contour) { + // Z=0 because this is a cross-section at height 0. + points.add(Vector3d.xyz(xy[0], xy[1], 0.0)); + } + + result.add(Polygon.fromPoints(points)); + } + + return result; + + } catch (Throwable e) { + if (csgm != null) + manifold.delete(csgm); + throw new RuntimeException("Failed to slice CSG at Z=0", e); + } + } + + // ------------------------------------------------------------------------- + // Boolean operations + // ------------------------------------------------------------------------- + + /** + * Returns the union of two CSG solids. Uses {@code manifold.union(a, b)} + * directly (wrapper around {@code manifold_union} in the C library). + */ + public CSG union(CSG a, CSG b) throws Throwable { + MemorySegment ma = toManifold(a); + MemorySegment mb = toManifold(b); + try { + MemorySegment result = manifold.union(ma, mb); + checkResult(result); + CSG fromManifold = fromManifold(result); + manifold.delete(result); + return fromManifold; + } finally { + manifold.delete(ma); + manifold.delete(mb); + } + } + + /** + * Returns the difference of two CSG solids (a minus b). Uses + * {@code manifold.difference(a, b)}. + */ + public CSG difference(CSG a, CSG b) throws Throwable { + MemorySegment ma = toManifold(a); + MemorySegment mb = toManifold(b); + try { + MemorySegment result = manifold.difference(ma, mb); + checkResult(result); + CSG fromManifold = fromManifold(result); + manifold.delete(result); + return fromManifold; + } finally { + manifold.delete(ma); + manifold.delete(mb); + } + } + + /** + * Returns the intersection of two CSG solids. Uses + * {@code manifold.intersection(a, b)}. + */ + public CSG intersection(CSG a, CSG b) throws Throwable { + MemorySegment ma = toManifold(a); + MemorySegment mb = toManifold(b); + try { + MemorySegment result = manifold.intersection(ma, mb); + checkResult(result); + CSG fromManifold = fromManifold(result); + manifold.delete(result); + return fromManifold; + } finally { + manifold.delete(ma); + manifold.delete(mb); + } + } + + // ------------------------------------------------------------------------- + // Convex hull + // ------------------------------------------------------------------------- + + /** + * Returns the convex hull of two CSG solids combined. + *

+ * Manifold's {@code manifold_hull} operates on a single manifold, so we first + * union the two inputs to combine their point sets, then hull the result via + * {@code manifold.hull(MemorySegment)}. + */ + public CSG hull(CSG a) throws Throwable { + MemorySegment ma = toManifold(a); + checkResult(ma); + try { + MemorySegment result = manifold.hull(ma); + checkResult(result); + CSG fromManifold = fromManifold(result); + manifold.delete(result); + return fromManifold; + } finally { + manifold.delete(ma); + } + } + + /** + * Convenience overload: convex hull over an arbitrary number of CSG solids. + * Uses {@code manifold.batchHull(MemorySegment[])} which maps to + * {@code manifold_batch_hull}. + */ + public CSG hull(CSG... solids) throws Throwable { + MemorySegment[] segs = new MemorySegment[solids.length]; + for (int i = 0; i < solids.length; i++) { + segs[i] = toManifold(solids[i]); + } + try { + MemorySegment result = manifold.batchHull(segs); + checkResult(result); + return fromManifold(result); + } finally { + for (MemorySegment seg : segs) + manifold.delete(seg); + } + } + + private void checkResult(MemorySegment... memorySegments) throws Throwable { + for (int i = 0; i < memorySegments.length; i++) { + MemorySegment ms = memorySegments[i]; + ManifoldError err = manifold.status(ms); + //System.out.println("Status of Manifold Op is "+result); + if (err != ManifoldError.NO_ERROR) { + System.out.println("Status: " + err); + System.out.println("Verts: " + manifold.numVert(ms)); + System.out.println("Tris: " + manifold.numTri(ms)); + System.out.println("Genus: " + manifold.genus(ms)); + throw new NonManifoldShapeError("Error was " + err); + }else { + System.out.println("Manifold check ok!"); + } + } + } + + + public CSG hull(List points) throws Throwable { + ArrayList pts = new ArrayList(); + for (int i = 0; i < points.size(); i++) { + Vector3d v = points.get(i); + double[] p = new double[] { v.x, v.y, v.z }; + pts.add(p); + } + MemorySegment mem = null; + try { + mem = manifold.hull(pts); + checkResult(mem); + + CSG fromManifold = fromManifold(mem); + manifold.delete(mem); + mem=null; + return fromManifold; + } finally { + manifold.delete(mem); + } + } + + +} diff --git a/src/main/java/com/neuronrobotics/manifold3d/NonManifoldShapeError.java b/src/main/java/com/neuronrobotics/manifold3d/NonManifoldShapeError.java new file mode 100644 index 00000000..802df317 --- /dev/null +++ b/src/main/java/com/neuronrobotics/manifold3d/NonManifoldShapeError.java @@ -0,0 +1,11 @@ +package com.neuronrobotics.manifold3d; + +public class NonManifoldShapeError extends Exception { + + private static final long serialVersionUID = 9171271069943603762L; + + public NonManifoldShapeError(String string) { + super(string); + } + +} diff --git a/src/main/java/eu/mihosoft/vrl/v3d/CSG.java b/src/main/java/eu/mihosoft/vrl/v3d/CSG.java index e376701b..ec3d2548 100644 --- a/src/main/java/eu/mihosoft/vrl/v3d/CSG.java +++ b/src/main/java/eu/mihosoft/vrl/v3d/CSG.java @@ -41,6 +41,7 @@ import eu.mihosoft.vrl.v3d.parametrics.LengthParameter; import eu.mihosoft.vrl.v3d.parametrics.Parameter; +import java.io.File; import java.io.Serializable; import java.lang.reflect.Field; import java.time.Duration; @@ -63,6 +64,7 @@ import com.aparapi.Range; import com.aparapi.internal.kernel.KernelRunner; import com.neuronrobotics.interaction.CadInteractionEvent; +import com.neuronrobotics.manifold3d.CSGManifold3d; import javafx.scene.paint.Color; import javafx.scene.paint.PhongMaterial; @@ -192,6 +194,7 @@ public void progressUpdate(int currentIndex, int finalIndex, String type, CSG in private int pointsAdded; private String uniqueId = UUID.randomUUID().toString(); + private static CSGManifold3d manifold = null; /** * Instantiates a new csg. @@ -886,13 +889,22 @@ public CSG union(CSG csg) { // triangulate(); // csg.triangulate(); switch (getOptType()) { - case CSG_BOUND : - return _unionCSGBoundsOpt(csg).historySync(this).historySync(csg); - case POLYGON_BOUND : - return _unionPolygonBoundsOpt(csg).historySync(this).historySync(csg); - default : - // return _unionIntersectOpt(csg); - return _unionNoOpt(csg).historySync(this).historySync(csg); + case Manifold3d: + try { + return getManifold().union(this, csg); + } catch (Throwable e) { + System.err.println("ERROR failing over to Java Union "+e.getMessage()); + e.printStackTrace(); + } + case CSG_BOUND: + return _unionCSGBoundsOpt(csg).historySync(this).historySync(csg); + case POLYGON_BOUND: + return _unionPolygonBoundsOpt(csg).historySync(this).historySync(csg); + + default: + // return _unionIntersectOpt(csg); + return _unionNoOpt(csg).historySync(this).historySync(csg); + } } @@ -1409,17 +1421,26 @@ public CSG difference(CSG csg) { // polygons if (this.getPolygons().size() > 0 && csg.getPolygons().size() > 0) { switch (getOptType()) { - case CSG_BOUND : - return _differenceCSGBoundsOpt(csg).historySync(this).historySync(csg); - case POLYGON_BOUND : - return _differencePolygonBoundsOpt(csg).historySync(this).historySync(csg); - default : - return _differenceNoOpt(csg).historySync(this).historySync(csg); + case Manifold3d: + try { + return getManifold().difference(this, csg); + } catch (Throwable e) { + System.err.println("ERROR failing over to Java Difference "+e.getMessage()); + e.printStackTrace(); + } + case CSG_BOUND: + return _differenceCSGBoundsOpt(csg).historySync(this).historySync(csg); + case POLYGON_BOUND: + return _differencePolygonBoundsOpt(csg).historySync(this).historySync(csg); + + default: + return _differenceNoOpt(csg).historySync(this).historySync(csg); + } } else return this; } catch (Exception ex) { - // ex.printStackTrace(); + ex.printStackTrace(); try { // com.neuronrobotics.sdk.common.Log.error("CSG difference failed, performing // workaround"); @@ -1428,14 +1449,16 @@ public CSG difference(CSG csg) { if (intersectingParts.getPolygons().size() > 0) { switch (getOptType()) { - case CSG_BOUND : - return _differenceCSGBoundsOpt(intersectingParts).historySync(this) - .historySync(intersectingParts); - case POLYGON_BOUND : - return _differencePolygonBoundsOpt(intersectingParts).historySync(this) - .historySync(intersectingParts); - default : - return _differenceNoOpt(intersectingParts).historySync(this).historySync(intersectingParts); + case CSG_BOUND: + return _differenceCSGBoundsOpt(intersectingParts).historySync(this) + .historySync(intersectingParts); + case POLYGON_BOUND: + return _differencePolygonBoundsOpt(intersectingParts).historySync(this) + .historySync(intersectingParts); + case Manifold3d: + new RuntimeException("Not implemented yet").printStackTrace(); + default: + return _differenceNoOpt(intersectingParts).historySync(this).historySync(intersectingParts); } } else return this; @@ -1577,13 +1600,20 @@ public CSG intersect(CSG csg) { e.printStackTrace(); } } - // triangulate(); - // csg.triangulate(); if (getPolygons().size() == 0 || csg.getPolygons().size() == 0) { Exception ex = new Exception("Error! Intersection is invalid when one CSG has no polygons!"); ex.printStackTrace(); return CSG.fromPolygons(new ArrayList()).historySync(this).historySync(csg); } + if (defaultOptType == OptType.Manifold3d) { + try { + return getManifold().intersection(this, csg); + } catch (Throwable e) { + System.err.println("ERROR failing over to Java Intersect "+e.getMessage()); + e.printStackTrace(); + } + } + Node a; try { a = new Node(this.clone().getPolygons(), this.getPolygons().get(0).getPlane()); @@ -1706,6 +1736,24 @@ public String toStlString() { return sb.toString(); } + public CSG to3mf(File target) { + if (defaultOptType == OptType.Manifold3d) { + new RuntimeException("Manifold3d 3mf export not implemented yet").printStackTrace(); + } else { + throw new RuntimeException("Non-Manifold3d 3mf export not implemented yet"); + } + return this; + } + + public static CSG loadFrom3mf(File target) { + if (defaultOptType == OptType.Manifold3d) { + new RuntimeException("Manifold3d 3mf export not implemented yet").printStackTrace(); + } else { + throw new RuntimeException("Non-Manifold3d 3mf export not implemented yet"); + } + return null; + } + /** * Returns this csg in STL string format. * @@ -1715,6 +1763,9 @@ public String toStlString() { * @return the specified string builder */ public StringBuilder toStlString(StringBuilder sb) { +// if (defaultOptType == OptType.Manifold3d) { +// new RuntimeException("Manifold3d STL export not implemented yet").printStackTrace(); +// } triangulate(false); try { sb.append("solid v3d.csg\n"); @@ -1874,7 +1925,7 @@ private int runGPUMakeManifold(int iteration, int np, int longLength, int numPol int[] added = new int[numberOfPolygons]; int testPointChunk = 1000; int snapChunk = 1000; - int[] tp = new int[]{0, snapChunk}; + int[] tp = new int[] { 0, snapChunk }; // Aparapi-compatible kernel with flattened data Kernel snapPointsToDistance = new Kernel() { @@ -2220,7 +2271,7 @@ public static void gpuRun(int numberOfPoints, Kernel kernel, float[] done, Strin progressMoniter.progressUpdate(0, 100, "CPU mode " + valueOf, null); kernel.setExecutionMode(Kernel.EXECUTION_MODE.JTP); // Java Thread Pool } - int[] iteration = new int[]{0}; + int[] iteration = new int[] { 0 }; long begin = System.currentTimeMillis(); boolean print = false; @@ -2771,6 +2822,32 @@ protected OptType getOptType() { * the optType to set */ public static void setDefaultOptType(OptType optType) { + if (optType == OptType.Manifold3d) { + try { + manifold = new CSGManifold3d(); + Slice.setSliceEngine(new ISlice() { + @Override + public List slice(CSG incoming, Transform slicePlane, double normalInsetDistance) + throws ColinearPointsException { + try { + return getManifold().sliceAtZero(incoming, slicePlane); + } catch (Throwable e) { + System.err.println("Slice failed on manifold, using legacy slice"); + e.printStackTrace(); + Slice.setSliceEngine(null); + Slice.getSliceEngine();// set the default when the engine is null + return Slice.getSliceEngine().slice(incoming,slicePlane,normalInsetDistance); + } + } + }); + } catch (Exception e) { + e.printStackTrace(); + optType = defaultOptType; + } + } else { + Slice.setSliceEngine(null); + Slice.getSliceEngine();// set the default when the engine is null + } defaultOptType = optType; } @@ -2781,6 +2858,7 @@ public static void setDefaultOptType(OptType optType) { * the optType to set */ public CSG setOptType(OptType optType) { + this.optType = optType; return this; } @@ -2809,6 +2887,8 @@ public static enum OptType { /** The polygon bound. */ POLYGON_BOUND, + Manifold3d, + /** The none. */ NONE } @@ -3301,7 +3381,7 @@ public static Color getDefaultColor() { public CSG getBoundingBox() { return new Cube((-this.getMinX() + this.getMaxX()), (-this.getMinY() + this.getMaxY()), (-this.getMinZ() + this.getMaxZ())).toCSG().toXMax().movex(this.getMaxX()).toYMax() - .movey(this.getMaxY()).toZMax().movez(this.getMaxZ()); + .movey(this.getMaxY()).toZMax().movez(this.getMaxZ()); } public String getName() { @@ -4072,8 +4152,8 @@ public static List tessellate(CSG incoming, int xSteps, int ySteps, int zSt public static List tessellate(CSG incoming, int xSteps, int ySteps, int zSteps, double oddRowXOffset, double oddRowYOffset, double oddRowZOffset, double oddColXOffset, double oddColYOffset, double oddColZOffset, double oddLayXOffset, double oddLayYOffset, double oddLayZOffset) { - double[][] offsets = {{oddRowXOffset, oddRowYOffset, oddRowZOffset}, - {oddColXOffset, oddColYOffset, oddColZOffset}, {oddLayXOffset, oddLayYOffset, oddLayZOffset}}; + double[][] offsets = { { oddRowXOffset, oddRowYOffset, oddRowZOffset }, + { oddColXOffset, oddColYOffset, oddColZOffset }, { oddLayXOffset, oddLayYOffset, oddLayZOffset } }; return tessellate(incoming, xSteps, ySteps, zSteps, incoming.getTotalX(), incoming.getTotalY(), incoming.getTotalZ(), offsets); } @@ -4285,4 +4365,13 @@ public String getUniqueId() { return uniqueId; } + public static OptType getDefaultOptionType() { + // TODO Auto-generated method stub + return defaultOptType; + } + + public static CSGManifold3d getManifold() { + return manifold; + } + } diff --git a/src/main/java/eu/mihosoft/vrl/v3d/Slice.java b/src/main/java/eu/mihosoft/vrl/v3d/Slice.java index 0452923e..66482055 100644 --- a/src/main/java/eu/mihosoft/vrl/v3d/Slice.java +++ b/src/main/java/eu/mihosoft/vrl/v3d/Slice.java @@ -419,7 +419,7 @@ boolean withinAPix(int[] incoming, int[] out) { } }; - private static ISlice sliceEngine = new DefaultSliceImp(); + private static ISlice sliceEngine; /** * Returns true if this polygon lies entirely in the z plane @@ -498,6 +498,8 @@ public static List slice(CSG incoming, double normalInsetDistance) thro return slice(incoming, new Transform(), normalInsetDistance); } public static ISlice getSliceEngine() { + if (sliceEngine == null) + sliceEngine = new DefaultSliceImp(); return sliceEngine; } diff --git a/src/main/java/eu/mihosoft/vrl/v3d/Sphere.java b/src/main/java/eu/mihosoft/vrl/v3d/Sphere.java index 971a8484..41187d41 100644 --- a/src/main/java/eu/mihosoft/vrl/v3d/Sphere.java +++ b/src/main/java/eu/mihosoft/vrl/v3d/Sphere.java @@ -34,7 +34,13 @@ package eu.mihosoft.vrl.v3d; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import eu.mihosoft.vrl.v3d.ext.quickhull3d.HullUtil; // Auto-generated Javadoc /** @@ -86,6 +92,7 @@ public Sphere(double radius) { init(); this.radius = radius; } + // public Cube(LengthParameter w, LengthParameter h, LengthParameter d) { // this(Vector3d.ZERO, new Vector3d(w.getMM(), h.getMM(), d.getMM())); // @@ -162,47 +169,49 @@ private void init() { private Vertex sphereVertex(Vector3d c, double r, double theta, double phi) { theta *= Math.PI * 2; phi *= Math.PI; + + // Clamp poles to exact values to avoid sin(PI) floating point error + if (phi <= 0) + return new Vertex(c.plus(new Vector3d(0, r, 0))); + if (phi >= Math.PI) + return new Vertex(c.plus(new Vector3d(0, -r, 0))); + Vector3d dir = new Vector3d(Math.cos(theta) * Math.sin(phi), Math.cos(phi), Math.sin(theta) * Math.sin(phi)); return new Vertex(c.plus(dir.times(r))); } - /* - * (non-Javadoc) - * - * @see eu.mihosoft.vrl.v3d.Primitive#toPolygons() - */ - @Override public List toPolygons() { - if (radius <= 0) - throw new NumberFormatException("radius can not be negative"); - List polygons = new ArrayList<>(); - - for (int i = 0; i < getNumSlices(); i++) { - for (int j = 0; j < getNumStacks(); j++) { - final List vertices = new ArrayList<>(); - - vertices.add(sphereVertex(center, radius, i / (double) getNumSlices(), j / (double) getNumStacks())); - if (j > 0) { - vertices.add(sphereVertex(center, radius, (i + 1) / (double) getNumSlices(), - j / (double) getNumStacks())); - } - if (j < getNumStacks() - 1) { - vertices.add(sphereVertex(center, radius, (i + 1) / (double) getNumSlices(), - (j + 1) / (double) getNumStacks())); - } - vertices.add( - sphereVertex(center, radius, i / (double) getNumSlices(), (j + 1) / (double) getNumStacks())); - try { - polygons.add(new Polygon(vertices, getProperties())); - } catch (ColinearPointsException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + List points = new ArrayList<>(); + + // 1. Add the Poles explicitly (avoiding the loop to ensure they are clean) + points.add(new Vector3d(center.x, center.y + radius, center.z)); // North + points.add(new Vector3d(center.x, center.y - radius, center.z)); // South + + // 2. Generate the rings (excluding the pole stacks) + for (int j = 1; j < numStacks; j++) { + double phi = Math.PI * j / numStacks; + double y = Math.cos(phi); + double rRing = Math.sin(phi); + + for (int i = 0; i < numSlices; i++) { + double theta = 2.0 * Math.PI * i / numSlices; + + double x = Math.cos(theta) * rRing; + double z = Math.sin(theta) * rRing; + + // Micro-snapping to zero for stability + if (Math.abs(x) < 1e-12) + x = 0; + if (Math.abs(z) < 1e-12) + z = 0; + + points.add(new Vector3d(center.x + x * radius, center.y + y * radius, center.z + z * radius)); } } - return polygons; + return HullUtil.hull(points, getProperties()).getPolygons(); } + /** * Gets the center. * @@ -259,8 +268,8 @@ public int getNumSlices() { * the numSlices to set */ public Sphere setNumSlices(int numSlices) { - if (numSlices > (NUM_SLICES * 4)) - System.out.println("Very large sphere! this may crash!"); + // if (numSlices > (NUM_SLICES * 4)) + // System.out.println("Very large sphere! this may crash!"); this.numSlices = numSlices; return this; } @@ -281,8 +290,8 @@ public int getNumStacks() { * the numStacks to set */ public Sphere setNumStacks(int numStacks) { - if (numStacks > (NUM_STACKS * 4)) - System.out.println("Very large sphere! this may crash!"); + // if (numStacks > (NUM_STACKS * 4)) + // System.out.println("Very large sphere! this may crash!"); this.numStacks = numStacks; return this; } diff --git a/src/main/java/eu/mihosoft/vrl/v3d/Vertex.java b/src/main/java/eu/mihosoft/vrl/v3d/Vertex.java index 5fb7d99c..1fcbe19e 100644 --- a/src/main/java/eu/mihosoft/vrl/v3d/Vertex.java +++ b/src/main/java/eu/mihosoft/vrl/v3d/Vertex.java @@ -81,6 +81,10 @@ private Vertex(Vector3d pos, double weight) { this.weight = weight; } + public Vertex(double x, double y, double z) { + pos = new Vector3d(x, y, z); + } + /* * (non-Javadoc) * diff --git a/src/main/java/eu/mihosoft/vrl/v3d/ext/imagej/STLLoader.java b/src/main/java/eu/mihosoft/vrl/v3d/ext/imagej/STLLoader.java index 3bae1749..217d6cb9 100644 --- a/src/main/java/eu/mihosoft/vrl/v3d/ext/imagej/STLLoader.java +++ b/src/main/java/eu/mihosoft/vrl/v3d/ext/imagej/STLLoader.java @@ -17,6 +17,8 @@ import java.text.ParseException; import java.util.ArrayList; +import eu.mihosoft.vrl.v3d.CSG; +import eu.mihosoft.vrl.v3d.CSG.OptType; import eu.mihosoft.vrl.v3d.ColinearPointsException; import eu.mihosoft.vrl.v3d.Plane; import eu.mihosoft.vrl.v3d.Polygon; @@ -47,6 +49,9 @@ public STLLoader() { * Signals that an I/O exception has occurred. */ public ArrayList parse(File f) throws IOException { + if (CSG.getDefaultOptionType() == OptType.Manifold3d) { + new RuntimeException("Manifold3d STL import not implemented yet").printStackTrace(); + } ArrayList polygons = new ArrayList<>(); // determine if this is a binary or ASCII STL diff --git a/src/main/java/eu/mihosoft/vrl/v3d/ext/quickhull3d/HullUtil.java b/src/main/java/eu/mihosoft/vrl/v3d/ext/quickhull3d/HullUtil.java index 3542d529..a5f52670 100644 --- a/src/main/java/eu/mihosoft/vrl/v3d/ext/quickhull3d/HullUtil.java +++ b/src/main/java/eu/mihosoft/vrl/v3d/ext/quickhull3d/HullUtil.java @@ -6,6 +6,7 @@ package eu.mihosoft.vrl.v3d.ext.quickhull3d; import eu.mihosoft.vrl.v3d.CSG; +import eu.mihosoft.vrl.v3d.CSG.OptType; import eu.mihosoft.vrl.v3d.CSGClient; import eu.mihosoft.vrl.v3d.ColinearPointsException; import eu.mihosoft.vrl.v3d.Vector3d; @@ -71,6 +72,14 @@ public static CSG hull(List points, PropertyStorage storage) { e.printStackTrace(); } } + if (CSG.getDefaultOptionType() == OptType.Manifold3d) { + try { + return CSG.getManifold().hull(points); + } catch (Throwable e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } Point3d[] hullPoints = points.stream().map((vec) -> new Point3d(vec.x, vec.y, vec.z)).toArray(Point3d[]::new); QuickHull3D hull = new QuickHull3D(); diff --git a/src/test/java/eu/mihosoft/vrl/v3d/Manifold3d_test.java b/src/test/java/eu/mihosoft/vrl/v3d/Manifold3d_test.java new file mode 100644 index 00000000..6cce321f --- /dev/null +++ b/src/test/java/eu/mihosoft/vrl/v3d/Manifold3d_test.java @@ -0,0 +1,47 @@ +package eu.mihosoft.vrl.v3d; + +import java.io.File; +import java.nio.file.Paths; +import java.util.List; + +import org.junit.Test; + +import eu.mihosoft.vrl.v3d.CSG.OptType; +import eu.mihosoft.vrl.v3d.svg.SVGExporter; + +public class Manifold3d_test { + @Test + public void loadTest() throws Throwable { + OptType og = CSG.getDefaultOptionType(); + + try { + CSG.setDefaultOptType(OptType.Manifold3d); + CSG cube = new Cube(50,50,50).toCSG(); + CSG sphere = new Sphere(30,10, 10).toCSG(); + FileUtil.write(Paths.get("Manifole-sphere.stl"), + sphere.toStlString()); + List polygons = Slice.slice(sphere, new Transform(), 0); + SVGExporter.export(polygons, new File("Manifold-SVGExportTest.svg"), false); + CSG difference = cube.difference(sphere); + CSG intersect = cube.intersect(sphere); + CSG union = cube.union(sphere); + + FileUtil.write(Paths.get("Manifole-union.stl"), + union.toStlString()); + FileUtil.write(Paths.get("Manifole-difference.stl"), + difference.toStlString()); + FileUtil.write(Paths.get("Manifole-intersect.stl"), + intersect.toStlString()); + CSG hull = union.hull(); + FileUtil.write(Paths.get("Manifole-hull.stl"), + hull.toStlString()); + } catch (Throwable t) { + t.printStackTrace(); + // Set back to default to complete test and not disrupt other tests + CSG.setDefaultOptType(og); + throw t; + } + // Set back to default to complete test and not disrupt other tests + CSG.setDefaultOptType(og); + } +}