jscad / OpenJSCAD.org

JSCAD is an open source set of modular, browser and command line tools for creating parametric 2D and 3D designs with JavaScript code. It provides a quick, precise and reproducible method for generating 3D models, and is especially useful for 3D printing applications.
https://openjscad.xyz/
MIT License
2.58k stars 505 forks source link

feat(modeling): input array structure #1299

Closed platypii closed 6 months ago

platypii commented 9 months ago

This PR contains my suggestions for JSCAD v3 handling of input arrays. Some of these are breaking changes from V2. I tried to follow a couple principles:

  1. Avoid throws unless the operation would be clearly invalid
  2. Preserve input structure when possible, don't flatten unless we need to. This is nice for grouping objects, and also preserves information when composing operations.
  3. Accept nested arrays anywhere a single geometry is accepted. So anywhere a user could pass a geometry, should also accept nested arrays of geometries.
  4. Make choices that give users more control. If there's two choices for how we define an API, but one way gives users more flexibility, then do that. See align below.
Before this PR: returns Input arrays Mix 2d 3d Empty input null/undefined Invalid geom
Booleans, extrusions, hulls, offsets Geometry flatten throw [] filtered throw*
modifiers Geometry[] flatten apply to each [] throw throw*
transforms Geometry[] flatten apply to each return input copy to output throw*

*throws for obviously incorrect geometries, but will incorrectly flatten and return some things like [[1,2],[3,4]]

After this PR: returns Input arrays Mix 2d 3d Empty input null/undefined Invalid geom
Booleans, extrusions, hulls, offsets Geometry flatten throw [] filtered throw
modifiers RecursiveArray Preserve input structure apply to each return input copy to output throw
transforms RecursiveArray Preserve input structure apply to each return input copy to output throw

Nested Arrays as Groups

I suggest that we preserve nested arrays where it makes sense to do so. This information can be treated like “groups” in svg and other file formats. For operations which return a single geometry, like the booleans, we should still flatten before we apply the operation, but if it makes sense to do so, I propose that we preserve the recursive structure as much as possible.

null/undefined

I suggest that we filter out null-ish values when we do a flatten operation for booleans, and most operations. To make working with nested arrays easier, I added a new util function coalesce which combines flatten and filtering out nullish values.

export function coalesce<T>(arr: RecursiveArray<T>): Array<T>

Empty arrays

What if a user calls a boolean operation with an empty array, or an array which is empty when flattened? We have three options 1) throw error, but I would prefer not to do this 2) return geom2.create() or geom3.create(), but the problem is that we don’t actually know if it’s intended to be 2D or 3D. I propose that in this case we return undefined, which can be filtered out by other operations using coalesce.

Mix 2D/3D

If you call a boolean between a 2D and 3D geometry, it’s not clear what to return in that case, so we should throw if not all types are the same. However for transforms, and modifiers, it’s okay to mix 2D and 3D. I propose that we apply functions where it makes sense. If an object is nullish, or not a geometry, I suggest we simply map the object to itself and return it in the output. This makes the operation robust against unexpected inputs.

Alignment

One other suggestion is that I believe that align and center operations should respect groupings, and use them when aligning objects rather than flattening. See suggestion from @hjfreyer in #1249. The reason I like this change is that it gives more flexibility to users. In V2, inputs will be flattened regardless of the user's intent. Whereas with this change, users can align groups of objects. The previous behavior can be accessed by setting options.grouped = true.

Examples

const g3 = h3 = cube()
const g2 = h2 = square()

generalize({}, g3) => out3
generalize({}, g2, g3) => [out2, out3]
generalize({}, [[g2], g3]) => [[out2], out3]
generalize({}, []) => []

offset({}, g3) => out3
offset({}, g2, g3) => [out2, out3]
offset({}, [[g2], g3]) => [[out2], out3]
offset({}, []) => []

union(g3) => g3
union(g2) => g2
union(g2, null) => g2
union(g2, undefined) => g2
union([g2, undefined, null]) => g2
union(g3, h3) => out3
union(g2, h2) => out2
union([[g2], h2]) => out2
union(g2, g3) => throw “union arguments must be the same geometry type”
union([]) => undefined

scission(g3, h3) => [[g3a, g3b], h3]

All Submissions:

z3dev commented 9 months ago

@platypii sorry for the lack of a review. hopefully, i can play with the new style this weekend.

platypii commented 8 months ago

Just to add a bit more justification for this PR:

One of my goals is to improve things like SVG serialize/deserialize. I want to be able to convert SVG groups into nested sub-arrays of geometries. And then it would be really nice if information was not lost along the way. So for example: 1) Load geometry from SVG, 2) Apply transform, or extrude, or offset, or align, etc to the nested geometry, 3) Re-export to SVG while still preserving the nested group structure.

Currently that information is lost when any operation is applied to a design because arrays get flattened. This PR attempts to preserve that structure where possible.

z3dev commented 7 months ago

@platypii Can you finish these changes? It would be nice to get these into V3.

platypii commented 7 months ago

@z3dev updated PR based on feedback, and added a few more tests to try and make things clearer. Thanks!