-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Showing and hiding geometry (construction geometry, assemblies) #18
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
# Showing and hiding geometry (construction geometry, assemblies) | ||
|
||
I propose an 'implicit show' syntax (with an explicit version as an alternative), where geometry which is consumed (by assigning into a variable, passing to a function, etc.) is treated as construction geometry and geometry which escapes to the top-level of a module is rendered (or becomes part of an assembly). | ||
|
||
## Background and motivation | ||
|
||
Most CAD software has the concept of *construction geometry* which is geometry used for constructing other geometry but which is not itself rendered. We currently don't have explicit support for this in KittyCAD, but the engine does make some choices about which geometry is rendered. In KCL, non-rendered geometry might take other forms too, e.g., geometry in functions or in imported modules. It's also likely to be used in many places, e.g., creating a single instance of an object and then using a pattern to create multiple instances, where we don't want to render the original instance. | ||
|
||
One particular question to answer is how geometry should work with imported modules (which I envisage to be the way that KCL supports assemblies). To recap the current and [proposed](https://github.com/KittyCAD/kcl-experiments/pull/12) syntax: | ||
|
||
- modules are declared in a separate file | ||
- exported functions of a file can be imported using `import foo from 'bar.kcl'` | ||
- the module itself (i.e., the geometry defined at the top-level of the file) can be imported using `import 'bar.kcl'` (creates a single object bound to the variable `bar`, which should be able to be used as an assembly) | ||
|
||
Currently, objects are effectively rendered by virtue of the side effects of execution (issuing API calls to the engine). Therefore, exactly how all the above might work is a bit vague. I'll spell some of it out here, focussing on the user-facing syntax. More of the fundamentals are discussed in the [foundations design doc](foundations.md), including side-effects. | ||
|
||
|
||
## Proposal: implicit `show` | ||
|
||
The basics of the proposal is that an object that is defined at the top level is rendered by the engine unless it is passed into a function, used in a pipeline, or assigned into a variable (or used in some other way we add to KCL in the future). Using the result of a function or a variable at the top level will render it. E.g., | ||
|
||
``` | ||
// Result of function call, rendered | ||
makeSphere() | ||
|
||
// Result of pipeline, rendered (nothing intermediate in the pipeline is rendered) | ||
startSketchOn(...) | ||
|> startPath() | ||
|> ... | ||
|> close() | ||
|> extrude() | ||
|
||
// As above, but assigned into a variable, nothing is rendered | ||
object = startSketchOn(...) | ||
|> startPath() | ||
|> ... | ||
|> close() | ||
|> extrude() | ||
|
||
// Variable is used, result of pipeline is rendered | ||
object | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if you have:
Is one thing displayed or two? Displaying two things at the same location is not useful. But if we need to keep track of what has been displayed, that may dictate how it's implemented. (And without thinking through it fully, my guess is it would be more complicated.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just noting that even if we have an explicit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good question. I think this is up to the engine rather than KCL really (although of course there is the question of what we send the engine). I'm also not sure if there is a difference between displaying twice or once? If it's not named or tagged, then it can't be referred to and if it's at exactly the same spot, the image shouldn't change. So I would argue that how many actual draw calls are made is just an optimisation that cannot be observed. In other words, KCL should not track what has been displayed |
||
``` | ||
|
||
Exactly how this is implemented with respect to the engine is left to the [foundations design doc](foundations.md), however, whether something is rendered or not must (I think) be explicit in the API calls. | ||
|
||
When a function is declared, nothing is rendered (if an entity escapes usage into the top-level of the function, then there should be an error or warning). When a function is called, the result returned by the function follows the usage rules as described above. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This seems like a pretty important point that should be front and center of the design. It means that if you have code at the top level, and you factor it out into a function, the perfectly good code now has errors or warnings. I generally don't like that code cannot be mechanically factored out in this way. There's some discussion below in the GUI section about showing construction geometry. These seem related to me. I think we all agree that it's a bad idea to render inside functions. But preventing this completely is very detrimental to users debugging. If you can't show geometry in a function, it's like in Haskell when you have a pure function and you can't debug-print. It can be extremely frustrating as a user. You basically have to rewrite all your code to wire it up to return all the way to the top level, or in the case of Haskell, be in the IO monad. At one point, I proposed a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I think mechanical refactoring of code into functions is a good guideline, but not an absolute principle. Consider Java for example, in general you can copy and paste into a function, but it's not 100% straightforward because you might reference variables which are out of scope (the IDE can make these into arguments, so mechanical, but not trivial). Then consider Rust, the same thing is mostly true but actually due to the borrowing rules there are times when it is not possible to do this, so not only not trivial but also not possible (and not even possible to predict without a very complete model of the borrow checker). So, in KCL we can basically copy and paste, but we are going to need to identify arguments, and we are also going to have to insert a Partly because (and addressing your second para) there is not a straightforward way to render geometry in functions, consider Anyway, afaict, there is not good way to render geometry in a function and we shouldn't try to do so by default, i.e., an error is the right thing to do. We probably should have some support in the UI for this (e.g., the user selects the geometry in the function and the UI can suggest values for missing variables and the user can manipulate them for debugging). |
||
|
||
When a file is imported as a module, all geometry which would be rendered is collected into the imported assembly (that is `bar` in `import 'bar.kcl'`). Anything else is not directly imported into the assembly, though it may be used indirectly, or if it is `export`ed, then it may be individually imported. E.g., if the above example code were imported, the resulting assembly would contain the result of `makeSphere()`, the first pipeline, and via `object`, the second pipeline. | ||
|
||
Since imported code is treated as immutable, geometry in modules cannot be changed from shown/hidden without editing the file directly. | ||
|
||
Naming of objects within assemblies is covered in the [modules design doc](modules.md). | ||
|
||
Implicit `show` would be mostly backwards compatible, I believe. Where there are changes, they would be a bit confusing, but I believe that we would require these changes in any case to have a better model of side-effects in the language. | ||
|
||
### GUI | ||
|
||
This proposal focusses on KCL, not the UI, so I won't get too deep into things, but I have the following recommendations/expectations for the UI: | ||
|
||
- By default non-rendered geometry is not rendered (surprise!) | ||
- There is some UI to show all construction geometry or a specific construction object rendered as a dashed wireframe, this does not affect the KCL program | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we decided it wasn't part of the code and only the UI, then we're accepting that the CLI and Text-to-CAD cannot use it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are many properties of viewing the scene which are not reflected in the code (e.g., camera angle, zoom, highlighting geometry when selected, etc). Rendering of construction geometry is just one more of those. The CLI and T2C can still use it, just not control how it is viewed |
||
- this might be implemented as a check box in the margin of the KCL code and/or in some object summary, as well as in a context menu, command, etc. | ||
- There is some UI to properly show/hide geometry, which is a simple code mod: | ||
- To hide geometry, insert a dummy variable, e.g., `makeSphere()` becomes `object003 = makeSphere()` or remove (or comment out) the use of a variable, | ||
- To show geometry, remove a variable if the variable is not used elsewhere, e.g., `object003 = makeSphere()` becomes `makeSphere()`, or use the variable at the top-level if it is, e.g., `object003 = makeSphere()` becomes `object003 = makeSphere(); object003` | ||
- I expect that construction geometry in assemblies is handled differently, future work... | ||
|
||
A note on a previous proposal: checkboxes in the source code editor should not be the primary mechanism for showing/hiding geometry in a permanent way, since it blurs the code/UI distinction and would not have a permanent representation in the source code. | ||
|
||
## Alternatives | ||
|
||
### Explicit `show` | ||
|
||
An alternative is to use a `show` keyword (or some other keyword or sigil). The rules as above remain basically the same, in particular with respect to modules and assemblies. However, rendering an object requires `show`. We have a 'not used, but not shown' state which is a little ambiguous and should probably be an error or warning. E.g., | ||
|
||
``` | ||
// Result of function call, not rendered, probably should be a warning | ||
makeSphere() | ||
// Rendered | ||
show makeSphere() | ||
|
||
// Result of pipeline, rendered (nothing intermediate in the pipeline is rendered) | ||
show startSketchOn(...) | ||
|> startPath() | ||
|> ... | ||
|> close() | ||
|> extrude() | ||
|
||
// Also rendered | ||
show object1 = startSketchOn(...) | ||
|> startPath() | ||
|> ... | ||
|> close() | ||
|> extrude() | ||
|
||
// Not rendered | ||
object2 = startSketchOn(...) | ||
|> startPath() | ||
|> ... | ||
|> close() | ||
|> extrude() | ||
|
||
// Variable is shown, result of pipeline is rendered | ||
show object2 | ||
``` | ||
|
||
This is more explicit which I think is both better (self-explanatory) and worse (more boilerplate for an extremely common concept). | ||
|
||
`show` in functions would be an error. | ||
|
||
`show` could be permitted in sub-expressions where `show expr` produces the result of `expr` and has the side effect of rendering it. E.g., `foo(show bar)` would render `bar` and pass it to `foo`. This is not possible with the implicit syntax (the work around is to use the variable either before or after as well as in the function call, easy enough here, but less ergonomic in the middle of a pipeline), but I'm not sure if this is a common use case. | ||
|
||
We could support both implicit and explicit `show` but I think that is a bad idea since it would be confusing and there is not much benefit. | ||
|
||
Explicit `show` would require many changes to existing code so is not as backwards compatible. | ||
|
||
### Explicit `hide` | ||
|
||
We could make geometry shown by default and require an explicit `hide` keyword. It is closer to existing CAD software though (where geometry is shown by default and the user opts-in to making it construction geometry). | ||
|
||
``` | ||
// Not rendered, not sure why you'd want to do this without assigning into a variable | ||
hide makeSphere() | ||
// Rendered | ||
makeSphere() | ||
|
||
// Result of pipeline, rendered (nothing intermediate in the pipeline is rendered) | ||
startSketchOn(...) | ||
|> startPath() | ||
|> ... | ||
|> close() | ||
|> extrude() | ||
|
||
// Also rendered | ||
object1 = startSketchOn(...) | ||
|> startPath() | ||
|> ... | ||
|> close() | ||
|> extrude() | ||
|
||
// Not rendered | ||
hide object2 = startSketchOn(...) | ||
|> startPath() | ||
|> ... | ||
|> close() | ||
|> extrude() | ||
|
||
// Variable is shown, result of pipeline is rendered | ||
object2 | ||
``` | ||
|
||
The first example here is why I think explicit `hide` is a bit weird and explicit `show` is better. | ||
|
||
### Property of objects | ||
|
||
When creating an object, whether it is to be used for construction or rendering could be included as a parameter. This is used in at least some other CAD software and is perhaps a good model for the engine API. However I don't think it is a good fit for KCL. Here, we might want to create construction geometry not just to reference from other geometry, but also as a template which is replicated and transformed. In the latter case, we want the original to be construction geometry and its clones to be rendered. That would require changing the construction/rendered property of the object during replication, but when exactly the user wants to do that is not trivial and we'd need some way to specify it as part of the replication/transformation functionality. | ||
|
||
More philosophically, I believe that whether an object is used for construction or rendered is not a property of the object itself, but of how the object is used. | ||
|
||
## Extensions | ||
|
||
### Leading underscores | ||
|
||
A variable name with a leading underscore (including just an underscore) can be declared but not used (using it is an error or warning, likewise not using a variable without a leading underscore should also be a warning). This makes it easy to temporarily hide geometry using `_ = ...`. This is useful because it means the UI or user does not need to create a name for the variable, but also it shows intention: that this variable is being used primarily to hide the geometry, not necessarily for reuse. | ||
|
||
### Implicit `return` | ||
|
||
When defining functions, we could allow eliding the `return` keyword in the return expression (final line) of the function body. This follows Rust, but note that Rust uses semicolons and KCL does not, so there is a different feel. | ||
|
||
This would make functions a bit more like the top-level of a module and I think that would be intuitive. It would also match the usual semantics for blocks in programming languages (and how I imagine they would work in KCL). However, one difference is that we should require explicit `return` on other lines of a function body, whereas at the top-level this is not needed. | ||
|
||
Not requiring explicit return elsewhere in the function would make it a bit easy to have weird bugs, although this could be addressed somewhat with syntax highlighting in the editor. It would also have different semantics to the top-level where we effectively return all values and continue executing, whereas in functions we return one value and terminate execution (we could allow multiple implicit returns as sugar for returning a tuple of value and explicit returns for early returns, but I think that is getting a bit weird). | ||
|
||
## Open questions | ||
|
||
### Sending construction geometry to the engine | ||
|
||
In theory, KCL should only send rendered geometry to the engine and construction geometry should only be used locally. However, that doesn't fit the current design. For example, we might create a line as construction geometry and then terminate a rendered line at the centre of the line. The engine would require knowledge of the construction line. We could send all geometry to the engine, labelled as construction/rendered or we could try to infer which geometry is required to be sent to the engine. I would hope the latter is purely an optimisation of the former, but haven't tried to prove that out. | ||
|
||
### Construction geometry within sketches | ||
|
||
E.g, (from [#1553](https://github.com/KittyCAD/modeling-app/issues/1553)): | ||
|
||
``` | ||
startSketchOn('XY') | ||
|> startProfileAt() | ||
|> line() | ||
|> line() | ||
|> construction(circle()) | ||
|> line() | ||
|> construction(line({from:[], to:[]})) | ||
``` | ||
|
||
Neither implicit nor explicit `show` address that very well. We could support a `construction` function specifically for use within sketches/pipelines. However, I think that fundamentally, construction geometry does not fit well with the ordering ethos of pipelining. I've been thinking of a block syntax for sketching which does not have an ordering, and in that case, construction geometry works similarly to functions. For example, the above example would become: | ||
|
||
``` | ||
sketch(on = XY) { | ||
l1 = line(...) | ||
l2 = line(...) | ||
cnstCircle = circle(...) | ||
cnstLine(...) | ||
l3 = line(...) | ||
enclose(l1, l2, l3) | ||
} | ||
``` | ||
Comment on lines
+196
to
+205
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have only ever used Construction Geometry with regards to sketches, so I'll reserve my comments for only sketches. I don't believe I've ever seen construction geometry for 3D objects, but it may just be a tool I've never used. The words "Construction Geometry" actually might be causing some miscommunication. I'm not positive. For sketches that use the construction (not rendered) lines, I'll use "References" (used in NX). Probably no surprise, but this code makes the most sense to me. The distribution of rendered entities vs reference entities is (guessing) 90/10. I'd rather the user be explicit about what needs to not be rendered, and having a |
||
|
||
As in the original example, three lines are rendered (the result of the `enclose` function) and a circle and one line are used for construction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we're misusing terminology.
If construction geometry weren't displayed in the 3D scene, point-and-click users wouldn't be able to use it. So that is a form of rendering, no?
The difference is that construction geometry shouldn't be exported as part of the glTF 3D geometry for use in other software. (Not to be confused with the KCL
export
keyword that allowsimport
, which is a completely different thing.)I propose the following terminology:
import
statement.The three things are orthogonal. "Construction geometry" is by definition not part-exported. But it says nothing of the other properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Furthermore, "rendered" should really specify whether you mean "rendered as normal geometry" or "rendered as construction geometry".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, "rendered" is a bit fast and loose. "rendered by default" might be better. There is some property of geometry which is "construction" or not and these two kinds are clearly treated differently. I think rendering and exporting in gITF or whatever follow from that more fundamental property (import/export in KCL is a bit different and I think mostly orthogonal to construction or not). So, most places I've written "rendered" should be read as "!construction".
As for actual rendering, my understanding is that rendering of construction geometry should be a property of viewing the scene, it can be turned on and off, and happens in a distinguished way (e.g., dashed wireframe). So it is not rendered in the same way as non-construction geometry. So yes it might be rendered, but also might not be, and definitely not in the same way as !construction geometry.