BCad fork

≺code≻export()≺/code≻ module
Login

≺code≻export()≺/code≻ module

Implemented. Exports part of the object tree to a file, allowing multiple files from one .scad script. Unlike the GUI's single export (--output, File → Export menu), export() runs during script evaluation in the OCCT worker process — no IPC changes required.

Syntax

// Two files from one script
export("part1.stl") { cube(10); }
export("part2.step") { rotate([0,0,45]) cylinder(r=5, h=20); }

// fmt can be explicit — the file name is used exactly as given
// (never modified): write to /dev/null, no extension, custom names all work
export(file="gear", fmt="dxf") { ... }

// All formats: step, stl, dxf (EXT_TO_FMT in make_writer)

The export node is a transparent container (an SCLPart3 without shape):

Design decisions (agreed)

  1. Syntaxexport(file) { children }. Exports the block as a subtree.
  2. Timing — write during evaluation. _module_scope evaluates children before writing (parse_block then kwargs), so the subtree is fully compiled at writer.Write() time. Errors elsewhere in the tree don't affect it.
  3. % background children — skipped (pattern of union(), scl_context.py:2589), so the file matches what's visually "solid".
  4. export is bcad-specific (OpenSCAD warns "Ignoring unknown module call 'export'").

Implementation

Files

File Change
scl.py export_module_definition (~356) + _handle_export (~2360, before _handle_surface) + _builtin_modules entry (has_block: True, func_id: FUNC_EXPORT)
progress_helper.py FUNC_EXPORT = 37 + name "export"
docs/scl.md documentation
tests/test_stl_resolution.scad STL resolution regression test (uses export/import/capture/num_faces)

Handler (pattern: _handle_color, scl.py:1787)

def _handle_export(self, node):
    sid, line, args, block = node['id'], node['line'], node['args'], node.get('block', [])
    with self._module_scope(SCLPart3, "export", sid, line, args,
                            export_module_definition['args'], block) as (ctx, top):
        filepath = self.find_variable_value('file', line) or ""
        fmt = self.find_variable_value('fmt', line) or None
        if not filepath:
            ctx.warn("export: missing file parameter")
        else:
            base = os.path.dirname(os.path.abspath(self.path))   # relative → script dir
            if not os.path.isabs(filepath):
                filepath = os.path.join(base, filepath)
            writer, filepath = self.make_writer(filepath, fmt)   # fmt from extension
            if writer is not None:
                for c in ctx.children:
                    if c.modifier & Modifier.BACKGROUND:
                        continue
                    c.display(writer)                            # colors → STEP automatically
                try:
                    writer.Write(filepath)                       # fresh writer per call
                except Exception as e:
                    warning(f"export failed (...): {e}")
        _child_keys = _collect_child_cache_keys(ctx)             # required (assert)
        if _child_keys is not None:
            ctx._cache_key = ShapeCache.instance().compute_key(
                'export', {'file': filepath, 'fmt': str(fmt)}, _child_keys)

Key mechanics

Behavior notes

Scoping constraints (3-phase evaluation)

eval_scope (scl.py:3075) runs in 3 phases: assignments (phase 1) execute BEFORE all non-assignment statements (phase 2). Consequences for tests/usage:

STL resolution semantics (in make_writer)

The exported STL mesh honors $fn/$fa/$fs/$fe (OpenSCAD semantics, approximated by OCCT):

Linear deflection only shows on large parts (angular dominates cylinder walls); planar faces are unaffected. hull() uses the same min($fs, $fe) rule (scl_context.py:2068).

Testing plan

A single .scad with two export() calls (different formats) + one % background child inside an export block; verify both files exist and the % child is absent.