Jump to content

Script: STL to STEP


---
 Share

Recommended Posts

There are times, where I have to convert a lot of STLs to STEP via the AutoSurfacing tool. It is tedious, and repetitive, so I created an app to do it for me. 

 At the moment, it will just prompt you to enter the directory where the STL files are located. I'm working on a better solution. 

# -*- coding: utf-8 -*-
import gom, os, glob

# --- CONFIG ---
RECURSIVE       = False
OUT_SUBFOLDER   = "STEP"
NUM_QUADS       = 100_000

def _find_stls(folder: str, recursive: bool) -> list[str]:
	pattern = os.path.join(folder, "**", "*.stl") if recursive else os.path.join(folder, "*.stl")
	return sorted(glob.glob(pattern, recursive=recursive))

def _safe_delete_part_by_name(name: str):
	"""Delete an existing Part by name, if present; ignore if absent."""
	try:
		part_obj = gom.app.project.parts[name]
	except Exception:
		return
	try:
		gom.script.cad.delete_element(
			elements=[part_obj],
			with_measuring_principle=True
		)
	except Exception:
		# If your build needs a different command, record one delete and swap it in here.
		pass

def main():
	# --- inline embedded dialog (1-line input) ---
	try:
		RES = gom.script.sys.execute_user_defined_dialog(dialog={
			"content": [[{
				"columns": 1,
				"name": "src_folder",
				"password": False,
				"read_only": False,
				"rows": 1,
				"tooltip": {"id": "", "text": "", "translatable": True},
				"type": "input::string",
				"value": ""
			}]],
			"control": {"id": "OkCancel"},
			"embedding": "always_toplevel",
			"position": "",
			"size": {"height": 124, "width": 420},
			"sizemode": "automatic",
			"style": "",
			"title": {"id": "", "text": "Source folder path", "translatable": True}
		})
	except gom.RequestError:
		print("Dialog canceled.")
		return

	# Safely fetch the folder path
	raw_path = None
	try:
		raw_path = getattr(RES, "src_folder", None)
	except Exception:
		pass
	if raw_path is None:
		try:
			raw_path = RES["src_folder"]
		except Exception:
			pass

	if not raw_path:
		print("No folder provided (empty value or object-name mismatch).")
		return

	# Normalize & validate
	SRC_FOLDER = os.path.normpath(str(raw_path).strip('" '))
	if not os.path.isdir(SRC_FOLDER):
		print(f"Not a valid folder: {SRC_FOLDER!r}")
		return

	stls = _find_stls(SRC_FOLDER, RECURSIVE)
	if not stls:
		print(f"ℹ️  No STL files found in: {SRC_FOLDER}")
		return

	out_folder = os.path.join(SRC_FOLDER, OUT_SUBFOLDER)
	os.makedirs(out_folder, exist_ok=True)
	print(f"🗂  Found {len(stls)} STL(s). Exporting STEP to: {out_folder}")

	prev_part = None

	for i, file_path in enumerate(stls, start=1):
		base = os.path.splitext(os.path.basename(file_path))[0]
		part_name = base                       # unique part per file
		step_path = os.path.join(out_folder, f"{base}.step")
		print(f"\n[{i}/{len(stls)}] Processing: {file_path}")

		try:
			# 1) Clear previous Part (do NOT close project or app)
			if prev_part:
				_safe_delete_part_by_name(prev_part)
				prev_part = None

			# 2) Import STL into clipboard
			gom.script.sys.import_stl(
				bgr_coding=True,
				files=[file_path],
				geometry_based_refining=False,
				import_mode='clipboard',
				length_unit='mm',
				stl_color_bit_set=False,
				target_type='mesh'
			)

			# 3) Create new Part for THIS file & move actuals into it
			gom.script.part.create_new_part(name=part_name)
			actuals = list(gom.app.project.clipboard.actual_elements)
			if not actuals:
				raise RuntimeError("No actual elements found after STL import.")

			gom.script.part.add_elements_to_part(
				delete_invisible_elements=True,
				elements=actuals,
				import_mode='new_elements',
				part=gom.app.project.parts[part_name]
			)

			# 4) Build CAD from THIS part's mesh (explicit target)
			gom.script.sys.switch_to_mesh_editing_workspace()
			cad_group = gom.script.mesh.automatic_cad_surface_from_mesh(
				merge=True,
				mesh=gom.app.project.parts[part_name].actual,  # <- unambiguous mesh
				name='CAD group 1',
				number_of_quads=NUM_QUADS,
				preview_type=1
			)

			# 5) Export ONLY the CAD group we just created
			gom.script.sys.export_step(
				elements=[cad_group],
				file=step_path
			)
			print(f" STEP exported: {step_path}")

			# 6) Optional: remove that temporary CAD group (keeps project lean)
			try:
				gom.script.cad.delete_element(
					elements=[cad_group],
					with_measuring_principle=True
				)
			except Exception:
				pass

			# remember for next loop's deletion
			prev_part = part_name

		except Exception as e:
			print(f" Error on '{file_path}': {e}  (continuing)")

	# Optional: delete last part too
	if prev_part:
		_safe_delete_part_by_name(prev_part)

	print("\n🎉 All done.")

