<kintel>
Has anyone ever noticed cgalpngtest_surface-png-image-tests (or similar) tests taking a long time? I'm investigating a performance issue which I'm still scratching the surface of, but this test passes on the CI in ~2 seconds, but takes 30 seconds on my laptop. Could be that I'm hitting a slow part in CGAL's arm64 implementation or smth., but any experiences from others would help
<kintel>
InPhase Huh, no, that renders almost instantly with the same features enabled
<kintel>
..so it must be related to GL rendering then. That's unexpected, but gives me somewhere to dig. thx!
<kintel>
That also explains why almost half of the runtime was system time
<kintel>
-> It's --enable=vertex-object-renderers that kills it.
<InPhase>
Well that's a narrower problem.
<kintel>
Perhaps this is how Apple delivers the final blow to OpenGL on macOS : /
<InPhase>
My logic behind the suggestion was "kintel is observant. If it happened in normal use he probably would have noticed it already." :)
<kintel>
heh, "normal" use for me these days is "ctest" ;)
<InPhase>
:)
teepee has quit [Remote host closed the connection]
teepee has joined #openscad
teepee has quit [Remote host closed the connection]
teepee has joined #openscad
<kintel>
Bedtime, but I found this: For some unfathomable reason, we call glBufferSubData() for each triangle in a single polyset
<kintel>
..and this call is a lot more expensive on the Apple's arm64 GL library than for x86_64. Not sure why, but it probably doesn't matter as this is not a great way of rendering..
<kintel>
We have a bunch of VBO-related features, perhaps time to read that VBO code in more detail :)
kintel has quit [Quit: My MacBook has gone to sleep. ZZZzzz…]
<guso78>
Hi, i was able to implement some of the topics which arose this night when i was away. even the header files are changed back.
<guso78>
is there any news on getting the python thing compiled on all platforms ?
<guso78>
does anybody have an idea how it could work and is there anything i could contribute with ?
L29Ah has left #openscad [#openscad]
castaway has joined #openscad
qwackyqwack has joined #openscad
qwackyqwack has quit [Quit: leaving]
qwackyqwack has joined #openscad
qwackyqwack has quit [Client Quit]
TheAssassin has quit [Ping timeout: 255 seconds]
TheAssassin has joined #openscad
teepee has quit [Quit: bye...]
teepee has joined #openscad
guso7838 has joined #openscad
teepee_ has joined #openscad
teepee has quit [Ping timeout: 255 seconds]
teepee_ is now known as teepee
TheAssassin has quit [Ping timeout: 255 seconds]
TheAssassin has joined #openscad
Guest9349 has joined #openscad
Guest9349 has quit [Client Quit]
L29Ah has joined #openscad
kintel has joined #openscad
<kintel>
guso78: Looks like it does technically build on all platforms, just need to make it build on all variants and make tests pass. Since you haven't started on Python-related tests yet, there is still tons of time to sort that out.
<peepsalot>
i thought i would try to remove usages of boost::format in favor of fmt, but damn this is opening a whole can of worms which means basically rewriting all the macros in printutils, and every usage of them
<InPhase>
peepsalot: Yeah, that's where I thought this was going to go when you first started talking about it.
<InPhase>
That's why I was bringing up the tangentially related things, like the _() that I had completely forgotten we had.
<InPhase>
If you do end up touching all those usages, perhaps aim to throw _() in everywhere you see it missing.
<peepsalot>
i am not sure how much I should actually change with this fmt stuff. like, do we want to use it to replace much of ostream usages too?
<InPhase>
Is it better?
<peepsalot>
i don't know. i'm confused about what is considered best practice with this. i mean, the api basically made it into c++20. its supposed to be very fast, but i don't know if those benchmark results require avoiding ostream
<peepsalot>
it has some interoperability with ostream, but its sort of hidden, like it will apparently make use of operator<<(ostream&, ...) behind the scenes, if defined for formatting user defined types, but it seems like there's no public API that directly interfaces with ostream
<peepsalot>
i find the documentation hard to follow, and have started to cross-check <format> docs on cppreference.com in attempt to understand the interface better
<peepsalot>
there's some stuff about type-erasure which I don't fully understand the significance of
<peepsalot>
there is a gitter set up, where i tried asking some questions, but only the dev responded once every day or two, and seemed to quickly get fed up, telling me to ask stackoverflow instead :-/
<peepsalot>
i guess i have to encapsulate our Value double variant in a class with its own formatter just to avoid printing "-nan", as that's not configurable
<InPhase>
Have you identified differences between it and the C++20 support?
<InPhase>
We can assume that in 2025 or so we'll probably want to bump to C++20 building. And I will note that 2025 is not nearly as far in the future as it sounds when you say it out loud. ;)
<InPhase>
And at that point it would be great if we could just swap it out and lose a dependency.
<InPhase>
So if there are opportunities to centralize any differences that would be great.
<peepsalot>
well, some of it, like <print> equivalent functions is even c++23
<InPhase>
If a straightforward swap out is possible upon going C++20, then I would argue this justifies a full replacement effort. It will make us C++20 ready ahead of time.
<InPhase>
This is work that would be done eventually, and if it gives some side-benefits to slip it in early, that's good.
<InPhase>
Sounds good except that I don't know what "grapheme clusterization" means.
<InPhase>
Is this a unicode thing?
<InPhase>
If so, I suppose that would not disrupt in any significant way.
<peepsalot>
yeah i guess so
<InPhase>
peepsalot: I therefore endorse that there are no signs you are doing something crazy. :)
<Scopeuk>
some searching suggests it is how unicode goes from underlying representation to char count, I guess it makes sense for anything under utf32 you will have a varying number of underlying utf8/urf16 entries for each character depending where in the char set you are
<Scopeuk>
possibly including compounding for singularly visible characters created by compounding.
* Scopeuk
remembers why he likes working on machine to machine and embded control stuff
milza has joined #openscad
<InPhase>
Scopeuk: Even utf32 will merge multiple utf32 entries to make a grapheme.
<InPhase>
And there are the zero-width ones.
<JordanBrown[m]>
I am not sure what exactly a grapheme is, but you can try playing with Arabic in OpenSCAD to see some related effects.
kintel has quit [Quit: My MacBook has gone to sleep. ZZZzzz…]
ur5us has joined #openscad
kintel has joined #openscad
<Scopeuk>
InPhase yes, I hadn't picked up the compounding subtlety until after the not utf32 remark
<kintel>
peepsalot: Perhaps it would be easier to explore fmt-related stuff if you made a stand-alone test program in e.g. https://github.com/openscad/dev-support
<kintel>
I assume that the print-related macros could all be pulled into a couple of files with very few other dependencies. That would also make it easier for others here to play around and help problem solving.
<kintel>
I agree that aligning with a future C++ standard is valuable; I'd be happy to chip in on this too
guso7838 has quit [Ping timeout: 260 seconds]
milza has quit [Quit: milza]
milza has joined #openscad
milza has quit [Client Quit]
ur5us has quit [Ping timeout: 260 seconds]
<InPhase>
kintel: In this case the future is fortunately the past. :) So we have C++20 well defined, just not well available.
<InPhase>
Oh, I see the std::print aligning with fmt::print is C++23. But probably we want to just centralize that with std::format anyway, to align console and gui printouts.
teepee has quit [Ping timeout: 255 seconds]
teepee has joined #openscad
<kintel>
yeah, the standard is already here :) ..but it could still be valuable to problem solve API usage outside of OpenSCAD to limit the scope of the investigation and make it easier to test stuff for others
guso7833 has joined #openscad
linext has joined #openscad
ur5us has joined #openscad
<InPhase>
kintel: Certainly. I routinely build up APIs like that externally, and find it a good speed-up for getting a clean result.
<InPhase>
The utility depends upon the problem complexity, but this might hit the threshold.
<guso7833>
kintel, i believe it could be a challenge, to get correct compiling environment to get the python compiled in windows. do you know which build chain matches best(msys2, mxe, MSVC ... ) ?
<teepee>
we may need to disable the build for windows first and then check if the msys2 solution could work
<teepee>
redoing *all* of the CI automation is not really realistic unless someone finds unlimited spare time
<kintel>
Doing all the final packaging for binary releases can indeed be a bunch of work, but that's really only necessary later in the process.
<teepee>
but we have no means to package it for release right now, the cmake stuff peepsalot did brings us quite a huge step hopefully but still there's no installer
<teepee>
and MXE looks like a huge problem when talking python integration
<teepee>
I don't see that as a huge problem though, it's not like we would take anything away. It would just mean windows might trail some time with getting the feature
<kintel>
guso7833 ..but since you mentioned you have some macOS packaging experience, that would also need some attention for packaging. I looked briefly at it, and Apple doesn't actually ship a Python C dev env.
<kintel>
..but I guess more important is the sandboxing story
<teepee>
in the long run, yes, definitely. but as initial feature I don't see it as the biggest problem
<guso7833>
Hi Kintel, you logs show me that MSYS2 build succeeded and i am impressed. I am wondering if this openscad.exe file will even work outside MSYS2 environment.
<guso7833>
it it possible to get a copy to test ?
<teepee>
not from the CI builds - at least not easily
<kintel>
I'm pretty sure it won't work outside, it's build for CI purposes only.
<teepee>
it's likely much easier to build via MSYS locally
<guso7833>
kintel, i am sure if it was me having packaing experience in macOS(i once seen that build succeeded)
<peepsalot>
guso7833: as I was saying the other day, we would need to run cpack on msys2 build and see if there is anything missing from the generated packages.
<kintel>
guso7833 Perhaps I read your comment a bit optimistically: quote "was developping software for apple before and it was always a hazzle when new OS's came out haha"
<guso7833>
peepsalot, i remember your message and i did try with cpack. the command succeded, but i was not sure with the result ...
<peepsalot>
guso7833: it should ahve resulted in two "packages" one a self installer .exe using NSIS and the other a simple zip file
<guso7833>
kintel yes i have written this, but this was something completely different. it was java jar files and even they suffered from new macos releases(snow leopart and all the others)
<kintel>
oh, ok, that's a different set of challenges.
<kintel>
As I always say: Making SW build is a lot harder than writing the SW itself :)
<teepee>
I would assume the macOS build links against the homebew python?
<kintel>
I think the CI image has the python.org version of Python installed, just judging from the install location
<teepee>
yep, and it's a process that needs to be continuously nursed
<teepee>
oh? that too? I'm seeing a python 3.11 from homebrew being installed if I'm reading the log correctly
<kintel>
Could be that homebrew installs into /Libraries/Frameworks/, just feels a bit odd
<teepee>
hmm, no, it then links to /usr/local/bin/python3 -> /Library/Frameworks/Python.framework/Versions/3.11/bin/python3
<teepee>
I'm confused, it also mentions 3.10 aswell
<kintel>
there are probably at least 3 python installations on that image, which all compete for attention depending on PATH : /
<teepee>
hmpf
<guso7833>
kintel, could you please elaborate on the tests needed for python ?
<peepsalot>
guso7833: so If the files were generated successfully from cpack, I would recommend testing the installer exe, (you will probably want change the default install path when given the option to e.g. "Program Files/Openscad_msys2" ) then try running the installed program
<kintel>
Re. testing: How do you know that your python implementation works?
<guso7833>
when adding the python interpreter, the functionality did not change, only the language to call them.
<kintel>
You still have written C++ code, which needs to be executed to prove it works.
<guso7833>
i could write a test program and have non-default values for all parameters and see if the resulting stl is correct
<teepee>
running test cases similar to the existing scad input should not be that difficult
<teepee>
there's quite a number of topics to tackle, maybe it's worth discussing some sort of priority
<guso7833>
peepsalot, i am little confused. i run cpack in the build dir, but this script does not have access to "Program Files".. does it ?
<kintel>
yeah, in terms of testing it's really about exercising all the logic in each of the python-related functions
<teepee>
from my perspective it would be worth it to get it into the hands of people even if it means it's for Linux only for a couple of month
<teepee>
who knows, it might even get someone interested helping with the dev stuff
<peepsalot>
guso7833: it should have resulted in two "packages" one a self installer .exe using NSIS and the other a simple zip file
<peepsalot>
guso7833: *running the installer exe* would need access to program files, and you should be prompted for that access
<guso7833>
from chip design i know that product are most successful when the code which writes the code is different from the person which writes the test. so you can minimze the chance of testing it in the same wrong way than implementing ...
<guso7833>
when the person which writes the code
<guso7833>
peepsalot, got it. will try again tomorrow
<guso7833>
and yes, i am aware, that i did not port *all* options in the existing functions haha
<guso7833>
e.g. rotating around a custom vectos is *not* coded yet
<kintel>
guso7833 Yeah, if we can get someone to volunteer writing tests, that would be awesome. In the meantime, writing tests tend to just be the less glorious aspect of implementing features : /
<guso7833>
i am just happy that i realized, teepee managed to also run the python next to me ...
<kintel>
-> my rule of thumb: Spend as much time testing as you spend implementing a feature
<teepee>
and I think the only way of maybe drawing other people in is by getting some basic version into the dev snapshots
<teepee>
it already has the experimental flag and the separation of code is much better with the latest version
<teepee>
so my proposal would be:
<guso7833>
haha, my first function to "port" to python was the cube, took me more than a day to get the interface translarions correct. much later translating "surface" took me 20 mins to get all compilation errors resolved
<teepee>
1) add compile time switch and enable for Linux only first
<JordanBrown[m]>
Somewhere in this process there should be a review of the proposed Python API. There are numerous ways that you could do CSG in Python; what's the best? As a trivial example, should translating a cube be translate([10,0,0], cube(1)) or should it be cube(1).translate([10,0,0])?
<teepee>
2) merge that and let people experiment with it
<teepee>
3) add test cases for at least a bunch of the core operations similar to the existing test framework
<teepee>
4) based on that go on with the other platforms
<teepee>
JordanBrown[m]: both work as far as I've seen
<guso7833>
jordan both options are corrrect. the 2nd one is object orientated and more easy to write ...
<teepee>
well, other way around for the parameters in translate()
<guso7833>
actually the obejct oriented version just calls the ordinary one . this is why am sure the functionaly is identically ...
<teepee>
although I've seen random crashes when using the 2nd way, I can't pinpoint what's happening yet
<guso7833>
teepee insteresting, would be awesome if you could extract 100% crashing cases haha
<JordanBrown[m]>
teepee should it be translate(obj, vec3), or translate(vec3, obj)? Should you be allowed to have multiple obj arguments with an implied union?
<teepee>
the current code has obj first
<guso7833>
the object should always go first, next is the parameters
<teepee>
like self
<guso7833>
dont plan to change that .
<JordanBrown[m]>
I'm not saying that any of these are right or wrong, or even (yet) giving an opinion on which is best, but it needs to be thought about.
<guso7833>
teepee knows my intent :)
<JordanBrown[m]>
If you can have two objects as arguments to translate, then I'd say that the vec3 goes first.
<JordanBrown[m]>
That would also be a more direct transliteration of OpenSCAD, to the extent that that's a consideration (which it probably mostly shouldn't be).
<guso7833>
jordan, you imply that you union them first right ?
<teepee>
guso7844: in your example code, changing the foot() from return translate(c, [x,y,0]) to return c.translate([x,y,0])
hrberg has joined #openscad
<teepee>
guso7844: first version runs without any issues it seems, after changing it, it crashes after a random number of F6 calls
<JordanBrown[m]>
If translate() is allowed to take multiple objects as arguments, then probably there's an implied union, just as there is for OpenSCAD-language.
<teepee>
which makes it sound like a memory corruption issue
<guso7833>
teepee in my example code i used both versions of translate intentionally
<guso7833>
to show that they exist
<guso7833>
jordan, translate in the normal form can only have exacly two arguments.
<guso7833>
if you want to translate multiple objects, you rather want to union them first
<teepee>
there should be the option to have a list of objects that are not unioned
<JordanBrown[m]>
I'm sure that your current implementation has particular behaviors. But are those the *best* behaviors, the ones that we want to live with for the next twenty years?
<teepee>
at least that would mirror the lazy union stuff which is not final either
<JordanBrown[m]>
And which has struck me as a very bad idea...
<guso7833>
jordan, i hope i managed to get the ball rolling with python and there is a working example implementation. i am confident that the community can optimize it if needed.
<JordanBrown[m]>
Yes... kind of.
<teepee>
that's not enough, no such ball will continue to roll
<kintel>
Another question: It's not uncommon to write code in OpenSCAD without first saving a file. Should we introduce some way of letting OpenSCAD know which language the design is written in? e.g. like glsl?
<teepee>
proof of which are all those dead projects on github, specifically on the topic openscad + python
<kintel>
Requiring a save is probably ok for now though
<JordanBrown[m]>
Once you get the ball rolling, it increasingly has inertia... and when an object has inertia, it's hard to change its direction.
<JordanBrown[m]>
Seems like there needs to be a per-tab "what language is this" switch.
<teepee>
there's no such thing in openscad at this time. the critical mass for that is way out
<guso7833>
right now the language is determined by the file suffix haha
<JordanBrown[m]>
and there is no file suffix until you save, as Marius says.
<teepee>
yeah, could be a switch on the tab, we already have a context menu there
<kintel>
otoh, it's not bad to let a piece of functionality live in experimental for a long time, until it gets more traction
<JordanBrown[m]>
Indeed, *today* Python-SCAD or whatever you want to call it has no inertia.
<guso7833>
when starting a new python i start openscad mydesign.py and then its clear from the beginning
<JordanBrown[m]>
But the day after it goes into the development snapshot it starts to gather inertia.
<kintel>
I still think sandboxing is important
<JordanBrown[m]>
After a few months, there are probably non-trivial projects based on it, and people will whine if it changes.
<JordanBrown[m]>
Yes, sandboxing is my biggest worry.
<kintel>
..otherwise, copy-pasting an OpenSCAD cmd-line can now root your computer
<teepee>
no, because it will not be enabled by default
<kintel>
..or actually: This is a GUI-only function, so we're safe from that attack vector now
<guso7833>
jordan right now there is the feature checkbox and clear implies the risk
<JordanBrown[m]>
Well, one hopes it can't root. That would be a bigger problem.
<JordanBrown[m]>
but it's also not all that useful until you routinely check that box.
<teepee>
we can decide on how annoying it should be at any time
<teepee>
but we can't solve Python problems in OpenSCAD
<JordanBrown[m]>
@kintel if that was in response to my "one hopes it can't root" comment... yes, it could do anything it likes *as you, an unprivileged user*. If an unprivileged user is able to do root stuff, then your system has other serious security problems.
<JordanBrown[m]>
We might, maybe, be able to solve this particular one.
<JordanBrown[m]>
For instance, if we disallow *any* file writes, then we've closed that hole.
<teepee>
no, once python is enabled we can put in some safeguards
<teepee>
you can't
<kintel>
JordanBrown[m] true, but it's more about expectations: When OpenSCAD is what you run, you expect no side-effects, but if you were to run Python instead, you might be a bit more careful
<teepee>
so don't claim you can
<JordanBrown[m]>
Why do you think you can't disallow writes?
<teepee>
because you can't once you have an active python interpreter
<teepee>
until python themselves provide a version that can be sandboxed
<JordanBrown[m]>
It might take a custom copy of the Python library, but somewhere down in its bowels is an "open(..., O_WRITE)", and if that call simply never allows O_WRITE, then there's no writes.
<teepee>
then someone comes and uses one of the monkey patch ways to just patch a myopen into the interpreter and you can still write files
<JordanBrown[m]>
@kintel I was reacting specifically to the "root" part of the comment. Yes, it's very bad if a Python-SCAD program can start scribbling on your home directory.
<JordanBrown[m]>
Yeah, it would take blocking the ability to access C library functions.
<teepee>
end of story - it's impossible
<JordanBrown[m]>
And admittedly if you go very far down that path, you've lost most of the libraries that were half of the point of the project.
<teepee>
yes, if you want to safely describe objects, use scad
guso7833 has quit [Ping timeout: 260 seconds]
guso78 has quit [Ping timeout: 260 seconds]
guso7844 has quit [Ping timeout: 260 seconds]
<kintel>
In terms of sandboxing, can we spawn a process, execute python in that process, and send back the OpenSCAD node via shared memory?
<kintel>
..then OS-level sandbox the python process?
<teepee>
if you want to communicate with the world, access databases, whatever, write files, you are writing a program and have to cope with the implications
<kintel>
^ Not too experienced with OS-level sandboxing; it may look different on each platform
<JordanBrown[m]>
that's a possibility. (Though I wouldn't use shared memory, I'd use a pipe.)
<teepee>
yes, there's probably solutions
<teepee>
but I don't see this as a realistic option with the dev power available
<JordanBrown[m]>
Yes, if you want full functionality then you are stuck with the implications. But that doesn't have to be all-or-nothing.
<kintel>
Pipe is tricky, but as long as the two processes uses the same architecture it should be fine
<JordanBrown[m]>
pipe is trickier than shared memory? :-)
<JordanBrown[m]>
I would just emit CSG text.
<kintel>
true, they should be equivalent, it really just a question of serialization
<teepee>
but making interop with scad parts very difficult
<kintel>
CSG text is indeed a very safe exchange format, and fully exchangable with an in-memory node tree
<JordanBrown[m]>
what kind of interop?
<teepee>
at some point I really would like to see a python main being able to use scad modules
<JordanBrown[m]>
that would be cool and is a whole new level of trouble.
<teepee>
it might be ok if the other way around is not there, not sure
<JordanBrown[m]>
If there was a Python mode available, I'd probably switch to 100% Python.
<JordanBrown[m]>
But in terms of the mix, my expectation would be that Python libraries with an OpenSCAD-language main would be the most interesting variation.
<JordanBrown[m]>
That way, for instance, a beginner could use the somewhat-simpler OpenSCAD language, and a library like BOSL2 could use Python, and the beginner could still use the library features without having to know Python.
<JordanBrown[m]>
But I don't think that level of interoperation is part of this project... right? What we're talking about right now is being able to use multiple languages, one at a time, to drive the OpenSCAD user interface and geometry processing.
<teepee>
I would not see that as included in the first step
<teepee>
but neither I see the security discussion as it's basically a lost cause with python anyway
<teepee>
I only see it important that we make sure there is a safe environment but that means disabling the python support
<teepee>
people use normal python all the time for anything, same security rating for python in openscad for the start
<JordanBrown[m]>
You are probably right about it being a lost cause. Not so much that it is intrinsically a lost cause, but that keeping it safe would require (a) deeply customizing the Python, and (b) lobotomizing it to the point where maybe it isn't interesting.
<JordanBrown[m]>
(Though... I would be happy to have a Python implementation with *just* the language, with *no* libraries, if *all* system call access was cut off.)
<JordanBrown[m]>
Having said that, I bet that, e.g., "import" is itself implemented in Python, and that would be hard to live without. But it still doesn't require file-write or network connections.
<teepee>
I assume that will happen at some point
<teepee>
I'm almost sure not in scope of the OpenSCAD project :)
<teepee>
just to bring up my main point, that the geometry engine and their features are much more important than the language
<teepee>
all that language discussion is 94.74% due to the geometry engine not supporting more powerful operations
<JordanBrown[m]>
I was following you up until that last.
<JordanBrown[m]>
People with huge investments in OpenSCAD-language projects might disagree with the "geometry engine is more important" comment, but I understand it.
<teepee>
I'm not talking about taking things away, so there's no problem with investment whatsoever
<JordanBrown[m]>
But nothing that you do to the geometry engine, absent turning it into a full language in its own right, will give the language associative arrays or the ability to process the mesh resulting from geometry.
<teepee>
giving for example better rounding options would just make things easier and/or faster going forward as option
<teepee>
you only need to process vertices if the geometry engine is not powerful enough
<JordanBrown[m]>
Eh, maybe. Absent turning the geometry engine into a full language, I'm not sure that I agree.
<teepee>
what's the main use cases for the powerful libraries?
<teepee>
path extrusion
<teepee>
chamfers
<teepee>
beziers
<teepee>
I'm totally not saying that would replace the libraries, they still would be extremely useful and needed
<JordanBrown[m]>
We're getting kind of philosophical here, but then the question is whether you can do those things without giving the geometry engine a full language in its own right.
<teepee>
but it would reduce the need for vertex processing inside the library a lot, maybe not 100%, but still by a huge amount
<JordanBrown[m]>
I absolutely agree that the more that the geometry engine does natively, the less that the libraries have to do. Where the "sweet spot" is... don't know. Whether we want to implement some of that stuff in C++, versus in a higher-level language... don't know.
<kintel>
Btw., related to this discussion, and re-reading some of the PR code: This PR really just exports a bunch of Python functions, and provide a way of building an OpenSCAD Node tree. It shouldn't be particularly hard to package src/core as a library and wrap all of that into a Python module, allowing pure Python programs to use OpenSCAD, without OpenSCAD as an overall process. That alone could have a lot of value. ..and may eventually enable people to
<kintel>
write OpenSCAD plugins to their favourite IDEs (which is another recurring topic).
<JordanBrown[m]>
Does their favorite IDE have a 3D model viewer?