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/Manifold-SVGExportTest.svg b/Manifold-SVGExportTest.svg new file mode 100644 index 00000000..0793552c --- /dev/null +++ b/Manifold-SVGExportTest.svg @@ -0,0 +1,1425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..b1940ec2 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-8") } @@ -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..14a7d345 --- /dev/null +++ b/src/main/java/com/neuronrobotics/manifold3d/CSGManifold3d.java @@ -0,0 +1,345 @@ +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.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); + } + + return manifold.importMeshGL64(vertices, triangles, nVerts, nTris); + } + + // ------------------------------------------------------------------------- + // 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 + /** + * 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) { + String key = Double.toHexString(v.pos.x) + "," + Double.toHexString(v.pos.y) + "," + + Double.toHexString(v.pos.z); + + return index.computeIfAbsent(key, k -> { + int idx = list.size(); + list.add(new double[] { v.pos.x, v.pos.y, v.pos.z }); + return idx; + }); + } + + 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 RuntimeException + * wrapping any native call failure + */ + public ArrayList sliceAtZero(CSG incoming, Transform slicePlane) { + CSG csg = incoming.transformed(slicePlane.inverse()); + try { + MemorySegment csgm = toManifold(csg); + 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 (RuntimeException e) { + throw e; + } catch (Throwable e) { + 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); + return fromManifold(result); + } 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); + return fromManifold(result); + } 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); + return fromManifold(result); + } 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); + try { + MemorySegment result = manifold.hull(ma); + return fromManifold(result); + } 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); + return fromManifold(result); + } finally { + for (MemorySegment seg : segs) + manifold.deleteMeshGL64(seg); + } + } + + 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.hullPoints(pts); + return fromManifold(mem); + } finally { + manifold.delete(mem); + } + } + + +} diff --git a/src/main/java/eu/mihosoft/vrl/v3d/CSG.java b/src/main/java/eu/mihosoft/vrl/v3d/CSG.java index e376701b..813dd95f 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. @@ -890,9 +893,17 @@ public CSG union(CSG csg) { return _unionCSGBoundsOpt(csg).historySync(this).historySync(csg); case POLYGON_BOUND : return _unionPolygonBoundsOpt(csg).historySync(this).historySync(csg); + case Manifold3d : + try { + return getManifold().union(this, csg); + } catch (Throwable e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } default : // return _unionIntersectOpt(csg); return _unionNoOpt(csg).historySync(this).historySync(csg); + } } @@ -1413,13 +1424,21 @@ public CSG difference(CSG csg) { return _differenceCSGBoundsOpt(csg).historySync(this).historySync(csg); case POLYGON_BOUND : return _differencePolygonBoundsOpt(csg).historySync(this).historySync(csg); + case Manifold3d : + try { + return getManifold().difference(this, csg); + } catch (Throwable e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } 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"); @@ -1434,6 +1453,8 @@ public CSG difference(CSG csg) { 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); } @@ -1577,13 +1598,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) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + Node a; try { a = new Node(this.clone().getPolygons(), this.getPolygons().get(0).getPlane()); @@ -1706,6 +1734,22 @@ 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 +1759,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"); @@ -2771,6 +2818,24 @@ 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 { + return getManifold().sliceAtZero(incoming, slicePlane); + } + }); + } 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 +2846,7 @@ public static void setDefaultOptType(OptType optType) { * the optType to set */ public CSG setOptType(OptType optType) { + this.optType = optType; return this; } @@ -2809,6 +2875,8 @@ public static enum OptType { /** The polygon bound. */ POLYGON_BOUND, + Manifold3d, + /** The none. */ NONE } @@ -4285,4 +4353,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/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..c10c538e --- /dev/null +++ b/src/test/java/eu/mihosoft/vrl/v3d/Manifold3d_test.java @@ -0,0 +1,44 @@ +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(25, 10, 10).toCSG(); + 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); + } +}