if __name__ == "__main__":
	main()

 

Auto CAD.addon

  • Like! 1
Link to comment
Share on other sites

To follow-up on this. There is File System Browser widget that is part of the documentation, that while I can add it to the script, and "use it". I am unable to actually access any information from it. 

https://zeissiqs.github.io/zeiss-inspect-addon-api/2025/howtos/python_api_introduction/user_defined_dialogs.html#file-system-browser-widget

It even shows an example of 

print(DIALOG.filesystemWidget.selected)

but it doesn't work for me. If someone is able to figure this out for me, I'd greatly appreciate it. 

Link to comment
Share on other sites

There is no problem with that widget.

You can filter filetypes, you can force user with ROOT folder, you can use multiselect.

All needed info about files is from ".selected" and it's a list of strings with full path

print(DIALOG.filesystem.selected) -> ['C:/Install.log', 'C:/csb.log', 'C:/appverifUI.dll']

Link to comment
Share on other sites

Yeah, it doesn't work for me. 

Can you post a sample snippet with the dialog as well? I'm uncertain why I can't get it work. 

Link to comment
Share on other sites

Well I am using different approach for dialogs.

I am using file with gdlg extension.


test.gdlg:

{
    "content": [
        [
            {
                "columns": 1,
                "name": "filesystem",
                "root": "",
                "rows": 1,
                "show_date": true,
                "show_size": true,
                "show_type": true,
                "tooltip": {
                    "id": "",
                    "text": "",
                    "translatable": true
                },
                "type": "special::filesystem",
                "use_multiselection": true
            }
        ]
    ],
    "control": {
        "id": "OkCancel"
    },
    "embedding": "always_toplevel",
    "position": "center",
    "size": {
        "height": 284,
        "width": 284
    },
    "sizemode": "automatic",
    "style": "Standard",
    "title": {
        "id": "",
        "text": "Test",
        "translatable": true
    }
}

test.py:
 

DIALOG = gom.script.sys.create_user_defined_dialog(file='test.gdlg')

gom.script.sys.show_user_defined_dialog(dialog=DIALOG)

print(DIALOG.filesystem.selected)

This way I can access each widget

Link to comment
Share on other sites

# -*- coding: utf-8 -*-
import gom, os, glob
from typing import List, Optional

# --- CONFIG ---
RECURSIVE       = False
OUT_SUBFOLDER   = "STEP"
NUM_QUADS       = 100_000

def _find_stls(folder: str, recursive: bool) -> List[str]:
	pattern = os.path.join(folder, "**", "*.stl") if recursive else os.path.join(folder, "*.stl")
	return sorted(glob.glob(pattern, recursive=recursive))

def _safe_delete_part_by_name(name: str) -> None:
	"""Delete an existing Part by name, if present; ignore if absent."""
	try:
		part_obj = gom.app.project.parts[name]
	except Exception:
		return
	try:
		gom.script.cad.delete_element(
			elements=[part_obj],
			with_measuring_principle=True
		)
	except Exception:
		pass

