diff --git a/CHANGELOG.md b/CHANGELOG.md index 426d319d..6e343250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Version [next](https://github.com/Haskell-Things/ImplicitCAD/compare/v0.4.1.0...master) (202Y-MM-DD) * ExtOpenScad interface changes - * Added `polyhedron()` support [#497](https://github.com/Haskell-Things/ImplicitCAD/pull/497) * Added `projection(cut=true)` support [#448](https://github.com/Haskell-Things/ImplicitCAD/pull/448) + * Added `polyhedron()` support [#497](https://github.com/Haskell-Things/ImplicitCAD/pull/497) + * Added `import()` support [#505](https://github.com/Haskell-Things/ImplicitCAD/pull/505) * Haskell interface changes * `extrude` arguments are now swapped, instead of `extrude obj height` we now have `extrude height obj` [#473](https://github.com/Haskell-Things/ImplicitCAD/issues/473) diff --git a/Graphics/Implicit/ExtOpenScad/Primitives.hs b/Graphics/Implicit/ExtOpenScad/Primitives.hs index bb1274ba..6ec3eceb 100644 --- a/Graphics/Implicit/ExtOpenScad/Primitives.hs +++ b/Graphics/Implicit/ExtOpenScad/Primitives.hs @@ -15,13 +15,13 @@ -- Export one set containing all of the primitive modules. module Graphics.Implicit.ExtOpenScad.Primitives (primitiveModules) where -import Prelude(any, concat, elem, error, foldr, head, mapM, (.), Either(Left, Right), Bool(True, False), Maybe(Just, Nothing), ($), pure, show, either, id, (-), (==), (&&), (<), (*), cos, sin, pi, (/), (>), const, uncurry, (/=), (||), not, null, fmap, (<>), otherwise, (<*>), (<$>)) +import Prelude(any, concat, elem, error, fromIntegral, foldr, head, length, mapM, (.), (+), Either(Left, Right), Bool(True, False), Maybe(Just, Nothing), ($), pure, show, either, id, (-), (==), (&&), (<), (*), cos, sin, pi, (/), (>), const, uncurry, (/=), (||), not, null, fmap, (<>), otherwise, (<*>), (<$>)) import Graphics.Implicit.Definitions (ℝ, ℝ2, ℝ3, ℕ, SymbolicObj2, SymbolicObj3, ExtrudeMScale(C1), fromℕtoℝ, isScaleID) import Graphics.Implicit.Export.Util (centroid) -import Graphics.Implicit.ExtOpenScad.Definitions (OVal (OObj2, OObj3, ONModule, ONModuleWithSuite), ArgParser, Symbol(Symbol), StateC, SourcePosition) +import Graphics.Implicit.ExtOpenScad.Definitions (ArgParser, OVal (OObj2, OObj3, ONModule, ONModuleWithSuite), ScadOpts(importsAllowed), SourcePosition, StateC, Symbol(Symbol)) import Graphics.Implicit.ExtOpenScad.Util.ArgParser (contoursAreClosed, doc, defaultTo, example, meshIsWaterTight, test, eulerCharacteristic) @@ -29,18 +29,22 @@ import qualified Graphics.Implicit.ExtOpenScad.Util.ArgParser as GIEUA (argument import Graphics.Implicit.ExtOpenScad.Util.OVal (OTypeMirror, caseOType, divideObjs, (<||>)) -import Graphics.Implicit.ExtOpenScad.Util.StateC (errorC, warnC) +import Graphics.Implicit.ExtOpenScad.Util.StateC (getRelPath, errorC, scadOptions, warnC) -import Graphics.Implicit.TriUtil (Tri) +import Graphics.Implicit.Import.Definitions (trianglesFromSTL) + +import Graphics.Implicit.TriUtil (Tri, Triangle) -- Note the use of a qualified import, so we don't have the functions in this file conflict with what we're importing. import qualified Graphics.Implicit.Primitives as Prim (withRounding, sphere, rect3, rect, translate, circle, polygon, polyhedron, extrude, cylinder2, union, unionR, intersect, intersectR, difference, differenceR, rotate, slice, transform, rotate3V, rotate3, transform3, scale, extrudeM, rotateExtrude, shell, mirror, pack3, pack2, torus, ellipsoid, cone) import Control.Monad (foldM, mplus) +import Data.ByteString (readFile) + import Data.Foldable (toList) -import Data.List (genericIndex) +import Data.List (concatMap, genericIndex) import Data.Maybe (fromMaybe, isJust) @@ -48,14 +52,18 @@ import Data.Sequence (Seq, deleteAt, filter, fromList) import qualified Data.Sequence as DS (null) import Data.Text.Lazy (Text) -import qualified Data.Text.Lazy as DTL (pack) +import qualified Data.Text.Lazy as DTL (pack, unpack) import Control.Lens ((^.)) +import Control.Monad.State (liftIO) + import Linear (_m33, cross, dot, M34, M44, V2(V2), V3(V3), V4(V4)) import Linear.Affine (qdA) +import System.Directory (doesFileExist) + default (ℝ) -- FIXME: `defaultTo` is used inconsistently. The line between defaults and examples is a bit blurry. @@ -79,6 +87,7 @@ primitiveModules = , consModule ellipsoid [[("a", noDefault), ("b", hasDefault), ("c", hasDefault)]] , consModule polygon [[("points", noDefault)]] , consModule polyhedron [[("points", noDefault), ("faces", noDefault)]] + , consModule stlImport [[("file", noDefault)]] , consModuleWithSuite union [[("r", hasDefault)]] , consModuleWithSuite intersect [[("r", hasDefault)]] , consModuleWithSuite difference [[("r", hasDefault)]] @@ -118,6 +127,10 @@ primitiveModules = fixupArgs :: (Text, Bool) -> (Symbol, Bool) fixupArgs (symbol, maybeDefault) = (Symbol symbol, maybeDefault) +------------------------------------------------ +------------- Geometry Generation -------------- +------------------------------------------------ + -- | sphere is a module without a suite. -- this means that the parser will look for this like -- sphere(args...); @@ -520,6 +533,46 @@ polygon = moduleWithoutSuite "polygon" $ \_ -> do | otherwise = Prim.polygon points addObj2 $ addPolyOrSquare points +-- | Import an STL file. +stlImport :: (Symbol, SourcePosition -> ArgParser (StateC [OVal])) +stlImport = moduleWithoutSuite "import" $ \sourcePos -> do + example "import(\"myModel.stl\");" + fileName <- argument "file" `doc` "path to STL file" + pure $ do + opts <- scadOptions + if importsAllowed opts + then do + filePath <- getRelPath (DTL.unpack fileName) + fileExists <- liftIO $ doesFileExist filePath + if not fileExists + then + do + errorC sourcePos $ "Failed to import \"" <> (DTL.pack filePath) <> "\": File not found." + pure [] + else + do + fileContents <- liftIO $ readFile filePath + let + (points, woundTris) = trianglesToPolyhedron $ trianglesFromSTL 1 fileContents + -- FIXME: create a new primitive for this. + pure [OObj3 $ Prim.polyhedron points woundTris] + else do + warnC sourcePos $ "Refusing to import \"" <> fileName <> "\": File import disabled." + pure [] + where + -- | convert a list of Triangles to a set of Points and Triangles. + trianglesToPolyhedron :: [Triangle] -> ([ℝ3], [Tri]) + trianglesToPolyhedron triangles = (points, indices) + where + points = concatMap triangleToPoints triangles + triangleToPoints (v1,v2,v3) = [v1,v2,v3] + indices :: [Tri] + indices = [ (i*3, i*3+1, i*3+2) | i <- [0.. fromIntegral (length triangles) -1]] + +---------------------------------------------------- +------------- Geometry Manipulation ---------------- +---------------------------------------------------- + union :: (Symbol, SourcePosition -> [OVal] -> ArgParser (StateC [OVal])) union = moduleWithSuite "union" $ \_ children -> do r :: ℝ <- argument "r" diff --git a/Graphics/Implicit/Import/Definitions.hs b/Graphics/Implicit/Import/Definitions.hs new file mode 100644 index 00000000..d5b2afa3 --- /dev/null +++ b/Graphics/Implicit/Import/Definitions.hs @@ -0,0 +1,101 @@ +{- ORMOLU_DISABLE -} +-- Originally from HSlice. Now a part of ImplicitCAD. +{- + - Copyright 2016 Noah Halford and Catherine Moresco + - Copyright 2019-2026 Julia Longtin + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as published by + - the Free Software Foundation, either version 3 of the License, or + - (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see . + -} + +-- The top module of our STL handling routines. + +-- Allow us to use string literals for ByteStrings +{-# LANGUAGE OverloadedStrings #-} + +{- https://www.utgjiu.ro/rev_mec/mecanica/pdf/2010-01/13_Catalin%20Iancu.pdf -} + +module Graphics.Implicit.Import.Definitions (trianglesFromSTL) where + +import Prelude (($), (==), error, (<$>), (<>), show) + +import Control.Parallel.Strategies (using, rdeepseq, parBuffer) + +import Data.Maybe (Maybe(Just, Nothing), catMaybes) + +import Data.ByteString (ByteString) + +import Data.ByteString.Char8 (lines, words, unpack, breakSubstring, break, null, drop) + +import Text.Read (readMaybe) + +import Graphics.Implicit.Definitions (ℝ3,Fastℕ, fromFastℕ) + +import Graphics.Implicit.TriUtil (Triangle) + +import Linear (V3(V3)) + +---------------------------------------------------------------- +----------- Functions to deal with ASCII STL reading ----------- +---------------------------------------------------------------- + +-- FIXME: ensure we account for https://github.com/openscad/openscad/issues/2651 +-- FIXME: handle upper case files. + +-- FIXME: support binary STL reading / writing. +{- from the OpenSCAD folks: +15:49 < InPhase> juri_: The binary standard I followed came from this official standards documenting body: https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL ;) +15:52 < InPhase> juri_: For example, I followed the "assumed to be little-endian, although this is not stated in documentation", enforcing that with a conversion in the event OpenSCAD is used on a big-endian system. +15:53 < InPhase> Such a constraint seems essential or else it cannot function as a document exchange format. +-} + +-- | produce a list of Triangles from the input STL file. +trianglesFromSTL :: Fastℕ -> ByteString -> [Triangle] +trianglesFromSTL threads stl = [readTriangle f | f <- rawTrianglesFromSTL strippedStl] `using` parBuffer (fromFastℕ threads) rdeepseq + where + -- strip the first line header off of the stl file. + (_, headStrippedStl) = break (== '\n') stl + -- and the last line terminator off of the stl file. + (strippedStl, _) = breakSubstring "endsolid" headStrippedStl + +-- | Separate the STL file into triangles +rawTrianglesFromSTL :: ByteString -> [ByteString] +rawTrianglesFromSTL l = if null l then [] else f : rawTrianglesFromSTL (drop 1 remainder) + where (f, r) = breakSubstring "endfacet" l + (_ , remainder) = break (=='\n') r + +-- | Read a point when it's given as a string of the form "vertex x y z". +-- Skip any other line. +readVertex :: ByteString -> Maybe ℝ3 +readVertex s = readVertex' $ words s + where + readVertex' :: [ByteString] -> Maybe ℝ3 + readVertex' [vertex,xs,ys,zs] + | vertex == "vertex" = case (readMaybe $ unpack xs, readMaybe $ unpack ys, readMaybe $ unpack zs) of + (Just x, Just y, Just z) -> Just $ V3 x y z + (_maybex,_maybey,_maybez) -> error "error reading." + readVertex' _ = Nothing + +-- | Read a list of three vertexes and generate a triangle from them. +readTriangle :: ByteString -> Triangle +readTriangle f = do + let + points = readVertex <$> lines f + foundPoints = catMaybes points + triangleFromPoints :: ℝ3 -> ℝ3 -> ℝ3 -> Triangle + triangleFromPoints p1 p2 p3 = (p1,p2,p3) + case foundPoints of + [] -> error $ "no points found" <> show f <> "\n" + [p1,p2,p3] -> triangleFromPoints p1 p2 p3 + (_a:_b) -> error $ "wrong number of points found." <> show f <> "\n" <> show foundPoints <> "\n" + diff --git a/Makefile b/Makefile index e10ac901..8eb658e1 100644 --- a/Makefile +++ b/Makefile @@ -152,6 +152,7 @@ dist: $(TARGETS) # Generate examples. examples: $(EXTOPENSCADBIN) + cd Examples && echo ../$(EXTOPENSCADBIN) $(SCADOPTS) example3.escad -f asciistl $(RTSOPTS) && ../$(EXTOPENSCADBIN) $(SCADOPTS) example3.escad -f asciistl $(RTSOPTS) cd Examples && for each in `find ./ -name '*scad' -type f | sort`; do { echo ../$(EXTOPENSCADBIN) $(SCADOPTS) $$each $(RTSOPTS); ../$(EXTOPENSCADBIN) $(SCADOPTS) $$each $(RTSOPTS); } done # NOTE: on debian, if this fails to find the linear package, run: 'apt install libghc-linear-dev libghc-show-combinators-dev libghc-blaze-svg-dev libghc-data-default-dev libghc-juicypixels-dev' cd Examples && for each in `find ./ -name '*.hs' -type f | sort`; do { filename=$(basename "$$each"); filename="$${filename%.*}"; cd ..; $(GHC) $(EXAMPLEOPTS) Examples/$$filename.hs -o Examples/$$filename; cd Examples; echo $$filename; $$filename +RTS -t ; } done diff --git a/implicit.cabal b/implicit.cabal index 9ff56c46..3659d8c1 100644 --- a/implicit.cabal +++ b/implicit.cabal @@ -149,6 +149,8 @@ Library Graphics.Implicit.Export.Resolution -- Exposed for in-argparser tests. Graphics.Implicit.ExtOpenScad.Util.ArgParser + -- Exposed for HSlice. + Graphics.Implicit.Import.Definitions Other-modules: Graphics.Implicit.FastIntUtil Graphics.Implicit.TriUtil