Skip to content

Commit

Permalink
* Logging finalized
Browse files Browse the repository at this point in the history
* Project cleanup
* SVG Output on modify
  • Loading branch information
afshawnlotfi committed Jul 23, 2024
1 parent 174d35a commit ae9013b
Show file tree
Hide file tree
Showing 10 changed files with 1,600 additions and 648 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.pkl
examples/**/*
cadquery_run.cache
test_shape.stl
Expand Down Expand Up @@ -202,7 +203,8 @@ cython_debug/
.LSOverride

# Icon must end with two \r
Icon
Icon


# Thumbnails
._*
Expand Down
52 changes: 22 additions & 30 deletions orion_cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
from typing import Optional
import click
import os

from orion_cli.services.log_service import logger
CACHE_FILE_PATH = 'cadquery_run.cache'

# TODO: look into just using a pathlib.Path
class CustomPath(click.Path):
def convert(self, value, param, ctx):
value = os.path.expanduser(value)
return super().convert(value, param, ctx)

@click.group()
@click.version_option()
def cli():
Expand All @@ -18,7 +13,7 @@ def cli():
@click.option("--name", help="The name of the project", required=False)
@click.option("--cad_path", help="The path for a step file (CAD/3D) to be processed with the tool", type=click.Path(), required=False)
@click.option("--remote_url", help="The URL of the remote repository", required=False, default=None)
def create_command(name: str, cad_path: str, remote_url: str):
def create_command(name: str, cad_path: str, remote_url: Optional[str]):
"""Create a new project"""
from pathlib import Path
from orion_cli.services.create_service import CreateService
Expand All @@ -33,51 +28,48 @@ def create_command(name: str, cad_path: str, remote_url: str):
full_project_path = project_path / name

if full_project_path.exists():
click.echo(f"Project '{name}' already exists at {full_project_path}")
logger.info(f"Project '{name}' already exists at {full_project_path}")
overwrite = click.confirm("Would you like to overwrite it?", default=False)
if not overwrite:
click.echo("Exiting without creating project.")
logger.info("Exiting without creating project.")
return
# Remove the project directory and its contents
shutil.rmtree(full_project_path)


# Prompt the user for inputs if not provided
if not cad_path:
cad_path = click.prompt("Please enter the path for a step file (CAD/3D) to be processed with the tool", type=click.Path(exists=True))
cad_path = click.prompt("CAD file (*.step, *.stp)", type=click.Path(exists=True))

if not remote_url:
provide_remote_url = click.confirm("Would you like to provide the URL of the remote repository?", default=False)
provide_remote_url = click.confirm("Would you like to provide the URL of the remote Git repository?", default=False)
if not provide_remote_url:
pass
else:
remote_url = click.prompt("Please enter the URL of the remote repository")
remote_url = click.prompt("Remote Git Repository")

if remote_url:
# Check if the remote repository is valid and accessible
valid_url = RemoteHelper.get_valid_remote_url(remote_url)
if valid_url is None:
print("Continuing without a remote repository.")
logger.info("Continuing without a remote Git repository.")
else:
print(f"Using remote repository: {valid_url}")
logger.info(f"Using remote repository: {valid_url}")
remote_url = valid_url

# Resolve the paths to ensure they are absolute
cad_path = Path(cad_path).resolve()

# Create the project
service = CreateService()
try:
service.create(name, str(project_path), str(cad_path), remote_url)
click.echo(f"Project '{name}' has been created/updated at {project_path / name}")
click.echo(f"Original CAD file: {cad_path}")
click.echo(f"A copy of the CAD file has been stored in the project directory.")
click.echo("Project configuration has been created and saved.")
service.create(name, project_path, cad_path, remote_url)
logger.info(f"Project '{name}' has been created/updated at {project_path / name}")
logger.info(f"Original CAD file: {cad_path}")
logger.info(f"CAD file has been copied in the project directory.")
logger.info("Project configuration has been created and saved.")
except Exception as e:
click.echo(f"Error creating/updating project: {e}")
logger.info(f"Error creating/updating project: {e}")
return