def _pick_src_folder_via_gdlg(file_path='dialog.gdlg'):
	"""
	Uses your .gdlg with a File System Browser widget named 'filesystem'
	and returns a normalized folder path, or None if canceled/invalid.
	"""
	DIALOG = gom.script.sys.create_user_defined_dialog(file=file_path)

	# Show dialog; on Cancel it raises gom.RequestError → return None
	try:
		gom.script.sys.show_user_defined_dialog(dialog=DIALOG)
	except gom.RequestError:
		return None

	# Read selection from the widget handle (not from a Result map)
	try:
		sel = DIALOG.filesystem.selected
	except Exception:
		sel = None
	if not sel:
		return None

	# If multiselect is on, take the first item
	if isinstance(sel, (list, tuple)):
		sel = sel[0]

	path = str(sel)
	# If a file was selected, use its parent folder
	folder = os.path.dirname(path) if os.path.isfile(path) else path
	folder = os.path.normpath(folder)
	return folder if os.path.isdir(folder) else None


def main() -> None:
	# ---- Get SRC_FOLDER from .gdlg filesystem picker ----
	SRC_FOLDER = _pick_src_folder_via_gdlg('dialog.gdlg')
	if SRC_FOLDER is None:
		print("No valid folder selected. Cancelled.")
		return

	# --------- batch process ----------
	stls = _find_stls(SRC_FOLDER, RECURSIVE)
	if not stls:
		print(f"ℹ️  No STL files found in: {SRC_FOLDER}")
		return

	out_folder = os.path.join(SRC_FOLDER, OUT_SUBFOLDER)
	os.makedirs(out_folder, exist_ok=True)
	print(f"🗂  Found {len(stls)} STL(s). Exporting STEP to: {out_folder}")

	prev_part = None

	for i, file_path in enumerate(stls, start=1):
		base = os.path.splitext(os.path.basename(file_path))[0]
		part_name = base                       # unique part per file
		step_path = os.path.join(out_folder, f"{base}.step")
		print(f"\n[{i}/{len(stls)}] Processing: {file_path}")

		try:
			# 1) Clear previous Part
			if prev_part:
				_safe_delete_part_by_name(prev_part)
				prev_part = None

			# 2) Import STL into clipboard
			gom.script.sys.import_stl(
				bgr_coding=True,
				files=[file_path],
				geometry_based_refining=False,
				import_mode='clipboard',
				length_unit='mm',
				stl_color_bit_set=False,
				target_type='mesh'
			)

			# 3) Create new Part for this file & move actuals into it
			gom.script.part.create_new_part(name=part_name)
			actuals = list(gom.app.project.clipboard.actual_elements)
			if not actuals:
				raise RuntimeError("No actual elements found after STL import.")

			gom.script.part.add_elements_to_part(
				delete_invisible_elements=True,
				elements=actuals,
				import_mode='new_elements',
				part=gom.app.project.parts[part_name]
			)

			# 4) Build CAD from this part's mesh (explicit target)
			gom.script.sys.switch_to_mesh_editing_workspace()
			cad_group = gom.script.mesh.automatic_cad_surface_from_mesh(
				merge=True,
				mesh=gom.app.project.parts[part_name].actual, 
				name='CAD group 1',
				number_of_quads=NUM_QUADS,
				preview_type=1
			)

			# 5) Export only the CAD group we just created
			gom.script.sys.export_step(
				elements=[cad_group],
				file=step_path
			)
			print(f" STEP exported: {step_path}")

			# 6) Optional: remove that temporary CAD group (keeps project lean)
			try:
				gom.script.cad.delete_element(
					elements=[cad_group],
					with_measuring_principle=True
				)
			except Exception:
				pass

			# remember for next loop's deletion
			prev_part = part_name

		except Exception as e:
			print(f" Error on '{file_path}': {e}  (continuing)")

	# Optional: delete last part too
	if prev_part:
		_safe_delete_part_by_name(prev_part)

	print("\n🎉 All done.")

if __name__ == "__main__":
	main()

Updated with the help from Martin. Now you can directly select the folder instead of entering the string. 
 

Auto CAD.addon

  • Like! 1
Link to comment
Share on other sites

 Share

×
×
  • Create New...