Skip to content

Instantly share code, notes, and snippets.

@pinkeen
Last active March 20, 2020 22:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pinkeen/52332e1a1769671ce299e6bb36a2659d to your computer and use it in GitHub Desktop.
Save pinkeen/52332e1a1769671ce299e6bb36a2659d to your computer and use it in GitHub Desktop.
Computational Solid Modeling Language - IDEA

Computational Solid Modeling Language - IDEA

Lets kickstart the 3D printing open things revolution!

...by creating a vibrant, coherent open things ecosystem modeled after the best practices already established by the Open Source Software community.

  • Purely functional and almost declarative - no mutations or side-effects allowed
  • Transpiled to JS which is then evaluated calling native libs or optimized computation cores for heavy-lifting.
    • Or transpile to TypeScript / AssemblyScript?
    • GPGPU using WebGL?
    • CGAL possible to compile with emscripten?
    • Bindings to CGAL or similar allowing only desktop application will suffice.
    • Using JS gives us web compat from the start and quite good performance for a functional language (V8 has tail call optimization?). Can reuse a lot of existing tooling. And the eage hype-prone userbase...
    • Integrate with VSCode, make a codepen, ParametrizedThingVerse and whatnot.
    • Live web preview when changing params.
    • Heavy lifting with web generators done on client's machine, no need to beefy servers for all the computations.
  • Easily use JS for scripting automation and everything besides generating the models themselves.
  • Compile the resulting JS (or TypeScript) to WASM for perf?
    • Question: Strong typing (like python) for sure but do we allow dynamic typing or go full strict static declarations?
  • Every Geometry node (an object or group of) is rendered to triangle mesh along the path and stored in cache. It shall also be given a name and id. Hash can be computed based code block and variable values - this is guaranteed to work since we have immutability. This way we will always re-render only the needed parts.
    • Cache should (optionally) use persistent storage for greatest acceleration (save resources when automating!).
    • Transpiled code can be used as intermediary format for various kinds of automation. Can be also precompiled for distribution or during installation to save time later.
  • Every (Geometry Node can be assigned a custom name and other properties like color and material (maybe later).
    • Export rendered final models to formats preserving this information. (Of course for the ones rendered from top-level nodes.)
    • Preview rendering automation in software like POVRAY or blender.
    • Single source produces multiple top-level Geometry Nodes thus can automatically exported into separate models upon build.
  • Geometry Nodes are connected to each other forming a graph, not a tree like the usual approach.
    • Allows for better caching - no need unwind the graph into a tree what would duplicate nodes and computing work.
    • Graph will contain cycles, but this is unavoidable and happens in every programming language.
    • We don't even need to walk the graph, we can just find any node that is not dependent on others and just execute the transpiled JS.
    • The Graph can be represented in an UI which would allow interactive, visual manipulation of the nodes and give nice overview. Module parameters could also be directly adjusted from the GUI. Selecting a Node could jump you into the relevant source-code.
  • Package manager (npm with custom repository?)
  • Metadata schema (YAML/JSON-based) for providing info about projects, libraries, parts, author, license, type, printing, etc.
    • Also a universal open schema for describing physical things, which will allow:
      • Easy cataloging, scraping, search, discovery. Find by:
        • Use-case
        • Compatiblity
        • Vistual style
        • Production technology
        • Tolerances
        • Suggested materials
      • Exploring links between projects, authors
      • Creation of huge sets of visually matching or interconnectable, modular parts created by separate people
      • Providing information for the actual production like suggested materials, parameters or even pre-computed production profiles
      • Slice complicated multi-part gizmos in minutes and start printing right away getting best possible result without hassle
  • Ready-made automation for:
    • Generating documentation
    • Building distribution packages with precompiled models
    • Distribution for use, reuse and remix
  • Big built-in library of native functions and transforms:
    • Single, perfomant, widely-used standard library instead of everybody reimplemeting the wheel and creating their own ecosystem - better interoperatiblity and reusability of community-made libraries.
    • Better performance possible due to optimizations in "native" bindings.
    • There's like a ton of cool stuff out there - subdivision, mesh repairing, finding intersections, wall centers. It would even allow this not only to create models but also easily remix existing ones which came from different ecosystems and are available only as single solids.
  • Access Geometry Node mesh data for computations what is impossible in OpenScad by design. We'd need a native function (resolve(somenode())) which would force rendering of the node before it can be utilized in calculations. The possibilities are endless:
    • Get size of bounding box
    • Find center of gravity
    • Implement custom advanced transforms?
  • Couple cool GUI ideas:
    • Click on the preview to jump to code that generated the object under cursor.
    • Live preview when adjusting parameters in the graph GUI via sliders and other controls. Can be limited to single node if performance is a concern.
  • Language must have namespacing for easier reuse, organization, limiting naming conflicts, overshadowing.
  • Model/Project parametrization metadata using JSON-schema and possible JSON-schema forms.
  • Generate technical 2D drawings, intersections with measurements for documentation, presentation and modeling.
  • Thanks to full metadata could be exported to WYSIWYG modelers
  • The code-only GUI could be slowly extended with visual, interactive features.
  • Imported libraries packages clearly separated in some ways to not clutter the UI and everything.
  • Component libraries can be directly browsed (with visual previews) with drag'n'drop or other easy mechanism (with editor hints?) for using them in code. Their docs and parameters could be also viewed directly from the context.
    • User can collect and organize 3rd party packages system-wide and based on usage in particular project dependency metadata generation and update would be (fully or partially) automated.
  • Features should be roughly a superset of OpenScad's what would allow to kickstart huge part library by just transpiling high quality open source projects that are already out there.

Sythetic Language Concepts and Syntax

Question to myself: Since this is already functional/almost declarative - I could scrap this syntax and just use YAML like ansible?

This OpenScad-like syntax is certainly nice, but yaml is interoperable, flexible and there's no need to start with my own lexer and parser from the beginning - just allow js expression-based templating somehow.

Or why not start the easy way and then support both?

Starting with YAML would be like getting a free serialization, data transfer format for free.

Or: XML/HTML similar to react - the modularization and reuse certainly seems like a similar use-case.

Data

  • num - any numerical type
  • vec - a list (array) Among others - used to represent and RGBA color, vector, matrix (list of lists), faces, points, complete polyhedron mesh data, etc.
  • str - string Can be used like vec most of the time, however, is stored internally differently and has other uses or limitations.
  • obj - nested dictionary (associative array, hash) modeled after JSON with the exact same syntax

Functions

Simple function

def subtract(a, b) = a - b;

Variables

All variables are really functions and can be defined only once in every scope. Redefining or trying to assing to an existing variable will fail.

Variable in a block

{
  let int a = 10
  let 
}

Scopes

Variables are always limited to scope of the current block, never passed to any outer or inner scope.

They are immutable and cannot be assigned to second time or redeclared. But they can be overshadowed in an inner block any level deeper.

The scope is defined by the body of:

  • function
  • module
  • block - curly-braced or colon-separated

Modules - Transform, Generate, Group Geometry

Generator modules

def rotate_and_translate(vec rot, vec trans, num size = 100) {
  rotate(rot = rot) {
    translate(trans = trans) {
      cube(size = size);
    }
  }
}

Transformator modules

def move_left(int distance = 5):
  translate([- distance, 0, 0]):
    children();

move_left():
  cube(size = 1);

Blocks

Can be indicated by wrapping statements with curly braces or separating them by colons.

Omit semicolons for single-line statements

def cubes(): {
  translate([10, 0, 0]): {
    cube(size = 10)
    cylinder(r = 20)
  }

  rotate([0, 10, 0]): {
    cube(size = 5)
  }

  cylinder(r = 10)
}

Omit braces for single statement

def cubes(): {
  translate([10, 0, 0]):
    cube(size = 10)
}

Omit braces for module body

def cubes():
  translate([10, 0, 0]):
    cube(size = 10)

Pass multiple children using colon instead of braces

def cubes(): {
  translate([10, 0, 0]):
    cube(size = 10),
    cylinder(r = 20)
  ;
}

You can still mix curly-braced blocks with semicolons

def cubes(): {
  translate([10, 0, 0]):
    cube(size = 10),
    cylinder(r = 20), {
        pyramid(s = 5, h = 2.5);
    }
}

And even go deeper with mixing all this

def cubes():
  translate([10, 0, 0]):
    cube(size = 10),
    cylinder(r = 20), mirror([0, -1, 0]): {
        union():
          pyramid(s = 5, h = 2.5),
          pyramid(s = 5, h = 2.5)
    }

Geometry tree and grouping

There is no implicit grouping unless necesarry.

Geometry generated by modules is not grouped

Consider this module relying on children:

def spread(int distance = 5):
  for(i = [0: count(children()) - 1]):
    translate([- distance * i, 0, 0]):
      child(i);

Then this code:

def items(): {
  cube(size = 1)
  cylinder(radius = 5)
}

spread(distance = 20): 
  items()

Is equivalent to:

spread(distance = 20): {
  cube(size = 1)
  cylinder(radius = 5)
}

Explicitly group to create a single geometry node

group() {
  cube(size = 1)
  cylinder(radius = 5)
}

Objects are merged into single Geometries when transformed

We'll have two geometries created from four:

hull(): a(), b()
union(): x(), y()

I have just realized that it could be a general purpose declarative/functional language.

It might be useful as some kind of instrastructure resource / state declaration language that could be used by a tool like ansible or terraform.

Ansible is declarative but yaml is very limiting an in the end not too readable. Mixing the declarative stuff with functional stuff via jinja templating in is very clunky, error prone and hard to debug.

Just replace the basic built-in geometry (e.g. polyhedron, sphere) generators (nodes) with calls to something that works like ansible "modules". Then just compute a graph of these nodes and execute by traversing it starting with the "root node" (outer/topmost one, wrapping the whole "program") then finding the first node in graph that does not "depend" on any other node (does not call anything but the built-ins).

This way we get automatic task ordering based on dependencies. Of course one could create artificial nodes (akin to "groups" in the CSML idea).


when(condition: false): {
  if condition:
    $children()
  else:
    $metadata(result: {
      skipped: true
    })
}


play('Get git repository info')                                                   // Build-in module that groups together all child nodes and ensures all task completed
  gitInfo(gitdir, failOnNotFound: false): {                                       // User declared module with two parameters, one of them having defaults                                                                                  // Arguments have module scope are never passed down, unless explicitly, or used in a submodule (closure)
    getGitDir():                                                                  // A user declared module, submodule specifically in this case
      task('Get git dir')                                                         // Built-in module that adds task metadata to child nodes, executes them one by one
        command(                                                                  // Built-in command node. Leaf nodes are the basic, smallest, building blocks, which perform action
          cmd: 'git rev-parse --git-dir',                                         // Arguments have the same syntax as JS object definiton (JSON on steroid) - sans the braces
          chdir: gitdir                                                           // Submodules directly become closures and inherit the concrete argument values from parent call
        )                

    gitDir = @getGitDir().stdout;                                                 // User defined function, functions must resolve module to get the data

    task('Fail with message')
      when(@getGitDir.rc == 4)                                                    // As modules do not return any data but node or node list, we must use the @ ("as-resolved") builtin to get the data returned by actual node run
        fail(                                                                     // Note that each node is executed only once per unique set of arguments, so it can be repeat like a variable
          msg: `Repo not found in dir ${gitdir}`                                  
        )

    metadata(

    );
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment