Files
braille_translator/model_gen/openscad_objects.py
2024-06-11 08:02:41 +01:00

316 lines
9.1 KiB
Python

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 27 12:03:30 2023
@author: Edward Middleton-Smith
Procedural OpenSCAD Generation
https://github.com/taxpon/openpyscad/blob/develop/openpyscad/base.py
"""
#import argument_validation as av
from colour_theme_braille_character import Colour_Theme_Character_Braille
# import openpyscad as ops
from openpyscad import Union
from typing import Optional
from pydantic import BaseModel, Field, validator
# from pydantic.fields import ModelField
from abc import ABC, abstractmethod
import os
def bool_2_str(mybool):
# FUNCTION
# Convert boolean to lowercase string for openscad
# ARGUMENTS
# bool mybool
# ARGUMENT VALIDATION
if not av.val_bool(mybool):
return f'Error converting boolean to string.\nInput: {mybool}'
# RETURNS
return str(mybool).lower()
def neg_fillet(L, r, centre = True, q = 25):
# FUNCTION
# return negative-space fillet object for use with boolean operation
# ARGUMENTS
# int L - cylinder height
# double r - fillet radius
# bool centre - is fillet centred vertically?
# int q - quality / discretisation - number of points per edge
# ARGUMENT VALIDATION
if not (av.val_double(L, 0) and av.val_double(r, 0) and av.val_bool(centre) and av.val_int(q, 0)):
return None
# VARIABLE INSTANTIATION
fillet = ops.Difference()
# METHODS
fillet.append(ops.Cube([r, r, L], center = "false", _fn = q))
fillet.append(ops.Cylinder(L, r, center = "false"))
fillet.mirror([1, 1, 0])
fillet.translate([r, r, -L/2 if centre else 0])
# RETURNS
return fillet
def base_fillet_cube(a, b, c, f, q, centre = True):
# FUNCTION
# return openpyscad object of cube with base edges and corners filleted with radius f
# ARGUMENTS
# double a, b, c - cube dimensions x, y, z
# double f - fillet radius
# int q - quality / discretisation - number of points per edge
# bool centre - is base-filleted cube centred?
# ARGUMENT VALIDATION
if not (av.val_double(a, 0) and av.val_double(a, 0) and av.val_double(a, 0)):
return None
if not (av.val_double(f, 0, min(a, b, c)) and av.val_int(q, 1) and av.val_bool(centre)):
return None
# VARIABLE INSTANTIATION
my_dif = ops.Difference()
# METHODS
my_dif.append(ops.Cube([a, b, c], center = "true", _fn = q).translate([0, 0, c/2]))
my_dif.append(neg_fillet(a, f, True, q).rotate([90, 0, 0]))
my_dif.append(neg_fillet(a, f, True, q).rotate([90, 0, 0]).mirror([0, 1, 0]).translate([f + a, 0, 0]))
my_dif.append(neg_fillet(a, f, True, q).rotate([0, 90, 0]))
my_dif.append(neg_fillet(a, f, True, q).rotate([0, 90, 0]).mirror([1, 0, 0]).translate([0, f + b, 0]))
# RETURNS
return my_dif.translate([0 if centre else a/2, 0 if centre else b/2, -c/2 if centre else 0])
def triprism(a, L, centre = True):
# FUNCTION
# Triangular-based prism
# ARGUMENTS
# double a - triangle side-length
# double L - depth in Z-direction
# bool centre - is triprism centred by volume?
# ARGUMENT VALIDATION
if not (av.val_double(a, 0) and av.val_double(L, 0) and av.val_bool(centre)):
return None
# RETURNS
return ops.Polygon([[0,0], [0, a], [a, 0]]).linear_extrude(L, center = bool_2_str(centre)).translate([-a/2 if centre else 0, -a/3 if centre else 0, 0])
def get_openscad_colours():
return [ # openscad_colours = [
"Lavender",
"Thistle",
"Plum",
"Violet",
"Orchid",
"Fuchsia",
"Magenta",
"MediumOrchid",
"MediumPurple",
"BlueViolet",
"DarkViolet",
"DarkOrchid",
"DarkMagenta",
"Purple",
"Indigo",
"DarkSlateBlue",
"SlateBlue",
"MediumSlateBlue",
"Pink",
"LightPink",
"HotPink",
"DeepPink",
"MediumVioletRed",
"PaleVioletRed",
"Aqua",
"Cyan",
"LightCyan",
"PaleTurquoise",
"Aquamarine",
"Turquoise",
"MediumTurquoise",
"DarkTurquoise",
"CadetBlue",
"SteelBlue",
"LightSteelBlue",
"PowderBlue",
"LightBlue",
"SkyBlue",
"LightSkyBlue",
"DeepSkyBlue",
"DodgerBlue",
"CornflowerBlue",
"RoyalBlue",
"Blue",
"MediumBlue",
"DarkBlue",
"Navy",
"MidnightBlue",
"IndianRed",
"LightCoral",
"Salmon",
"DarkSalmon",
"LightSalmon",
"Red",
"Crimson",
"FireBrick",
"DarkRed",
"GreenYellow",
"Chartreuse",
"LawnGreen",
"Lime",
"LimeGreen",
"PaleGreen",
"LightGreen",
"MediumSpringGreen",
"SpringGreen",
"MediumSeaGreen",
"SeaGreen",
"ForestGreen",
"Green",
"DarkGreen",
"YellowGreen",
"OliveDrab",
"Olive",
"DarkOliveGreen",
"MediumAquamarine",
"DarkSeaGreen",
"LightSeaGreen",
"DarkCyan",
"Teal",
"LightSalmon",
"Coral",
"Tomato",
"OrangeRed",
"DarkOrange",
"Orange",
"Gold",
"Yellow",
"LightYellow",
"LemonChiffon",
"LightGoldenrodYellow",
"PapayaWhip",
"Moccasin",
"PeachPuff",
"PaleGoldenrod",
"Khaki",
"DarkKhaki",
"Cornsilk",
"BlanchedAlmond",
"Bisque",
"NavajoWhite",
"Wheat",
"BurlyWood",
"Tan",
"RosyBrown",
"SandyBrown",
"Goldenrod",
"DarkGoldenrod",
"Peru",
"Chocolate",
"SaddleBrown",
"Sienna",
"Brown",
"Maroon",
"White",
"Snow",
"Honeydew",
"MintCream",
"Azure",
"AliceBlue",
"GhostWhite",
"WhiteSmoke",
"Seashell",
"Beige",
"OldLace",
"FloralWhite",
"Ivory",
"AntiqueWhite",
"Linen",
"LavenderBlush",
"MistyRose",
"Gainsboro",
"LightGrey",
"Silver",
"DarkGray",
"Gray",
"DimGray",
"LightSlateGray",
"SlateGray",
"DarkSlateGray",
"Black"
]
"""
def base_fillet_cube(a, b, c, f, q, centre = "true"):
# fillets removed as they print horribly
# now just cube
my_dif = ops.Difference();
my_dif.append(ops.Cube([a, b, c], center = "true", _fn = q));
return my_dif.translate([0 if (centre == "true") else a/2, 0 if (centre == "true") else b/2, 0 if (centre == "true") else c/2])
def triprism(a, L, centre = "true"):
return ops.Polygon([[0,0], [0, a], [a, 0]]).linear_extrude(L, center = "true").translate([0, 0, 0 if (centre == "true") else L/2]);
"""
"""
def __get_pydantic_core_schema__(cls, handler):
return handler.generate_schema(ops.Union)
ops.Union.__get_pydantic_core_schema__ = classmethod(__get_pydantic_core_schema__)
class Union_Field(ModelField):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, value):
if not isinstance(value, Union):
raise ValueError(f'Union_Field: {value} is not a valid Union. Type: {type(value)}')
return value
"""
class ObjectOpenSCAD(BaseModel, ABC):
path_dir: str
filename: str
def __init__(self, **kwargs): # , path_dir, filename
# print(f'ObjectOpenSCAD: {path_dir}, {filename}')
# BaseModel.__init__(self, path_dir=path_dir, filename=filename)
# super().__init__(path_dir=path_dir, filename=filename, **kwargs)
BaseModel.__init__(self, **kwargs)
@validator('path_dir')
def validate_path_dir(cls, value):
if not os.path.exists(value):
raise ValueError("Path not found: " + value)
return value
@abstractmethod
def write(self):
pass
class ModelOpenSCAD(ObjectOpenSCAD): # , BaseModel):
colour_theme: Colour_Theme_Character_Braille
fn: int = 25
model: Optional[Union] = None #= Field(default_factory=Union) # : Optional[ops.Union] = None
def __init__(self, **kwargs): # , path_dir, filename, colour_theme, fn):
# print(f'ModelOpenSCAD: {path_dir}, {filename}, {colour_theme}, {fn}')
ObjectOpenSCAD.__init__(self, **kwargs) # , path_dir=path_dir, filename=filename)
BaseModel.__init__(self, **{**kwargs, 'model': None}) # , colour_theme=colour_theme, fn=fn)
self.model = Union()
def write(self):
self.model.write(self.path_dir + self.filename + '.scad')
class Config:
arbitrary_types_allowed = True
class AssemblyOpenSCAD(ObjectOpenSCAD): # , BaseModel):
dx: int
dy: int
max_models_per_row: int
fn: int = Field(ge=0)
def __init__(self, path_dir, filename, dx, dy, max_models_per_row, fn=25):
ObjectOpenSCAD.__init__(self, path_dir, filename)
BaseModel.__init__(self, dx, dy, max_models_per_row, fn)
self.assembly = ops.Assembly()