click.echo("Project creation/update completed successfully.")
logger.info("Project creation/update completed successfully.")

@cli.command(name="config")
def config_command():
Expand All @@ -88,11 +80,11 @@ def config_command():

@cli.command(name="revision")
@click.option(
"--project-path", type=CustomPath(exists=True), prompt="Please enter the project path",
"--project-path", type=click.Path(exists=True), prompt="Please enter the project path",
help="The path of the project to be revised"
)
@click.option(
"--step-file", type=CustomPath(exists=True), prompt="Please enter the step file path",
"--step-file", type=click.Path(exists=True), prompt="Please enter the step file path",
help="The path for a step file (CAD/3D) to be processed with the tool"
)
@click.option(
Expand All @@ -107,7 +99,7 @@ def revision_command(project_path: str, cad_path: str, commit_message: str):

@cli.command(name="sync")
@click.option(
"--project-path", type=CustomPath(exists=True), prompt="Please enter the project path",
"--project-path", type=click.Path(exists=True), prompt="Please enter the project path",
help="The path of the project to be synchronized"
)
def sync_command(project_path: str):
Expand All @@ -120,7 +112,7 @@ def sync_command(project_path: str):
def test_cadquery_command():
"""Test CadQuery by creating and displaying a basic shape"""
if not os.path.exists(CACHE_FILE_PATH):
click.echo("This may take a while the first time it is run. Please be patient...")
logger.info("This may take a while the first time it is run. Please be patient...")
with open(CACHE_FILE_PATH, 'w') as f:
f.write("") # Create the cache file

Expand All @@ -133,7 +125,7 @@ def test_cadquery_command():
# Export the shape to an STL file
exporters.export(box, 'test_shape.stl')

click.echo("CadQuery test shape created and saved as 'test_shape.stl'")
logger.info("CadQuery test shape created and saved as 'test_shape.stl'")

if __name__ == "__main__":
cli()
100 changes: 69 additions & 31 deletions orion_cli/helpers/cad_helper.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
from dataclasses import dataclass
import hashlib
from pathlib import Path
from typing import Optional, Union
import pickle
from typing import Optional, Union, cast
from cachetools import LRUCache
import numpy as np
from OCP.GProp import GProp_GProps
from OCP.TopoDS import TopoDS_Shape, TopoDS_Vertex, TopoDS
from OCP.TopoDS import TopoDS_Shape, TopoDS_Vertex, TopoDS, TopoDS_Solid
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
from OCP.gp import gp_Trsf
from OCP.BRepTools import BRepTools
from OCP.BRep import BRep_Builder, BRep_Tool
import cadquery as cq
from OCP.BRepGProp import BRepGProp
from ocp_tessellate.tessellator import Tessellator, compute_quality
from ocp_tessellate.tessellator import Tessellator, compute_quality, cache_size, get_size
from ocp_tessellate.ocp_utils import bounding_box, get_location
import cadquery as cq
from orion_cli.readers.step_reader import StepReader
from ocp_tessellate.stepreader import StepReader

RotationMatrixLike = Union[np.ndarray, list[list[float]]]
VectorLike = Union[np.ndarray, list[float]]

@dataclass
class Mesh:
Expand Down Expand Up @@ -59,34 +63,34 @@ def tesselate_shape(shape: cq.Solid):

@staticmethod
def transform_solid(
solid: cq.Solid, orientation: np.ndarray, offset: Optional[np.ndarray] = None
solid: cq.Solid, rotmat: RotationMatrixLike, offset: Optional[VectorLike] = None
):
if offset is None:
offset = np.zeros(3)

# Get the transformation
loc = CadHelper.get_location(orientation, offset)
loc = CadHelper.get_location(rotmat, offset)
transformation = loc.wrapped.Transformation()

# Apply the transformation
transformer = BRepBuilderAPI_Transform(solid.wrapped, transformation, False)
return cq.Solid(transformer.Shape())

@staticmethod
def get_location(orientation: np.ndarray, offset: np.ndarray):
def get_location(rotmat: RotationMatrixLike, offset: VectorLike):
transformation = gp_Trsf()
transformation.SetValues(
orientation[0][0],
orientation[0][1],
orientation[0][2],
rotmat[0][0],
rotmat[0][1],
rotmat[0][2],
offset[0],
orientation[1][0],
orientation[1][1],
orientation[1][2],
rotmat[1][0],
rotmat[1][1],
rotmat[1][2],
offset[1],
orientation[2][0],
orientation[2][1],
orientation[2][2],
rotmat[2][0],
rotmat[2][1],
rotmat[2][2],
offset[2],
)
return cq.Location(transformation)
Expand All @@ -111,12 +115,26 @@ def import_step(file_path: Union[Path, str]) -> cq.Assembly:
Import a STEP file
Returns a TopoDS_Shape object
"""
file_path = Path(file_path)
assert file_path.exists(), f"File not found: {file_path}"
assert file_path.suffix.lower() in [".step", ".stp"], "Invalid file type"

r = StepReader()
r.load(str(file_path))
return r.to_cadquery()
return cast(cq.Assembly, r.to_cadquery())


@staticmethod
def import_cad(file_path: Union[Path, str]) -> cq.Assembly:
"""
Import a CAD file
Returns a TopoDS_Shape object
"""
file_path = Path(file_path)
if file_path.suffix.lower() in [".step", ".stp"]:
return CadHelper.import_step(file_path)

raise ValueError("Invalid file type")

@staticmethod
def export_brep(shape: TopoDS_Shape, file_path: str):
Expand Down Expand Up @@ -208,19 +226,7 @@ def geo_align_vertices(vertices1, vertices2):
# Singular Value Decomposition (SVD)
U, S, Vt = np.linalg.svd(H)
rotation_matrix = np.dot(Vt.T, U.T)

# Ensure the rotation matrix is proper (determinant should be +1)
# if np.linalg.det(rotation_matrix) < 0:
# Vt[2, :] *= -1
# rotation_matrix = np.dot(Vt.T, U.T)
# print(np.linalg.det(rotation_matrix))

# Rotate vertices2 to align with vertices1
# aligned_vertices2 = np.dot(vertices2_centered, rotation_matrix)

# # Translate aligned vertices to the position of vertices1
# aligned_vertices2 += centroid1


return rotation_matrix

@staticmethod
Expand All @@ -239,7 +245,8 @@ def align_parts(part1: cq.Solid, part2: cq.Solid):
raise ValueError(f"failed to align, error: {error}")

@staticmethod
def get_part_checksum(solid: cq.Solid, precision=3):
def get_part_checksum(solid: Union[cq.Solid, TopoDS_Solid], precision=3):
solid = solid if isinstance(solid, cq.Solid) else cq.Solid(solid)

vertices = np.array(
[CadHelper.vertex_to_Tuple(TopoDS.Vertex_s(v)) for v in solid._entities("Vertex")]
Expand All @@ -253,3 +260,34 @@ def get_part_checksum(solid: cq.Solid, precision=3):
vertices_hash = hashlib.md5(sorted_vertices.tobytes()).digest()
return hashlib.md5(vertices_hash).hexdigest()

@staticmethod
def save_cache(cache: LRUCache, path: Union[str, Path]):
with open(path, 'wb') as f:
pickle.dump(cache, f)

# Function to load cache from a file
@staticmethod
def load_cache(path: Union[str, Path]):

try:
with open(path, 'rb') as f:
return pickle.load(f)
except FileNotFoundError:
return LRUCache(maxsize=cache_size, getsizeof=get_size)

@staticmethod
def get_viewer(cad_obj, cache_path: Union[Path, str, None] = None):
from jupyter_cadquery.viewer import show
from jupyter_cadquery.tessellator import create_cache

if cache_path and Path(cache_path).exists():
with open(cache_path, 'rb') as f:
cache = pickle.load(f)
else:
cache = create_cache()
# print(tess.cache)
viewer = show(cad_obj, cache=cache)

if cache_path:
CadHelper.save_cache(cache, cache_path)
return viewer
Loading

0 comments on commit ae9013b

Please sign in to comment.