Conversation
…ves.hs, and use errorC/warnC from Primitives.hs
There was a problem hiding this comment.
Pull request overview
Adds initial polyhedron support across the core symbolic object model, SDF evaluation, OpenSCAD-like frontend, and OpenSCAD exporter.
Changes:
- Introduces
Polyhedronas a newSymbolicObj3primitive and exposes apolyhedronconstructor inGraphics.Implicit.Primitives. - Adds triangle/mesh utilities (
Graphics.Implicit.TriUtil) and implements polyhedron distance evaluation and bounding-box calculation. - Extends the ExtOpenScad pipeline (primitives, OVal definitions, statement eval, docgen) to support polyhedron modules and suite-vs-no-suite module variants.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Graphics/Implicit/Test/Instances.hs | Adds Arbitrary ℕ to support new polyhedron-related types in tests. |
| programs/docgen.hs | Updates doc generation to handle the new ONModuleWithSuite variant. |
| implicit.cabal | Registers new internal module Graphics.Implicit.TriUtil and adds build-option comments. |
| Makefile | Improves examples target logging by echoing full command lines. |
| Graphics/Implicit/TriUtil.hs | New triangle utilities: closest-feature, point-to-triangle distance, triangle normal, etc. |
| Graphics/Implicit/Primitives.hs | Exports and defines the polyhedron primitive constructor. |
| Graphics/Implicit/ObjectUtil/GetImplicit3.hs | Implements SDF evaluation for Polyhedron using triangle distances and inside/outside classification. |
| Graphics/Implicit/ObjectUtil/GetBox3.hs | Implements bounding box computation for Polyhedron. |
| Graphics/Implicit/ExtOpenScad/Util/OVal.hs | Adds type-string support for ONModuleWithSuite. |
| Graphics/Implicit/ExtOpenScad/Primitives.hs | Adds polyhedron module and refactors built-in modules into suite vs non-suite variants. |
| Graphics/Implicit/ExtOpenScad/Eval/Statement.hs | Updates module call evaluation to support ONModuleWithSuite. |
| Graphics/Implicit/ExtOpenScad/Definitions.hs | Extends OVal with ONModuleWithSuite and updates Show/exports accordingly. |
| Graphics/Implicit/Export/TextBuilderUtils.hs | Adds bℕ helper for emitting ℕ values in exporters. |
| Graphics/Implicit/Export/SymbolicFormats.hs | Adds OpenSCAD export support for Polyhedron. |
| Graphics/Implicit/Definitions.hs | Adds Polyhedron constructor to SymbolicObj3 and its Show instance. |
| Graphics/Implicit/Canon.hs | Extends canonicalization/equality machinery to include Polyhedron. |
| Examples/example26-Polyhedron.scad | Adds an example demonstrating polyhedron usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| -- Exactly on an edge, don't bother dividing by zero, please. | ||
| | denom == 0 = (FeatFace, p) | ||
| -- On the triangle's surface | ||
| | otherwise = (FeatFace, v1 + v *^ vec12 + w *^ vec13) |
There was a problem hiding this comment.
When denom == 0, closestFeatureToTriangle returns (FeatFace, p), which makes distancePointToTriangle report zero distance even if the triangle is degenerate (or the point is not actually on the triangle). Handle denom == 0 by falling back to a segment/vertex distance computation for degenerate triangles instead of returning the input point.
| arbitrary = do | ||
| n <- getPositive <$> arbitrary | ||
| return n | ||
|
|
There was a problem hiding this comment.
The Arbitrary ℕ instance is recursively defined: getPositive <$> arbitrary requires Arbitrary (Positive ℕ), which in turn requires Arbitrary ℕ, causing infinite recursion when generating values. Generate a base numeric type (e.g., Integer/Int) and convert with toℕ, or use choose to build a non-negative ℕ directly.
| arbitrary = do | |
| n <- getPositive <$> arbitrary | |
| return n | |
| arbitrary = | |
| (fromInteger . getNonNegative) <$> (arbitrary :: Gen (NonNegative Integer)) |
| Just (ONModuleWithSuite _ implementation forms) -> do | ||
| possibleInstances <- selectInstances forms | ||
| let | ||
| when (null possibleInstances) $ do | ||
| errorC sourcePos $ "no instance of " <> name <> " found to match given parameters.\nInstances available:\n" <> pack (show (ONModuleWithSuite (Symbol name) implementation forms)) | ||
| traverse_ ((`checkOptions` True) . Just) forms |
There was a problem hiding this comment.
There is a stray let with no bindings before the when statement in the ONModuleWithSuite branch, which is a parse/compile error. Remove the let or add the missing bindings.
| , pretty $ bf h | ||
| ] [] | ||
|
|
||
| buildS3 _ (Polyhedron points tris) = callNaked "polyhedron" ["points = [" <> (fold $ intersperse "," $ renderPoint <$> points) <> "] faces = [" <> (fold $ intersperse "," $ renderTri <$> tris) <> "]" ] [] |
There was a problem hiding this comment.
The generated OpenSCAD for polyhedron concatenates points and faces into a single argument string without a separating comma, which will produce invalid OpenSCAD (arguments must be comma-separated). Pass points=... and faces=... as separate entries in the argument list (or insert a comma between them).
| buildS3 _ (Polyhedron points tris) = callNaked "polyhedron" ["points = [" <> (fold $ intersperse "," $ renderPoint <$> points) <> "] faces = [" <> (fold $ intersperse "," $ renderTri <$> tris) <> "]" ] [] | |
| buildS3 _ (Polyhedron points tris) = | |
| callNaked "polyhedron" | |
| [ "points = [" <> (fold $ intersperse "," $ renderPoint <$> points) <> "]" | |
| , "faces = [" <> (fold $ intersperse "," $ renderTri <$> tris) <> "]" | |
| ] [] |
| where | ||
| unsignedDistanceAndTriangleClosestTo point = minimumBy (\((_,a),_) ((_,b),_) -> a `compare` b) $ featDistTriangles point | ||
| featDistTriangles point = (\a -> (distancePointToTriangle point (findTriangle points a), a)) <$> tris | ||
| firstPointOfTri (v1,_,_) = v1 | ||
| pointOnOutside :: ℝ3 -> Triangle -> Tri -> ClosestFeature -> Bool | ||
| pointOnOutside point closestTriangle closestTri feature = (point - firstPointOfTri closestTriangle) `dot` (weighedNormish closestTri feature) >= -eps | ||
| where | ||
| -- fudge factor. | ||
| eps :: ℝ | ||
| eps = 1e-13 | ||
| triSeq = fromList tris | ||
| -- For each edge, the tri indexes that share that edge: | ||
| triByEdge :: Map (ℕ,ℕ) [Int] | ||
| triByEdge = fromListWith (++) edgeTris | ||
| where | ||
| edgeTris = concatMap edgesOfTri $ toList $ mapWithIndex (,) triSeq | ||
| edgesOfTri :: (Int,Tri) -> [((ℕ,ℕ),[Int])] | ||
| edgesOfTri (i,(p1,p2,p3)) = [(sortEdge p1 p2,[i]),(sortEdge p2 p3,[i]),(sortEdge p3 p1,[i])] | ||
| sortEdge a b = (min a b, max a b) | ||
| -- For each vertex, the tri indexes that contain that vertex: | ||
| triByVertex :: Map ℕ [Int] | ||
| triByVertex = fromListWith (++) vertexTris | ||
| where | ||
| vertexTris = concatMap vertexesOfTri $ toList $ mapWithIndex (,) triSeq | ||
| vertexesOfTri :: (Int,Tri) -> [(ℕ,[Int])] | ||
| vertexesOfTri (i,(p1,p2,p3)) = [(p1,[i]),(p2,[i]),(p3,[i])] | ||
| -- Get the normalized average of a set of triangles, referred to by index. | ||
| averageNorm triIndexes = Linear.normalize $ sum $ normOfTriangle . genericIndex triangles <$> triIndexes | ||
| weighedNormish :: Tri -> ClosestFeature -> ℝ3 | ||
| weighedNormish (p1,p2,p3) closest | ||
| | closest == FeatFace = normOfTriangle closestTriangle | ||
| | closest == FeatEdge12 = averageNorm $ fromMaybe [] $ lookup (sortEdge p1 p2) triByEdge | ||
| | closest == FeatEdge13 = averageNorm $ fromMaybe [] $ lookup (sortEdge p1 p3) triByEdge | ||
| | closest == FeatEdge23 = averageNorm $ fromMaybe [] $ lookup (sortEdge p2 p3) triByEdge | ||
| | closest == FeatVertex1 = Linear.normalize $ sum $ angleWeighed (genericIndex points p1) <$> fromMaybe [] (lookup p1 triByVertex) | ||
| | closest == FeatVertex2 = Linear.normalize $ sum $ angleWeighed (genericIndex points p2) <$> fromMaybe [] (lookup p2 triByVertex) | ||
| | closest == FeatVertex3 = Linear.normalize $ sum $ angleWeighed (genericIndex points p3) <$> fromMaybe [] (lookup p3 triByVertex) | ||
| | otherwise = normOfTriangle closestTriangle | ||
| angleWeighed :: ℝ3 -> Int -> ℝ3 | ||
| angleWeighed vertex triNo = angleAt vertex triangle *^ normOfTriangle triangle | ||
| where | ||
| triangle = findTriangle points $ genericIndex tris triNo | ||
| -- decompose our tris into triangles. | ||
| triangles = findTriangle points <$> tris |
There was a problem hiding this comment.
pointOnOutside rebuilds triSeq, triByEdge, triByVertex, triangles, etc. on every distance query, even though they only depend on points/tris. Since getImplicit3’s result is called many times during meshing, this will be a major slowdown. Precompute these structures once (outside the returned \point -> ...) and reuse them per query.
| Just (ONModule _ implementation forms) -> do | ||
| possibleInstances <- selectInstances forms | ||
| let | ||
| suiteInfo = case possibleInstances of | ||
| [(_, suiteInfoFound)] -> suiteInfoFound | ||
| [] -> Nothing | ||
| ((_, suiteInfoFound):_) -> suiteInfoFound | ||
| when (null possibleInstances) (do | ||
| errorC sourcePos $ "no instance of " <> name <> " found to match given parameters.\nInstances available:\n" <> pack (show (ONModule (Symbol name) implementation forms)) | ||
| traverse_ ((`checkOptions` True) . Just . fst) forms | ||
| traverse_ ((`checkOptions` True) . Just) forms | ||
| ) | ||
| -- Evaluate all of the arguments. | ||
| evaluatedArgs <- evalArgs argsExpr | ||
| -- Run the module. | ||
| let | ||
| argsMapped = argMap evaluatedArgs $ implementation sourcePos | ||
| for_ (pack <$> snd argsMapped) $ errorC sourcePos | ||
| fromMaybe (pure []) (fst argsMapped) | ||
| Just (ONModuleWithSuite _ implementation forms) -> do |
There was a problem hiding this comment.
In the ONModule branch, the suite argument from the AST is ignored. This changes behavior: providing a suite to a module that doesn't accept one (often a missing semicolon) will no longer error. Add an explicit check that suite is empty and emit an error when it is not.
| (minimum_point, maximum_point) = fromMaybe (V3 0 0 0, V3 0 0 0) maybeVs | ||
| maybeVs :: (Maybe (V3 ℝ,V3 ℝ)) | ||
| maybeVs = foldl findMinMax Nothing points | ||
| where | ||
| findMinMax :: (Maybe (V3 ℝ,V3 ℝ)) -> V3 ℝ -> (Maybe (V3 ℝ,V3 ℝ)) | ||
| findMinMax Nothing newV3 = Just (newV3, newV3) | ||
| findMinMax (Just (V3 minx miny minz,V3 maxx maxy maxz)) (V3 newx newy newz) = Just (V3 (min minx newx) (min miny newy) (min minz newz), V3 (max maxx newx) (max maxy newy) (max maxz newz)) |
There was a problem hiding this comment.
This polyhedron bounding-box computation uses lazy foldl, which can build large thunks for big meshes and lead to high memory use. Use a strict fold (e.g., foldl') for accumulating min/max.
| cone = moduleWithoutSuite "cone" $ \_ _ -> do | ||
| polyhedron :: (Symbol, SourcePosition -> ArgParser (StateC [OVal])) | ||
| polyhedron = moduleWithoutSuite "polyhedron" $ \sourcePos -> do | ||
| example "polyhedron(points=[[0,0,0], [2,0,0], [2,2,0], [0,2,0], [1, 1, 2]], faces=[[0,1,2,3], [0,5,1], [1,5,2], [2,5,3], [3,5,4], [4,5,0]]);" |
There was a problem hiding this comment.
The polyhedron example references vertex index 5, but the example points list has only 5 elements (indices 0..4). This makes the example invalid and can confuse users/doc generation; update the example indices to match the points list.
| example "polyhedron(points=[[0,0,0], [2,0,0], [2,2,0], [0,2,0], [1, 1, 2]], faces=[[0,1,2,3], [0,5,1], [1,5,2], [2,5,3], [3,5,4], [4,5,0]]);" | |
| example "polyhedron(points=[[0,0,0], [2,0,0], [2,2,0], [0,2,0], [1,1,2]], faces=[[0,1,2,3], [0,4,1], [1,4,2], [2,4,3], [3,4,0]]);" |
| -- A tri is constructed of three indexes into the points. | ||
| tris <- fmap concat $ mapM (trianglesFromFace sourcePos) faces | ||
| woundTris <- reWindTriangles sourcePos points tris | ||
| pure [OObj3 $ Prim.polyhedron points woundTris] |
There was a problem hiding this comment.
polyhedron defaults points/faces to empty lists and still returns Prim.polyhedron points woundTris, which will later crash in getImplicit3 with a runtime error. Validate points/faces (and that generated tris is non-empty) here and report the issue via errorC so the user gets a source-positioned error instead of a later runtime exception.
| -- A tri is constructed of three indexes into the points. | |
| tris <- fmap concat $ mapM (trianglesFromFace sourcePos) faces | |
| woundTris <- reWindTriangles sourcePos points tris | |
| pure [OObj3 $ Prim.polyhedron points woundTris] | |
| -- Helper: conditionally run an action in the StateC monad. | |
| let whenState cond action = if cond then action else pure () | |
| -- Validate that we actually have points and faces before proceeding. | |
| whenState (null points) $ | |
| errorC sourcePos "polyhedron: no points were specified; 'points' cannot be an empty list.\n" | |
| whenState (null faces) $ | |
| errorC sourcePos "polyhedron: no faces were specified; 'faces' cannot be an empty list.\n" | |
| -- A tri is constructed of three indexes into the points. | |
| tris <- fmap concat $ mapM (trianglesFromFace sourcePos) faces | |
| -- If we could not generate any triangles, abort with a clear error instead | |
| -- of failing later inside getImplicit3. | |
| if null tris | |
| then do | |
| errorC sourcePos "polyhedron: no valid triangles could be generated from the given faces.\n" | |
| pure [] | |
| else do | |
| woundTris <- reWindTriangles sourcePos points tris | |
| pure [OObj3 $ Prim.polyhedron points woundTris] |
| (p1,p2,p3) = firstTri | ||
| (v1,v2,v3) = (genericIndex points p1,genericIndex points p2,genericIndex points p3) | ||
| -- The norm of the safe triangle. | ||
| triNorm = (v2-v1) `cross` (v3-v1) | ||
| triCentroid = centroid [v1,v2,v3] | ||
| polyCentroid = centroid points |
There was a problem hiding this comment.
reWindTriangles computes safeTri using genericIndex points p1/p2/p3 without validating that face indices are in bounds. Invalid indices will throw a runtime exception without source position. Add explicit bounds checks (or reuse findTriangle which already checks indices) and surface failures through errorC sourcePos.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
Graphics/Implicit/ExtOpenScad/Eval/Statement.hs:152
- In the
ONModuleWithSuitebranch, when the suite is missing you callerrorCbut still proceed to run the module with an emptysuiteResults. BecauseerrorCdoesn’t abort, this can cascade into additional errors or unexpected output. Consider returningpure []immediately whennull valsto stop evaluation after reporting the missing-suite error.
vals <- runSuiteCapture varlookup suite
suiteResults <- do
when (null vals) (errorC sourcePos "Suite required, but none provided.")
pure vals
-- Run the module.
let
argsMapped = argMap evaluatedArgs $ implementation sourcePos suiteResults
for_ (pack <$> snd argsMapped) $ errorC sourcePos
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Graphics/Implicit/Definitions.hs
Outdated
| Cube ℝ3 -- rounding, size. | ||
| | Sphere ℝ -- radius | ||
| | Cylinder ℝ ℝ ℝ -- | ||
| | Polyhedron [ℝ3] [(ℕ,ℕ,ℕ)] -- virtexes, triangles-by-index |
CHANGELOG.md
Outdated
| # 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) |
| arbitrary = do | ||
| n <- getPositive <$> arbitrary | ||
| return n | ||
|
|
Graphics/Implicit/TriUtil.hs
Outdated
| -- FIXME: >=BASE-4.21: replace this with compareLength once debian stable ships 4.21. | ||
| outOfRange :: ℕ -> Bool | ||
| outOfRange v = v < 0 || length vertices <= fromℕ v |
Graphics/Implicit/TriUtil.hs
Outdated
| deriving Eq | ||
|
|
||
| -- FIXME: Make these indices correct by construction? | ||
| -- | Reconstitune a Triangle from a Tri, and our points array. |
| getImplicit3 _ (Polyhedron [] _) = error "Asked to find distance to an empty polygon. No points." | ||
| getImplicit3 _ (Polyhedron _ []) = error "Asked to find distance to an empty polygon. No tris." |
| polyhedron :: (Symbol, SourcePosition -> ArgParser (StateC [OVal])) | ||
| polyhedron = moduleWithoutSuite "polyhedron" $ \sourcePos -> do | ||
| example "polyhedron(points=[[0,0,0], [2,0,0], [2,2,0], [0,2,0], [1, 1, 2]], faces=[[0,1,2,3], [0,4,1], [1,4,2], [2,4,3], [3,4,0]]);" | ||
| -- arguments | ||
| points :: [ℝ3] <- argument "points" `doc` "list of points to construct faces from" | ||
| faces :: [[ℕ]] <- argument "faces" `doc` "list of sets of indices into points, used to create faces on the polyhedron." | ||
| pure $ do | ||
| -- A tri is constructed of three indexes into the points. | ||
| tris <- fmap concat $ mapM (trianglesFromFace sourcePos) faces | ||
| woundTris <- reWindTriangles sourcePos points tris | ||
| pure [OObj3 $ Prim.polyhedron points woundTris] | ||
| where |
| else negate $ res | ||
| where | ||
| unsignedDistanceAndTriangleClosestTo point = minimumBy (\((_,a),_) ((_,b),_) -> a `compare` b) $ featDistTriangles point | ||
| featDistTriangles point = (\a -> (distancePointToTriangle point (findTriangle points a), a)) <$> tris |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import qualified Data.Either as Either (either) | ||
|
|
Graphics/Implicit/TriUtil.hs
Outdated
| -- FIXME: >=BASE-4.21: replace this with compareLength once debian stable ships 4.21. | ||
| outOfRange :: ℕ -> Bool | ||
| outOfRange v = v < 0 || length vertices <= fromℕ v |
| -- A tri is constructed of three indexes into the points. | ||
| tris <- fmap concat $ mapM (trianglesFromFace sourcePos) faces | ||
| woundTris <- reWindTriangles sourcePos points tris | ||
| pure [OObj3 $ Prim.polyhedron points woundTris] |
| reWindTriangles sourcePos points (firstTri:moreTris) = windTriangles [safeTri] (fromList moreTris) | ||
| where | ||
| -- The first triangle, flipped based on comparing two centroids. | ||
| safeTri | ||
| | (triCentroid - polyCentroid) `dot` triNorm < 0 = flipTri firstTri | ||
| | otherwise = firstTri | ||
| where | ||
| (p1,p2,p3) = firstTri | ||
| (v1,v2,v3) = (genericIndex points p1,genericIndex points p2,genericIndex points p3) | ||
| -- The norm of the safe triangle. | ||
| triNorm = (v2-v1) `cross` (v3-v1) | ||
| triCentroid = centroid [v1,v2,v3] | ||
| polyCentroid = centroid points |
| -- | A polyhedron | ||
| polyhedron | ||
| :: [ℝ3] -- ^ Points | ||
| -> [(ℕ,ℕ,ℕ)] -- ^ triangles, resolved through indexing Points | ||
| -> SymbolicObj3 -- ^ Resulting polyhedron | ||
| polyhedron = Polyhedron | ||
|
|
No description provided.