Compare commits

..

111 Commits

Author SHA1 Message Date
Leni Aniva 4f48955c81 Merge pull request 'build: Migrate to uv' (#17) from build/uv into main
Reviewed-on: #17
2025-07-04 16:11:40 -07:00
Leni Aniva d7e3e1a8de
build: Add parallelization of unit test 2025-07-04 16:11:18 -07:00
Leni Aniva f669b61647
build: Migrate to uv 2025-07-04 16:08:35 -07:00
Leni Aniva 2a405f916a Merge pull request 'cosplay: Touhou/Yasaka Kanako' (#11) from touhou/yasaka-kanako into main
Reviewed-on: #11
2025-06-16 10:16:57 -07:00
Leni Aniva 4c0a54dc4a
Update controller to use internal pullup 2025-06-16 10:16:32 -07:00
Leni Aniva db4232f94d
Add startup sequence 2025-06-11 15:30:28 -07:00
Leni Aniva 675f4f995b
Add controller code for Kanako 2025-06-10 00:53:41 -07:00
Leni Aniva 52b4e0b329
Add ears to the pipe joint 2025-06-03 18:50:39 -07:00
Leni Aniva 7ec2728a6c
Use 0.5 spindle gap for rotation resistance 2025-06-03 18:50:24 -07:00
Leni Aniva 5d7137c037
Shimenawa geometry 2025-06-03 09:11:06 -07:00
Leni Aniva 49d3fa44bf
Separate interior and exterior gaps 2025-06-03 00:53:31 -07:00
Leni Aniva 89c0d88de7
Use a much larger spindle gap 2025-06-02 22:48:39 -07:00
Leni Aniva 0a4ca64dad
Add front stabilization bracket 2025-06-01 22:52:13 -07:00
Leni Aniva d6ccc3496b
Use jute rope for handle 2025-05-31 07:41:54 -07:00
Leni Aniva c8bbc0de91
Magnet holder 2025-05-30 22:31:08 -07:00
Leni Aniva 34ecf59124
Add more mounting points on chamber front 2025-05-30 18:00:48 -07:00
Leni Aniva a0100f8fb7
Front chamber separator 2025-05-30 14:35:55 -07:00
Leni Aniva e4cfc71f1a
Electrnics assembly 2025-05-30 14:20:25 -07:00
Leni Aniva 80730a9c5a
Fix stator coupler hole size 2025-05-30 10:20:49 -07:00
Leni Aniva 6777033383
Remove conflict with base geometry 2025-05-30 01:54:19 -07:00
Leni Aniva 758b51c9db
Remove redundant geometry from seat 2025-05-30 01:35:55 -07:00
Leni Aniva ef0b0dad91
Add reinforcements to motor seat 2025-05-30 01:33:14 -07:00
Leni Aniva 6ad74047bc
Simplify seat geometry 2025-05-30 01:22:48 -07:00
Leni Aniva 7d3845f3c1
Align coupler holes 2025-05-30 00:32:40 -07:00
Leni Aniva b55dc8caa3
Calibrate measurements 2025-05-30 00:28:44 -07:00
Leni Aniva e94546b017
Motor seat and coupler 2025-05-29 23:40:12 -07:00
Leni Aniva 2d6d65235d
Turning bar 2025-05-29 17:24:06 -07:00
Leni Aniva 0f151bd279
Motor assembly 2025-05-29 16:21:01 -07:00
Leni Aniva 6709e4f32e
Bearing couplers 2025-05-29 14:04:12 -07:00
Leni Aniva d937fc9513
Retrofit handle 2025-05-29 09:22:44 -07:00
Leni Aniva a3288ce98f
Spindle geometry 2025-05-29 01:19:19 -07:00
Leni Aniva bec15c5136
Model of the motor 2025-05-29 00:52:56 -07:00
Leni Aniva b565ab05a0
Additional mounting points for machinery on first 3 rings 2025-05-29 00:13:48 -07:00
Leni Aniva af82a86652
Eliminate difficult geometry in angle joint 2025-05-28 23:49:04 -07:00
Leni Aniva 40c32213e1
Motor and bolt models 2025-05-28 08:01:14 -07:00
Leni Aniva a8c80a307f
Handle stub 2025-05-21 20:25:57 -07:00
Leni Aniva b1fe538747
Use dihedral angle to calculate sanding block 2025-05-20 19:47:16 -07:00
Leni Aniva bd15f28403
Improve grometry 2025-05-20 08:24:41 -07:00
Leni Aniva 0574a767a3
Add sanding block 2025-05-20 08:18:11 -07:00
Leni Aniva bd7e8677c7
Make all angle joints flanged 2025-05-16 07:15:30 -07:00
Leni Aniva c5f9e570a6
Fit onbashira profile in 12x12 in panel 2025-05-15 23:16:39 -07:00
Leni Aniva 63c2c74e02
Barrel position solver 2025-05-15 21:20:22 -07:00
Leni Aniva 0fb88a97d3
Chamber connectors 2025-05-15 20:55:17 -07:00
Leni Aniva 4edad88299
Larger mirror dimensions 2025-05-15 13:23:52 -07:00
Leni Aniva 83d4232ad7
Add holes for gohei 2025-05-14 23:21:13 -07:00
Leni Aniva 4d4e4c7eab
Mating structure for angle joint 2025-05-14 23:08:47 -07:00
Leni Aniva b83bf5a57d
Use only one bolt for angle bracket 2025-05-14 22:40:53 -07:00
Leni Aniva 670d4a8c21
Improve geometry of angle joint 2025-05-14 13:13:44 -07:00
Leni Aniva 22a4f4ceec
Mirror wing geometry 2025-05-13 17:20:51 -07:00
Leni Aniva a684996475
Geometry of mirror and rotor spacer 2025-05-13 14:29:10 -07:00
Leni Aniva 5b5ccee94e
Optimize angle joint geometry; Mirror stub 2025-05-13 09:11:28 -07:00
Leni Aniva b88d52f4be
Use 8mm bolt 2025-05-13 00:02:43 -07:00
Leni Aniva ca606c6bc1
Fix rotation radius 2025-05-12 23:59:34 -07:00
Leni Aniva 916ccee260
Centre holes 2025-05-12 23:52:45 -07:00
Leni Aniva 44cd6ee960
Onbashira dimension update and flanges 2025-05-12 23:33:20 -07:00
Leni Aniva 4dcd97613b
Section bracing 2025-05-12 22:08:44 -07:00
Leni Aniva 97675a2fc8
Add angle joint stub, hole in rotor 2025-05-12 14:50:59 -07:00
Leni Aniva 74145f88d2
Add bolts on rotor 2025-05-12 12:24:33 -07:00
Leni Aniva 878d532890
Use rotor-stator configuration for bearing 2025-05-09 16:58:14 -04:00
Leni Aniva 7511efa9ee
Add Kanako set class 2025-04-22 11:16:44 -07:00
Leni Aniva 3e0eab0cec
Merge branch 'main' into touhou/yasaka-kanako 2025-04-20 12:49:17 -07:00
Leni Aniva 70a157a0ab Merge pull request 'cosplay: Touhou/Shiki Eiki' (#14) from touhou/shiki-eiki into main
Reviewed-on: #14
2025-04-20 12:49:01 -07:00
Leni Aniva 026c2c933c
fix: Add control vertex to eye shape 2025-04-20 12:48:20 -07:00
Leni Aniva eae736fdfb
fix: Add outer curvature to side guard 2025-04-11 11:15:15 -07:00
Leni Aniva b15db172a0
Merge branch 'main' into touhou/yasaka-kanako 2025-04-08 23:28:30 -07:00
Leni Aniva 1e692b89ed Merge pull request 'cosplay: Touhou/Shiki Eiki' (#7) from touhou/shiki-eiki into main
Reviewed-on: #7
2025-04-08 23:01:04 -07:00
Leni Aniva f7b915a7e4
Merge branch 'main' into touhou/shiki-eiki 2025-04-08 23:00:38 -07:00
Leni Aniva ccdcf018f8
Add curvature to side profile 2025-04-03 11:30:55 -07:00
Leni Aniva bfb4ad6973
Add fine angular tolerance for side guards 2025-03-31 00:22:28 -07:00
Leni Aniva 9d9d59ffeb
Add attachment point wings to Eiki crown 2025-03-30 16:57:03 -07:00
Leni Aniva 704baebd1e
Bent surface for crown face 2025-03-30 00:48:53 -07:00
Leni Aniva aef269e2eb Merge pull request 'chore: Update environment' (#12) from chore/env into main
Reviewed-on: #12
2025-03-29 18:14:18 -07:00
Leni Aniva 46a27ef543
Add keyhole and set tilt to 60deg 2025-03-14 10:25:43 -07:00
Leni Aniva eb6343fa32
Slot-based connectors for crown side guards 2025-03-14 01:52:52 -07:00
Leni Aniva 0991b39d8a
Cleanup dovetail geometry 2025-03-05 09:48:21 -08:00
Leni Aniva e1893d139f
Side guard attachment dovetail 2025-03-03 00:13:41 -08:00
Leni Aniva a74f919a5b
Onbashira rotor-stator mechanism 2025-02-25 21:04:25 -08:00
Leni Aniva f4704b9ad6
fix: Remove shebang in init 2025-02-24 00:54:39 -08:00
Leni Aniva 590033e492
Kanako onbashira barrel 2025-02-24 00:21:31 -08:00
Leni Aniva 396dd997e0
New Eiki crown design using conformal mapping 2025-02-23 22:01:45 -08:00
Leni Aniva 89283efd59
feat: Four side panels in Eiki crown 2025-02-13 00:41:07 -08:00
Leni Aniva 524ab73ea4
Merge branch 'chore/env' into touhou/shiki-eiki 2025-02-12 23:37:27 -08:00
Leni Aniva 6842b0c4c8
chore: Update environment 2025-02-12 23:28:50 -08:00
Leni Aniva 6b0b604ae1
Merge branch 'main' into touhou/shiki-eiki 2025-02-12 22:31:03 -08:00
Leni Aniva 317b187d43
feat: Side guard stub 2025-02-12 22:30:44 -08:00
Leni Aniva 9ff3e72474 Merge pull request 'tool: Light panel' (#9) from tool/lighting into main
Reviewed-on: #9
2025-01-28 17:56:20 -08:00
Leni Aniva 9d78bf604a
feat: Add tripod attachment point 2025-01-28 17:15:38 -08:00
Leni Aniva 596311f326
feat: Improve spacing 2025-01-23 13:52:37 -08:00
Leni Aniva 6384d326c1 Merge pull request 'feat: Add model name prefix to build path' (#10) from lib/build into main
Reviewed-on: #10
2025-01-20 21:47:32 -08:00
Leni Aniva 6470010da6
fix: Model name 2024-12-07 13:47:32 -08:00
Leni Aniva 5ab611666e
Merge branch 'lib/build' into tool/lighting 2024-12-07 13:47:06 -08:00
Leni Aniva a6685e6779
feat: Add model name prefix to build path 2024-12-07 13:46:23 -08:00
Leni Aniva 418e6517a0
fix: Sketch object attribute 2024-12-07 13:44:35 -08:00
Leni Aniva 9563501327 Merge pull request 'feat: Add cq-editor as dev dependency' (#8) from chore/version into main
Reviewed-on: #8
2024-12-07 13:38:04 -08:00
Leni Aniva d1fd830766
feat: Light panel assembly 2024-12-07 12:09:41 -08:00
Leni Aniva 3884d75f1c
chore: Add build function 2024-12-06 16:43:12 -08:00
Leni Aniva ca437c3855
feat: Light panel layer 2024-12-06 16:40:07 -08:00
Leni Aniva 8e4553311b
doc: Instructions for using cq-editor 2024-12-06 12:42:26 -08:00
Leni Aniva 016b717b41
feat: Add cq-editor in dev dependencies 2024-12-06 12:40:57 -08:00
Leni Aniva eace8745f3
chore: Update cadquery 2024-12-06 12:27:26 -08:00
Leni Aniva 70fbe7dcb3
fix: Side hinge plate hole position 2024-11-20 23:42:03 -08:00
Leni Aniva 21b3c98856
feat: Rod assembly 2024-11-20 23:21:37 -08:00
Leni Aniva dbf374fe20
feat: Update crown size and shape 2024-11-19 16:01:06 -08:00
Leni Aniva 9109676502
feat: Dot in Eiki's crown 2024-11-18 20:54:06 -08:00
Leni Aniva 077651e708
feat: Set output prefix 2024-11-18 20:53:46 -08:00
Leni Aniva e02ec4d257
fix: Eiki build target types 2024-11-18 14:54:29 -08:00
Leni Aniva 3ac342a65d
Eiki epaulette 2024-11-18 14:16:35 -08:00
Leni Aniva d910326096
feat: Eiki crown side 2024-11-18 00:36:39 -08:00
Leni Aniva 95313b76eb
feat: Eiki rod 2024-11-17 20:55:53 -08:00
Leni Aniva bfa96e7cef
feat: Rod outline 2024-11-14 13:59:01 -08:00
Leni Aniva fbacd980c0
feat: Shiki Eiki set stub 2024-11-13 22:36:10 -08:00
35 changed files with 11762 additions and 5399 deletions

View File

@ -1,24 +1,74 @@
# Cosplay
This is the design repository for NorCal Hakkero Factory No. 1.
This is the design repository for NorCal Hakkero Factory No. 1, where we use
parametric CAD to make cosplay props. We design cosplay props based on an
engineering point of view.
> NorCal Hakkero Factory № 1
>
> 北加国営八卦炉第一工場
Most cosplay schematics are created with Blender, CadQuery, and Inkscape.
## Development
Most cosplay schematics are created with Blender, CadQuery, and Inkscape. To
enter into a CadQuery environment, install `poetry` and use
```sh
poetry install
poetry shell
```
and this should succeed
```sh
python3 -c "import nhf"
Install `uv`, and then execute
``` sh
uv sync
```
## Testing
To get a development environment, run
``` sh
uv venv
```
Then, either follow the instruction to activate this venv, or install `direnv`
and create the file
``` sh
# .envrc
source .venv/bin/activate
```
Test the environment with `python3 -c "import nhf"`
To visualize an object, create a file `visualize.py`, and run `cq-editor`:
``` sh
python3 -m cq_editor visualize.py
```
### Folder Structure
- `nhf/parts/`: Ready-made parts
- `nhf/$WORK/$CHARACTER`: Design for an individual character
For each individual character, the `__init__.py` script stores the overall build
entry point and the entry point for all unit tests.
### Testing
Run all tests with
``` sh
python3 -m unittest
unittest-parallel
```
## Troubleshooting
### Wayland
If there is the error
```
X Error of failed request: BadWindow (invalid Window parameter)
Major opcode of failed request: 3 (X_GetWindowAttributes)
Resource id in failed request: 0x3
Serial number of failed request: 28
Current serial number in output stream: 29
```
Export the environment variable
``` sh
export QT_QPA_PLATFORM=xcb
```

View File

@ -90,7 +90,7 @@ class Target:
x = (
Cq.Workplane()
.add(x._faces)
.add(x._wires)
.add(x.wires)
.add(x._edges)
)
assert isinstance(x, Cq.Workplane)
@ -214,7 +214,7 @@ class Submodel:
def write_to(self, obj, path: str):
x = self._method(obj)
assert isinstance(x, Model), f"Unexpected type: {type(x)}"
x.build_all(path)
x.build_all(path, prefix=False)
@classmethod
def methods(cls, subject):
@ -271,11 +271,17 @@ class Model:
total += 1
return total
def build_all(self, output_dir: Union[Path, str] = "build", verbose=1):
def build_all(
self,
output_dir: Union[Path, str] = "build",
prefix: bool = True,
verbose=1):
"""
Build all targets in this model and write the results to file
"""
output_dir = Path(output_dir)
if prefix:
output_dir = output_dir / self.name
targets = Target.methods(self)
for t in targets.values():
file_name = t.file_name

View File

@ -25,13 +25,16 @@ class Role(Flag):
PARENT = auto()
CHILD = auto()
CASING = auto()
STATOR = auto()
ROTOR = auto()
BEARING = auto()
# Springs, cushions
DAMPING = auto()
# Main structural support
STRUCTURE = auto()
DECORATION = auto()
ELECTRONIC = auto()
MOTION = auto()
MOTOR = auto()
# Fasteners, etc.
CONNECTION = auto()
@ -59,11 +62,14 @@ ROLE_COLOR_MAP = {
Role.PARENT: _color('blue4', 0.6),
Role.CASING: _color('dodgerblue3', 0.6),
Role.CHILD: _color('darkorange2', 0.6),
Role.STATOR: _color('gray', 0.5),
Role.ROTOR: _color('blue3', 0.5),
Role.BEARING: _color('green3', 0.8),
Role.DAMPING: _color('springgreen', 1.0),
Role.STRUCTURE: _color('gray', 0.4),
Role.DECORATION: _color('lightseagreen', 0.4),
Role.ELECTRONIC: _color('mediumorchid', 0.7),
Role.MOTION: _color('thistle3', 0.7),
Role.MOTOR: _color('thistle3', 0.7),
Role.CONNECTION: _color('steelblue3', 0.8),
Role.HANDLE: _color('tomato4', 0.8),
}
@ -84,6 +90,9 @@ class Material(Enum):
ACRYLIC_TRANSLUSCENT = 1.18, _color('ivory2', 0.8)
ACRYLIC_TRANSPARENT = 1.18, _color('ghostwhite', 0.5)
STEEL_SPRING = 7.8, _color('gray', 0.8)
STEEL_STAINLESS = 7.8, _color('gray', 0.9)
METAL_AL = 2.7, _color('gray', 0.6)
METAL_BRASS = 8.5, _color('gold1', 0.8)
def __init__(self, density: float, color: Cq.Color):
self.density = density
@ -116,6 +125,9 @@ def add_with_material_role(
Cq.Assembly.addS = add_with_material_role
def color_by_material(self: Cq.Assembly) -> Cq.Assembly:
"""
Set colours in an assembly by material
"""
for _, a in self.traverse():
if KEY_MATERIAL not in a.metadata:
continue
@ -123,6 +135,9 @@ def color_by_material(self: Cq.Assembly) -> Cq.Assembly:
return self
Cq.Assembly.color_by_material = color_by_material
def color_by_role(self: Cq.Assembly, avg: bool = True) -> Cq.Assembly:
"""
Set colours in an assembly by role
"""
for _, a in self.traverse():
if KEY_ROLE not in a.metadata:
continue

View File

@ -83,11 +83,16 @@ class BatteryBox18650(Item):
battery_dist: float = 20.18
height: float = 19.66
# space from bottom to battery begin
thickness: float = 1.66
thickness: float = 2.28
battery_diam: float = 18.48
battery_height: float = 68.80
n_batteries: int = 3
battery_gap: float = 2.0
diam_thread: float = 3.0
hole_dy: float = 39.50 / 2
def __post_init__(self):
assert 2 * self.thickness < min(self.length, self.height)
@ -95,13 +100,20 @@ class BatteryBox18650(Item):
def name(self) -> str:
return f"BatteryBox 18650*{self.n_batteries}"
@property
def holes(self) -> list[Cq.Location]:
return [
Cq.Location.from2d(0, self.hole_dy),
Cq.Location.from2d(0, -self.hole_dy),
]
@property
def role(self) -> Role:
return Role.ELECTRONIC
def generate(self) -> Cq.Workplane:
width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam
return (
result = (
Cq.Workplane('XY')
.box(
length=self.length,
@ -117,7 +129,7 @@ class BatteryBox18650(Item):
centered=(True, True, False),
combine='cut',
)
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2)))
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2 + self.battery_gap)))
.rarray(
xSpacing=1,
ySpacing=self.battery_dist,
@ -132,4 +144,16 @@ class BatteryBox18650(Item):
centered=(True, True, False),
combine=True,
)
.copyWorkplane(Cq.Workplane('XY'))
)
hole = Cq.Solid.makeCylinder(
radius=self.diam_thread/2,
height=self.thickness,
)
result -= hole.moved(0, self.hole_dy)
result -= hole.moved(0, -self.hole_dy)
result.tagAbsolute("holeT0", (0, self.hole_dy, self.thickness), direction="+Z")
result.tagAbsolute("holeT1", (0, -self.hole_dy, self.thickness), direction="+Z")
result.tagAbsolute("holeB0", (0, self.hole_dy, 0), direction="-Z")
result.tagAbsolute("holeB1", (0, -self.hole_dy, 0), direction="-Z")
return result

View File

@ -11,6 +11,7 @@ class FlatHeadBolt(Item):
height_head: float
diam_thread: float
height_thread: float
pitch: float = 1.0
@property
def name(self) -> str:
@ -34,7 +35,7 @@ class FlatHeadBolt(Item):
centered=(True, True, False))
)
rod.faces("<Z").tag("tip")
rod.faces(">Z").tag("root")
rod.tagAbsolute("root", (0, 0, self.height_thread), direction="-Z")
rod = rod.union(head.located(Cq.Location((0, 0, self.height_thread))))
return rod

12
nhf/primitive.py Normal file
View File

@ -0,0 +1,12 @@
import cadquery as Cq
def mystery():
return (
Cq.Workplane("XY")
.box(10, 5, 5)
.faces(">Z")
.workplane()
.hole(1)
.edges("|Z")
.fillet(2)
)

0
nhf/tool/__init__.py Normal file
View File

146
nhf/tool/light_panel.py Normal file
View File

@ -0,0 +1,146 @@
from dataclasses import dataclass
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind, submodel
from nhf.parts.box import MountingBox, Hole
from nhf.parts.electronics import ArduinoUnoR3
import nhf.utils
@dataclass
class LightPanel(Model):
# Dimensions of the base panel
length: float = 300.0
width: float = 200.0
attach_height: float = 20.0
attach_diam: float = 8.0
attach_depth: float = 12.7
grid_height: float = 20.0
grid_top_height: float = 5.0
# Distance from grid to edge
grid_margin: float = 20.0
# Number of holes in each row of the grid
grid_holes: int = 9
grid_layers: int = 6
grid_hole_width: float = 15.0
base_thickness: float = 25.4/16
grid_thickness: float = 25.4/4
base_material: Material = Material.WOOD_BIRCH
grid_material: Material = Material.ACRYLIC_TRANSPARENT
controller: ArduinoUnoR3 = ArduinoUnoR3()
def __post_init__(self):
assert self.grid_holes >= 2
super().__init__(name="light-panel")
@property
def grid_spacing_y(self) -> float:
return (self.width - 2 * self.grid_margin - self.grid_thickness) / (self.grid_layers - 1)
@target(name="grid", kind=TargetKind.DXF)
def grid_profile(self):
w = self.length - self.grid_margin * 2
h = self.grid_height + self.grid_top_height
# The width of one hole (w0) satisfies
# n * w0 + (n+1) t = w
# where t is the thickness of the edge
n = self.grid_holes
w0 = self.grid_hole_width
t = (w - n * w0) / (n + 1)
# The spacing is such that the first and last holes are a distance `margin`
# away from the edges, so it satisfies
# t + w0/2 + (n-1) * s + w0/2 + t = w
step = (w - t*2 - w0) / (n - 1)
return (
Cq.Sketch()
.push([(0, h/2)])
.rect(w, h)
.push([
(i * step + t + w0/2 - w/2, self.grid_height/2)
for i in range(0, n)
])
.rect(w0, self.grid_height, mode='s')
)
def grid(self) -> Cq.Workplane:
return (
Cq.Workplane('XY')
.placeSketch(self.grid_profile())
.extrude(self.grid_thickness)
)
@submodel(name="base")
def base(self) -> MountingBox:
xshift = self.length / 2 - self.controller.length - self.grid_margin / 2
yshift = self.grid_margin / 2
holes = [
Hole(
x=x + xshift, y=y + yshift,
diam=self.controller.hole_diam,
tag=f"controller_conn{i}",
)
for i, (x, y) in enumerate(self.controller.holes)
]
return MountingBox(
holes=holes,
hole_diam=self.controller.hole_diam,
length=self.length,
width=self.width,
centred=(True, False),
thickness=self.base_thickness,
)
@target(name="attachment")
def attachment(self) -> Cq.Workplane:
l = self.length / 2
w = self.width / 2
return (
Cq.Workplane('XY')
.box(
l, w, self.attach_height,
centered=(True, True, False),
)
.faces(">Z")
.hole(self.attach_diam, self.attach_depth)
)
def assembly(self) -> Cq.Assembly:
assembly = (
Cq.Assembly()
.addS(
self.base().generate(),
name="base",
role=Role.STRUCTURE,
material=self.base_material,
)
)
# Grid thickness t is fixed, so the spacing of the grid satisfies
# margin + t + (n-1) * spacing + margin = width
spacing = self.grid_spacing_y
shift = self.grid_margin + self.grid_thickness / 2
for i in range(self.grid_layers):
assembly = assembly.addS(
self.grid(),
name=f"grid_{i}",
role=Role.STRUCTURE,
material=self.grid_material,
loc=Cq.Location(0, spacing * i + shift, self.base_thickness, 90, 0, 0),
)
return assembly
if __name__ == '__main__':
import sys
p = LightPanel()
print(p.grid_spacing_y)
if len(sys.argv) == 1:
p.build_all()
sys.exit(0)

View File

@ -1,14 +0,0 @@
#+title: Cosplay: Houjuu Nue
* Controller
This part describes the electrical connections and the microcontroller code.
* Structure
This part describes the 3d printed and laser cut structures. ~structure.blend~
is an overall sketch of the shapes and looks of the wing.
* Pattern
This part describes the sewing patterns.

View File

@ -1,204 +0,0 @@
"""
To build, execute
```
python3 nhf/touhou/houjuu_nue/__init__.py
```
This cosplay consists of 3 components:
## Trident
The trident is composed of individual segments, made of acrylic, and a 3D
printed head (convention rule prohibits metal) with a metallic paint. To ease
transportation, the trident handle has individual segments with threads and can
be assembled on site.
## Snake
A 3D printed snake with a soft material so it can wrap around and bend
## Wings
This is the crux of the cosplay and the most complex component. The wings mount
on a wearable harness. Each wing consists of 4 segments with 3 joints. Parts of
the wing which demands transluscency are created from 1/16" acrylic panels.
These panels serve double duty as the exoskeleton.
The wings are labeled r1,r2,r3,l1,l2,l3. The segments of the wings are labeled
from root to tip s0 (root),
s1, s2, s3. The joints are named (from root to tip)
shoulder, elbow, wrist in analogy with human anatomy.
"""
from dataclasses import dataclass, field
from typing import Optional
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
import nhf.touhou.houjuu_nue.wing as MW
import nhf.touhou.houjuu_nue.trident as MT
import nhf.touhou.houjuu_nue.joints as MJ
import nhf.touhou.houjuu_nue.harness as MH
import nhf.touhou.houjuu_nue.electronics as ME
from nhf.parts.item import Item
import nhf.utils
WING_DEFLECT_ODD = 0.0
WING_DEFLECT_EVEN = 25.0
@dataclass
class Parameters(Model):
"""
Defines dimensions for the Houjuu Nue cosplay
"""
harness: MH.Harness = field(default_factory=lambda: MH.Harness())
wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r1",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(0,1,1,1),
parent_substrate_cull_edges=(0,0,1,0),
),
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=False,
s0_bot_hole=True,
arrow_height=350.0
))
wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r2",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,1),
parent_substrate_cull_edges=(0,0,1,0),
),
electronic_board=ME.ElectronicBoardControl(),
shoulder_angle_bias=WING_DEFLECT_EVEN,
s0_top_hole=True,
s0_bot_hole=True,
))
wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r3",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,0),
parent_substrate_cull_edges=(0,0,1,0),
),
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=True,
s0_bot_hole=False,
))
wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l1",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,0,1,1),
parent_substrate_cull_edges=(1,0,0,0),
),
shoulder_angle_bias=WING_DEFLECT_EVEN,
wrist_angle=-60.0,
s0_top_hole=False,
s0_bot_hole=True,
))
wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l2",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,1),
parent_substrate_cull_edges=(1,0,0,0),
),
wrist_angle=-30.0,
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=True,
s0_bot_hole=True,
))
wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l3",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,0,1),
parent_substrate_cull_edges=(1,0,0,0),
),
shoulder_angle_bias=WING_DEFLECT_EVEN,
wrist_angle=-0.0,
s0_top_hole=True,
s0_bot_hole=False,
))
trident: MT.Trident = field(default_factory=lambda: MT.Trident())
def __post_init__(self):
super().__init__(name="houjuu-nue")
@submodel(name="harness")
def submodel_harness(self) -> Model:
return self.harness
@submodel(name="wing-r1")
def submodel_wing_r1(self) -> Model:
return self.wing_r1
@submodel(name="wing-r2")
def submodel_wing_r2(self) -> Model:
return self.wing_r2
@submodel(name="wing-r3")
def submodel_wing_r3(self) -> Model:
return self.wing_r3
@submodel(name="wing-l1")
def submodel_wing_l1(self) -> Model:
return self.wing_l1
@submodel(name="wing-l2")
def submodel_wing_l2(self) -> Model:
return self.wing_l2
@submodel(name="wing-l3")
def submodel_wing_l3(self) -> Model:
return self.wing_l3
@assembly()
def wings_harness_assembly(self,
parts: Optional[list[str]] = None,
**kwargs) -> Cq.Assembly:
"""
Assembly of harness with all the wings
"""
result = (
Cq.Assembly()
.add(self.harness.assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
.add(self.wing_r1.assembly(parts, root_offset=9, **kwargs), name="wing_r1")
.add(self.wing_r2.assembly(parts, root_offset=7, **kwargs), name="wing_r2")
.add(self.wing_r3.assembly(parts, root_offset=6, **kwargs), name="wing_r3")
.add(self.wing_l1.assembly(parts, root_offset=19, **kwargs), name="wing_l1")
.add(self.wing_l2.assembly(parts, root_offset=20, **kwargs), name="wing_l2")
.add(self.wing_l3.assembly(parts, root_offset=21, **kwargs), name="wing_l3")
)
for tag in ["r1", "r2", "r3", "l1", "l2", "l3"]:
self.harness.add_root_joint_constraint(
result,
"harness/base",
f"wing_{tag}/root",
tag
)
return result.solve()
@submodel(name="trident")
def submodel_trident(self) -> Model:
return self.trident
def stat(self) -> dict[str, float]:
a = self.wings_harness_assembly()
bbox = a.toCompound().BoundingBox()
return {
"wing-span": bbox.xlen,
"wing-depth": bbox.ylen,
"wing-height": bbox.zlen,
"wing-mass": a.total_mass(),
"wing-centre-of-mass": a.centre_of_mass().toTuple(),
"items": Item.count(a),
}
if __name__ == '__main__':
import sys
p = Parameters()
if len(sys.argv) == 1:
p.build_all()
sys.exit(0)
if sys.argv[1] == 'stat':
print(p.stat())
elif sys.argv[1] == 'model':
file_name = sys.argv[2]
a = p.wings_harness_assembly()
a.save(file_name, exportType='STEP')

View File

@ -1,18 +0,0 @@
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
NUT_COMMON = HexNut(
# FIXME: measure
mass=0.0,
diam_thread=4.0,
pitch=0.7,
thickness=3.2,
width=7.0,
)
BOLT_COMMON = FlatHeadBolt(
# FIXME: measure
mass=0.0,
diam_head=8.0,
height_head=2.0,
diam_thread=4.0,
height_thread=20.0,
)

View File

@ -1,68 +0,0 @@
#include <FastLED.h>
// Main LED strip setup
#define LED_PIN 5
#define NUM_LEDS 100
#define LED_PART 50
#define BRIGHTNESS 250
#define LED_TYPE WS2811
CRGB leds[NUM_LEDS];
CRGB color_red;
CRGB color_blue;
CRGB color_green;
#define DIAG_PIN 6
void setup() {
// Calculate colors
hsv2rgb_spectrum(CHSV(4, 255, 100), color_red);
hsv2rgb_spectrum(CHSV(170, 255, 100), color_blue);
hsv2rgb_spectrum(CHSV(90, 255, 100), color_green);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(DIAG_PIN, OUTPUT);
// Main LED strip
FastLED.addLeds<LED_TYPE, LED_PIN, RGB>(leds, NUM_LEDS);
}
void loop() {
fill_segmented(CRGB::Green, CRGB::Orange);
delay(500);
flash(leds, NUM_LEDS, color_red, 10, 20);
delay(500);
flash(leds, NUM_LEDS, color_blue, 10, 20);
delay(500);
}
void fill_segmented(CRGB c1, CRGB c2)
{
//fill_solid(leds, LED_PART, c1);
fill_gradient_RGB(leds, LED_PART, CRGB::Black ,c1);
fill_gradient_RGB(leds + LED_PART, NUM_LEDS - LED_PART, CRGB::Black, c2);
FastLED.show();
}
void flash(CRGB *ptr, uint16_t num, CRGB const& lead, int steps, int step_time)
{
digitalWrite(LED_BUILTIN, LOW);
//fill_solid(leds, NUM_LEDS, CRGB::Black);
for (int i = 0; i < steps; ++i)
{
uint8_t factor = 255 * i / steps;
analogWrite(DIAG_PIN, factor);
CRGB tail = blend(lead, CRGB::Black, factor);
uint16_t front = factor * (int) num / 255;
fill_solid(ptr, front, tail);
//fill_gradient_RGB(ptr, front, tail, lead);
//fill_solid(leds + front, NUM_LEDS - front, CRGB::Black);
FastLED.show();
delay(step_time);
}
fill_gradient_RGB(ptr, num, CRGB::Black, lead);
FastLED.show();
analogWrite(DIAG_PIN, LOW);
}

View File

@ -1,540 +0,0 @@
"""
Electronic components
"""
from dataclasses import dataclass, field
from typing import Optional, Tuple
import math
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.materials import Role, Material
from nhf.parts.box import MountingBox, Hole
from nhf.parts.fibre import tension_fibre
from nhf.parts.item import Item
from nhf.parts.fasteners import FlatHeadBolt, HexNut
from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650
from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON
import nhf.utils
@dataclass(frozen=True)
class LinearActuator(Item):
stroke_length: float
shaft_diam: float = 9.04
front_hole_ext: float = 4.41
front_hole_diam: float = 4.41
front_length: float = 9.55
front_width: float = 9.24
front_height: float = 5.98
segment1_length: float = 37.54
segment1_width: float = 15.95
segment1_height: float = 11.94
segment2_length: float = 37.37
segment2_width: float = 20.03
segment2_height: float = 15.03
back_hole_ext: float = 4.58
back_hole_diam: float = 4.18
back_length: float = 9.27
back_width: float = 10.16
back_height: float = 8.12
@property
def name(self) -> str:
return f"LinearActuator {self.stroke_length}mm"
@property
def role(self) -> Role:
return Role.MOTION
@property
def conn_length(self):
return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext
def generate(self, pos: float=0) -> Cq.Assembly:
assert -1e-6 <= pos <= 1 + 1e-6, f"Illegal position: {pos}"
stroke_x = pos * self.stroke_length
front = (
Cq.Workplane('XZ')
.cylinder(
radius=self.front_width / 2,
height=self.front_height,
centered=True,
)
.box(
length=self.front_hole_ext,
width=self.front_width,
height=self.front_height,
combine=True,
centered=(False, True, True)
)
.copyWorkplane(Cq.Workplane('XZ'))
.cylinder(
radius=self.front_hole_diam / 2,
height=self.front_height,
centered=True,
combine='cut',
)
)
front.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
if stroke_x > 0:
shaft = (
Cq.Workplane('YZ')
.cylinder(
radius=self.shaft_diam / 2,
height=stroke_x,
centered=(True, True, False)
)
)
else:
shaft = None
segment1 = (
Cq.Workplane()
.box(
length=self.segment1_length,
height=self.segment1_width,
width=self.segment1_height,
centered=(False, True, True),
)
)
segment2 = (
Cq.Workplane()
.box(
length=self.segment2_length,
height=self.segment2_width,
width=self.segment2_height,
centered=(False, True, True),
)
)
back = (
Cq.Workplane('XZ')
.cylinder(
radius=self.back_width / 2,
height=self.back_height,
centered=True,
)
.box(
length=self.back_hole_ext,
width=self.back_width,
height=self.back_height,
combine=True,
centered=(False, True, True)
)
.copyWorkplane(Cq.Workplane('XZ'))
.cylinder(
radius=self.back_hole_diam / 2,
height=self.back_height,
centered=True,
combine='cut',
)
)
back.faces(">X").tag("dir")
back.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
result = (
Cq.Assembly()
.add(front, name="front",
loc=Cq.Location((-self.front_hole_ext, 0, 0)))
.add(segment1, name="segment1",
loc=Cq.Location((stroke_x, 0, 0)))
.add(segment2, name="segment2",
loc=Cq.Location((stroke_x + self.segment1_length, 0, 0)))
.add(back, name="back",
loc=Cq.Location((stroke_x + self.segment1_length + self.segment2_length + self.back_hole_ext, 0, 0), (0, 1, 0), 180))
)
if shaft:
result.add(shaft, name="shaft")
return result
@dataclass(frozen=True)
class MountingBracket(Item):
"""
Mounting bracket for a linear actuator
"""
mass: float = 1.6
hole_diam: float = 4.0
width: float = 8.0
height: float = 12.20
thickness: float = 0.98
length: float = 13.00
hole_to_side_ext: float = 8.25
def __post_init__(self):
assert self.hole_to_side_ext - self.hole_diam / 2 > 0
@property
def name(self) -> str:
return f"MountingBracket M{int(self.hole_diam)}"
@property
def role(self) -> Role:
return Role.MOTION
def generate(self) -> Cq.Workplane:
result = (
Cq.Workplane('XY')
.box(
length=self.hole_to_side_ext,
width=self.width,
height=self.height,
centered=(False, True, True)
)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.height,
radius=self.width / 2,
combine=True,
)
.copyWorkplane(Cq.Workplane('XY'))
.box(
length=2 * (self.hole_to_side_ext - self.thickness),
width=self.width,
height=self.height - self.thickness * 2,
combine='cut',
)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.height,
radius=self.hole_diam / 2,
combine='cut'
)
.copyWorkplane(Cq.Workplane('YZ'))
.cylinder(
height=self.hole_to_side_ext * 2,
radius=self.hole_diam / 2,
combine='cut'
)
)
result.copyWorkplane(Cq.Workplane('YZ', origin=(self.hole_to_side_ext, 0, 0))).tagPlane("conn_side")
result.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.height/2))).tagPlane("conn_top")
result.copyWorkplane(Cq.Workplane('YX', origin=(0, 0, -self.height/2))).tagPlane("conn_bot")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid")
return result
LINEAR_ACTUATOR_50 = LinearActuator(
mass=40.8,
stroke_length=50,
shaft_diam=9.05,
front_hole_ext=4.32,
back_hole_ext=4.54,
segment1_length=57.35,
segment1_width=15.97,
segment1_height=11.95,
segment2_length=37.69,
segment2_width=19.97,
segment2_height=14.96,
front_length=9.40,
front_width=9.17,
front_height=6.12,
back_length=9.18,
back_width=10.07,
back_height=8.06,
)
LINEAR_ACTUATOR_30 = LinearActuator(
mass=34.0,
stroke_length=30,
)
LINEAR_ACTUATOR_21 = LinearActuator(
# FIXME: Measure
mass=0.0,
stroke_length=21,
front_hole_ext=4,
back_hole_ext=4,
segment1_length=34,
segment2_length=34,
)
LINEAR_ACTUATOR_10 = LinearActuator(
mass=41.3,
stroke_length=10,
front_hole_ext=4.02,
back_hole_ext=4.67,
segment1_length=13.29,
segment1_width=15.88,
segment1_height=12.07,
segment2_length=42.52,
segment2_width=20.98,
segment2_height=14.84,
)
LINEAR_ACTUATOR_HEX_NUT = HexNut(
mass=0.8,
diam_thread=4,
pitch=0.7,
thickness=4.16,
width=6.79,
)
LINEAR_ACTUATOR_BOLT = FlatHeadBolt(
mass=1.7,
diam_head=6.68,
height_head=2.98,
diam_thread=4.0,
height_thread=15.83,
)
LINEAR_ACTUATOR_BRACKET = MountingBracket()
BATTERY_BOX = BatteryBox18650()
# Acrylic hex nut
ELECTRONIC_MOUNT_HEXNUT = HexNut(
mass=0.8,
diam_thread=4,
pitch=0.7,
thickness=3.57,
width=6.81,
)
@dataclass(kw_only=True, frozen=True)
class Winch:
linear_motion_span: float
actuator: LinearActuator = LINEAR_ACTUATOR_21
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
@dataclass(kw_only=True)
class Flexor:
"""
Actuator assembly which flexes, similar to biceps
"""
motion_span: float
arm_radius: Optional[float] = None
pos_smaller: bool = True
actuator: LinearActuator = LINEAR_ACTUATOR_50
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
# Length of line attached to the flexor
line_length: float = 0.0
line_thickness: float = 0.5
# By how much is the line permitted to slack. This reduces the effective stroke length
line_slack: float = 0.0
def __post_init__(self):
assert self.line_slack <= self.line_length, f"Insufficient length: {self.line_slack} >= {self.line_length}"
assert self.line_slack < self.actuator.stroke_length
@property
def mount_height(self):
return self.bracket.hole_to_side_ext
@property
def d_open(self):
return self.actuator.conn_length + self.actuator.stroke_length + self.line_length - self.line_slack
@property
def d_closed(self):
return self.actuator.conn_length + self.line_length
def open_pos(self) -> Tuple[float, float, float]:
r, phi, r_ = nhf.geometry.contraction_span_pos_from_radius(
d_open=self.d_open,
d_closed=self.d_closed,
theta=math.radians(self.motion_span),
r=self.arm_radius,
smaller=self.pos_smaller,
)
return r, math.degrees(phi), r_
def target_length_at_angle(
self,
angle: float = 0.0
) -> float:
"""
Length of the actuator at some angle
"""
assert 0 <= angle <= self.motion_span
r, phi, rp = self.open_pos()
th = math.radians(phi - angle)
result = math.sqrt(r * r + rp * rp - 2 * r * rp * math.cos(th))
#result = math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2)
assert self.d_closed -1e-6 <= result <= self.d_open + 1e-6,\
f"Illegal length: {result} not in [{self.d_closed}, {self.d_open}]"
return result
def add_to(
self,
a: Cq.Assembly,
target_length: float,
tag_prefix: Optional[str] = None,
tag_hole_front: Optional[str] = None,
tag_hole_back: Optional[str] = None,
tag_dir: Optional[str] = None):
"""
Adds the necessary mechanical components to this assembly. Does not
invoke `a.solve()`.
"""
draft = max(0, target_length - self.d_closed - self.line_length)
pos = draft / self.actuator.stroke_length
line_l = target_length - draft - self.actuator.conn_length
if tag_prefix:
tag_prefix = tag_prefix + "_"
else:
tag_prefix = ""
name_actuator = f"{tag_prefix}actuator"
name_bracket_front = f"{tag_prefix}bracket_front"
name_bracket_back = f"{tag_prefix}bracket_back"
name_bolt_front = f"{tag_prefix}front_bolt"
name_bolt_back = f"{tag_prefix}back_bolt"
name_nut_front = f"{tag_prefix}front_nut"
name_nut_back = f"{tag_prefix}back_nut"
(
a
.add(self.actuator.assembly(pos=pos), name=name_actuator)
.add(self.bracket.assembly(), name=name_bracket_front)
.add(self.bolt.assembly(), name=name_bolt_front)
.add(self.nut.assembly(), name=name_nut_front)
.constrain(f"{name_bolt_front}?root", f"{name_bracket_front}?conn_top",
"Plane", param=0)
.constrain(f"{name_nut_front}?bot", f"{name_bracket_front}?conn_bot",
"Plane")
.add(self.bracket.assembly(), name=name_bracket_back)
.add(self.bolt.assembly(), name=name_bolt_back)
.add(self.nut.assembly(), name=name_nut_back)
.constrain(f"{name_actuator}/back?conn", f"{name_bracket_back}?conn_mid",
"Plane", param=0)
.constrain(f"{name_bolt_back}?root", f"{name_bracket_back}?conn_top",
"Plane", param=0)
.constrain(f"{name_nut_back}?bot", f"{name_bracket_back}?conn_bot",
"Plane")
)
if self.line_length == 0.0:
a.constrain(
f"{name_actuator}/front?conn",
f"{name_bracket_front}?conn_mid",
"Plane", param=0)
else:
(
a
.addS(tension_fibre(
length=line_l,
hole_diam=self.nut.diam_thread,
thickness=self.line_thickness,
), name="fibre", role=Role.CONNECTION)
.constrain(
f"{name_actuator}/front?conn",
"fibre?male",
"Plane"
)
.constrain(
f"{name_bracket_front}?conn_mid",
"fibre?female",
"Plane"
)
)
if tag_hole_front:
a.constrain(tag_hole_front, f"{name_bracket_front}?conn_side", "Plane")
if tag_hole_back:
a.constrain(tag_hole_back, f"{name_bracket_back}?conn_side", "Plane")
if tag_dir:
a.constrain(tag_dir, f"{name_bracket_front}?conn_mid", "Axis", param=0)
@dataclass
class ElectronicBoard(Model):
name: str = "electronic-board"
nut: HexNut = NUT_COMMON
bolt: FlatHeadBolt = BOLT_COMMON
length: float = 70.0
width: float = 170.0
mount_holes: list[Hole] = field(default_factory=lambda: [
Hole(x=25, y=75),
Hole(x=25, y=-75),
Hole(x=-25, y=75),
Hole(x=-25, y=-75),
])
panel_thickness: float = 25.4 / 16
mount_panel_thickness: float = 25.4 / 4
material: Material = Material.WOOD_BIRCH
@property
def mount_hole_diam(self) -> float:
return self.bolt.diam_thread
def __post_init__(self):
super().__init__(name=self.name)
def panel(self) -> MountingBox:
return MountingBox(
holes=self.mount_holes,
hole_diam=self.mount_hole_diam,
length=self.length,
width=self.width,
centred=(True, True),
thickness=self.panel_thickness,
generate_reverse_tags=True,
)
def assembly(self) -> Cq.Assembly:
panel = self.panel()
result = (
Cq.Assembly()
.addS(panel.generate(), name="panel",
role=Role.ELECTRONIC | Role.STRUCTURE, material=self.material)
)
for hole in self.mount_holes:
bolt_name = f"{hole.tag}_bolt"
(
result
.add(self.bolt.assembly(), name=bolt_name)
.constrain(
f"{bolt_name}?root",
f"panel?{hole.tag}",
"Plane", param=0
)
)
return result.solve()
@dataclass
class ElectronicBoardBattery(ElectronicBoard):
name: str = "electronic-board-battery"
battery_box: BatteryBox18650 = BATTERY_BOX
@submodel(name="panel")
def panel_out(self) -> MountingBox:
return self.panel()
@dataclass
class ElectronicBoardControl(ElectronicBoard):
name: str = "electronic-board-control"
controller_datum: Cq.Location = Cq.Location.from2d(-25, 23, -90)
controller: ArduinoUnoR3 = ArduinoUnoR3()
def panel(self) -> MountingBox:
box = super().panel()
def transform(i, x, y):
pos = self.controller_datum * Cq.Location.from2d(x, self.controller.width - y)
x, y = pos.to2d_pos()
return Hole(
x=x, y=y,
diam=self.controller.hole_diam,
tag=f"controller_conn{i}",
)
box.holes = box.holes.copy() + [
transform(i, x, y)
for i, (x, y) in enumerate(self.controller.holes)
]
return box
@submodel(name="panel")
def panel_out(self) -> MountingBox:
return self.panel()
def assembly(self) -> Cq.Assembly:
result = super().assembly()
result.add(self.controller.assembly(), name="controller")
for i in range(len(self.controller.holes)):
result.constrain(f"controller?conn{i}", f"panel?controller_conn{i}", "Plane")
return result.solve()
@dataclass(frozen=True)
class LightStrip:
width: float = 10.0
height: float = 4.5

View File

@ -1,204 +0,0 @@
from dataclasses import dataclass, field
import cadquery as Cq
from nhf.parts.joints import HirthJoint
from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.touhou.houjuu_nue.joints import RootJoint
from nhf.parts.box import MountingBox
import nhf.utils
@dataclass(frozen=True, kw_only=True)
class Mannequin:
"""
A mannequin for calibration
"""
shoulder_width: float = 400
shoulder_to_waist: float = 440
waist_width: float = 250
head_height: float = 220.0
neck_height: float = 105.0
neck_diam: float = 140
head_diam: float = 210
torso_thickness: float = 150
def generate(self) -> Cq.Workplane:
head_neck = (
Cq.Workplane("XY")
.cylinder(
radius=self.neck_diam/2,
height=self.neck_height,
centered=(True, True, False))
.faces(">Z")
.workplane()
.cylinder(
radius=self.head_diam/2,
height=self.head_height,
combine=True, centered=(True, True, False))
)
result = (
Cq.Workplane("XY")
.rect(self.waist_width, self.torso_thickness)
.workplane(offset=self.shoulder_to_waist)
.rect(self.shoulder_width, self.torso_thickness)
.loft(combine=True)
.union(head_neck.translate((0, 0, self.shoulder_to_waist)))
)
return result.translate((0, self.torso_thickness / 2, 0))
BASE_POS_X = 70.0
BASE_POS_Y = 100.0
@dataclass(kw_only=True)
class Harness(Model):
thickness: float = 25.4 / 8
width: float = 200.0
height: float = 304.8
fillet: float = 10.0
wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [
("r1", BASE_POS_X, BASE_POS_Y),
("l1", -BASE_POS_X, BASE_POS_Y),
("r2", BASE_POS_X, 0),
("l2", -BASE_POS_X, 0),
("r3", BASE_POS_X, -BASE_POS_Y),
("l3", -BASE_POS_X, -BASE_POS_Y),
])
root_joint: RootJoint = field(default_factory=lambda: RootJoint())
mannequin: Mannequin = Mannequin()
def __post_init__(self):
super().__init__(name="harness")
@submodel(name="bridge-pair-horizontal")
def bridge_pair_horizontal(self) -> MountingBox:
return self.root_joint.bridge_pair_horizontal(centre_dx=BASE_POS_X * 2)
@submodel(name="bridge-pair-vertical")
def bridge_pair_vertical(self) -> MountingBox:
return self.root_joint.bridge_pair_vertical(centre_dy=BASE_POS_Y)
@target(name="profile", kind=TargetKind.DXF)
def profile(self) -> Cq.Sketch:
"""
Creates the harness shape
"""
w, h = self.width / 2, self.height / 2
sketch = (
Cq.Sketch()
.polygon([
(w, h),
(w, -h),
(-w, -h),
(-w, h),
#(0.7 * w, h),
#(w, 0),
#(0.7 * w, -h),
#(0.7 * -w, -h),
#(-w, 0),
#(0.7 * -w, h),
])
#.rect(self.harness_width, self.harness_height)
.vertices()
.fillet(self.fillet)
)
for tag, x, y in self.wing_base_pos:
conn = [(px + x, py + y) for px, py in self.root_joint.corner_pos()]
sketch = (
sketch
.push(conn)
.tag(tag)
.circle(self.root_joint.corner_hole_diam / 2, mode='s')
.reset()
)
return sketch
def surface(self) -> Cq.Workplane:
"""
Creates the harness shape
"""
result = (
Cq.Workplane('XZ')
.placeSketch(self.profile())
.extrude(self.thickness)
)
result.faces(">Y").tag("mount")
plane = result.faces(">Y").workplane()
for tag, x, y in self.wing_base_pos:
conn = [(px + x, py + y) for px, py
in self.root_joint.corner_pos()]
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPlane(f"{tag}_{i}")
return result
def add_root_joint_constraint(
self,
a: Cq.Assembly,
harness_tag: str,
joint_tag: str,
mount_tag: str):
for i in range(4):
a.constrain(f"{harness_tag}?{mount_tag}_{i}", f"{joint_tag}/parent?h{i}", "Point")
@assembly()
def assembly(self, with_root_joint: bool = False) -> Cq.Assembly:
harness = self.surface()
mannequin_z = self.mannequin.shoulder_to_waist * 0.6
result = (
Cq.Assembly()
.addS(
harness, name="base",
material=Material.WOOD_BIRCH,
role=Role.STRUCTURE)
.constrain("base", "Fixed")
.addS(
self.mannequin.generate(),
name="mannequin",
role=Role.FIXTURE,
loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180))
.constrain("mannequin", "Fixed")
)
bridge_h = self.bridge_pair_horizontal().generate()
for i in [1,2,3]:
name = f"r{i}l{i}_bridge"
(
result
.addS(
bridge_h, name=name,
role=Role.FIXTURE,
material=Material.WOOD_BIRCH,
)
.constrain(f"{name}?conn0_rev", f"base?r{i}_1", "Point")
.constrain(f"{name}?conn1_rev", f"base?l{i}_0", "Point")
.constrain(f"{name}?conn2_rev", f"base?l{i}_3", "Point")
.constrain(f"{name}?conn3_rev", f"base?r{i}_2", "Point")
)
bridge_v = self.bridge_pair_vertical().generate()
(
result
.addS(bridge_v, name="r1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("r1_bridge?conn0_rev", "base?r1_3", 'Plane')
.constrain("r1_bridge?conn1_rev", "base?r2_0", 'Plane')
.addS(bridge_v, name="r2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("r2_bridge?conn0_rev", "base?r2_3", 'Plane')
.constrain("r2_bridge?conn1_rev", "base?r3_0", 'Plane')
.addS(bridge_v, name="l1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("l1_bridge?conn0_rev", "base?l1_2", 'Plane')
.constrain("l1_bridge?conn1_rev", "base?l2_1", 'Plane')
.addS(bridge_v, name="l2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("l2_bridge?conn0_rev", "base?l2_2", 'Plane')
.constrain("l2_bridge?conn1_rev", "base?l3_1", 'Plane')
)
if with_root_joint:
for name in ["l1", "l2", "l3", "r1", "r2", "r3"]:
result.addS(
self.root_joint.assembly(), name=name,
role=Role.PARENT,
material=Material.PLASTIC_PLA)
self.add_root_joint_constraint(result, "base", name, name)
result.solve()
return result

File diff suppressed because it is too large Load Diff

View File

@ -1,130 +0,0 @@
import unittest
import cadquery as Cq
import nhf.touhou.houjuu_nue as M
import nhf.touhou.houjuu_nue.joints as MJ
import nhf.touhou.houjuu_nue.electronics as ME
from nhf.checks import pairwise_intersection
class TestElectronics(unittest.TestCase):
def test_actuator_length(self):
self.assertAlmostEqual(
ME.LINEAR_ACTUATOR_50.conn_length, 103.9
)
self.assertAlmostEqual(
ME.LINEAR_ACTUATOR_30.conn_length, 83.9
)
self.assertAlmostEqual(
ME.LINEAR_ACTUATOR_10.conn_length, 64.5
)
self.assertAlmostEqual(
ME.LINEAR_ACTUATOR_21.conn_length, 76.0
)
def test_flexor(self):
flexor = ME.Flexor(
motion_span=60,
)
self.assertAlmostEqual(
flexor.target_length_at_angle(0),
flexor.actuator.stroke_length + flexor.actuator.conn_length)
self.assertAlmostEqual(
flexor.target_length_at_angle(flexor.motion_span),
flexor.actuator.conn_length)
class TestJoints(unittest.TestCase):
def test_shoulder_collision_of_torsion_joint(self):
j = MJ.ShoulderJoint()
assembly = j.torsion_joint.rider_track_assembly()
self.assertEqual(pairwise_intersection(assembly), [])
def test_shoulder_collision_0(self):
j = MJ.ShoulderJoint()
assembly = j.assembly()
self.assertEqual(pairwise_intersection(assembly), [])
def test_shoulder_align(self):
j = MJ.ShoulderJoint()
a = j.assembly()
l_t_c0 = a.get_abs_location("parent_top/lip?conn0")
l_b_c0 = a.get_abs_location("parent_bot/lip?conn0")
v = l_t_c0 - l_b_c0
self.assertAlmostEqual(v.x, 0)
self.assertAlmostEqual(v.y, 0)
def test_shoulder_joint_dist(self):
"""
Tests the arm radius
"""
j = MJ.ShoulderJoint()
for deflection in [0, 40, j.angle_max_deflection]:
with self.subTest(deflection=deflection):
a = j.assembly(deflection=deflection)
# Axle
o = a.get_abs_location("parent_top/track?spring")
l_c1 = a.get_abs_location("parent_top/lip?conn0")
l_c2= a.get_abs_location("parent_top/lip?conn1")
v_c = 0.5 * ((l_c1 - o) + (l_c2 - o))
v_c.z = 0
self.assertAlmostEqual(v_c.Length, j.parent_lip_ext)
def test_disk_collision_0(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=0)
self.assertEqual(pairwise_intersection(assembly), [])
def test_disk_collision_mid(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=j.movement_angle / 2)
self.assertEqual(pairwise_intersection(assembly), [])
def test_disk_collision_max(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=j.movement_angle)
self.assertEqual(pairwise_intersection(assembly), [])
def test_elbow_joint_dist(self):
"""
Tests the arm radius
"""
j = MJ.ElbowJoint()
for angle in [0, 10, 20, j.disk_joint.movement_angle]:
with self.subTest(angle=angle):
a = j.assembly(angle=angle)
o = a.get_abs_location("child/disk?mate_bot")
l_c1 = a.get_abs_location("child/lip?conn_top0")
l_c2 = a.get_abs_location("child/lip?conn_bot0")
v_c = 0.5 * ((l_c1 - o) + (l_c2 - o))
v_c.z = 0
self.assertAlmostEqual(v_c.Length, j.child_arm_radius)
l_p1 = a.get_abs_location("parent_upper/lip?conn_top0")
l_p2 = a.get_abs_location("parent_upper/lip?conn_bot0")
v_p = 0.5 * ((l_p1 - o) + (l_p2 - o))
v_p.z = 0
self.assertAlmostEqual(v_p.Length, j.parent_arm_radius)
class Test(unittest.TestCase):
def test_hs_joint_parent(self):
p = M.Parameters()
obj = p.harness.hs_joint_parent()
self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece")
def test_wings_assembly(self):
p = M.Parameters()
p.wings_harness_assembly()
def test_trident_assembly(self):
p = M.Parameters()
assembly = p.trident.assembly()
bbox = assembly.toCompound().BoundingBox()
length = bbox.zlen
self.assertGreater(length, 1300)
self.assertLess(length, 1700)
#def test_assemblies(self):
# p = M.Parameters()
# p.check_all()
if __name__ == '__main__':
unittest.main()

View File

@ -1,88 +0,0 @@
import math
from dataclasses import dataclass, field
import cadquery as Cq
from nhf import Material, Role
from nhf.parts.handle import Handle, BayonetMount
from nhf.build import Model, target, assembly
import nhf.utils
@dataclass
class Trident(Model):
handle: Handle = field(default_factory=lambda: Handle(
diam=38,
diam_inner=38-2 * 25.4/8,
diam_connector_internal=18,
simplify_geometry=False,
mount=BayonetMount(n_pin=3),
))
terminal_height: float = 80
terminal_hole_diam: float = 24
terminal_bottom_thickness: float = 10
segment_length: float = 24 * 25.4
@target(name="handle-connector")
def handle_connector(self):
return self.handle.connector()
@target(name="handle-insertion")
def handle_insertion(self):
return self.handle.insertion()
@target(name="proto-handle-terminal-connector", prototype=True)
def proto_handle_connector(self):
return self.handle.one_side_connector(height=15)
@target(name="handle-terminal-connector")
def handle_terminal_connector(self):
result = self.handle.one_side_connector(height=self.terminal_height)
#result.faces("<Z").circle(radius=25/2).cutThruAll()
h = self.terminal_height + self.handle.insertion_length - self.terminal_bottom_thickness
result = result.faces(">Z").hole(self.terminal_hole_diam, depth=h)
return result
@assembly()
def assembly(self):
def segment():
return self.handle.segment(self.segment_length)
terminal = (
self.handle
.one_side_connector(height=self.terminal_height)
.faces(">Z")
.hole(15, self.terminal_height + self.handle.insertion_length - 10)
)
mat_c = Material.PLASTIC_PLA
mat_i = Material.RESIN_TOUGH_1500
mat_s = Material.ACRYLIC_BLACK
role_i = Role.CONNECTION
role_c = Role.CONNECTION
role_s = Role.STRUCTURE
a = (
Cq.Assembly()
.addS(self.handle.insertion(), name="i0",
material=mat_i, role=role_i)
.constrain("i0", "Fixed")
.addS(segment(), name="s1",
material=mat_s, role=role_s)
.constrain("i0?rim", "s1?mate1", "Plane", param=0)
.addS(self.handle.insertion(), name="i1",
material=mat_i, role=role_i)
.addS(self.handle.connector(), name="c1",
material=mat_c, role=role_c)
.addS(self.handle.insertion(), name="i2",
material=mat_i, role=role_i)
.constrain("s1?mate2", "i1?rim", "Plane", param=0)
.constrain("i1?mate", "c1?mate1", "Plane")
.constrain("i2?mate", "c1?mate2", "Plane")
.addS(segment(), name="s2",
material=mat_s, role=role_s)
.constrain("i2?rim", "s2?mate1", "Plane", param=0)
.addS(self.handle.insertion(), name="i3",
material=mat_i, role=role_i)
.constrain("s2?mate2", "i3?rim", "Plane", param=0)
.addS(self.handle.one_side_connector(), name="head",
material=mat_c, role=role_c)
.constrain("i3?mate", "head?mate", "Plane")
.addS(terminal, name="terminal",
material=mat_c, role=role_c)
.constrain("i0?mate", "terminal?mate", "Plane")
)
return a.solve()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
from dataclasses import dataclass, field
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
import nhf.touhou.shiki_eiki.rod as MR
import nhf.touhou.shiki_eiki.crown as MC
import nhf.touhou.shiki_eiki.epaulette as ME
import nhf.utils
@dataclass
class Parameters(Model):
rod: MR.Rod = field(default_factory=lambda: MR.Rod())
crown: MC.Crown = field(default_factory=lambda: MC.Crown())
epaulette_ze: ME.Epaulette = field(default_factory=lambda: ME.Epaulette(side="ze"))
epaulette_hi: ME.Epaulette = field(default_factory=lambda: ME.Epaulette(side="hi"))
def __post_init__(self):
super().__init__(name="shiki-eiki")
@submodel(name="rod")
def submodel_rod(self) -> Model:
return self.rod
@submodel(name="crown")
def submodel_crown(self) -> Model:
return self.crown
@submodel(name="epaulette_ze")
def submodel_epaulette_ze(self) -> Model:
return self.epaulette_ze
@submodel(name="epaulette_hi")
def submodel_epaulette_hi(self) -> Model:
return self.epaulette_hi
if __name__ == '__main__':
import sys
p = Parameters()
if len(sys.argv) == 1:
p.build_all()
sys.exit(0)

View File

@ -0,0 +1,758 @@
from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind
import nhf.utils
import math
from typing import Optional
from dataclasses import dataclass, field
from enum import Enum
import cadquery as Cq
class AttachPoint(Enum):
DOVETAIL_IN = 1
DOVETAIL_OUT = 2
NONE = 3
# Inset slot for front surface attachment j
SLOT = 4
@dataclass
class Crown(Model):
facets: int = 5
# Lower circumference
base_circ: float = 538.0
# Upper circumference, at the middle
tilt_circ: float = 640.0
front_base_circ: float = (640.0 + 538.0) / 2
# Total height
height: float = 120.0
# Front guard has a wing that inserts into the side guards.
front_wing_angle: float = 9.0
front_wing_dh: float = 40.0
front_wing_height: float = 20.0
margin: float = 10.0
thickness: float = 0.4 # 26 Gauge
side_guard_thickness: float = 15.0
side_guard_channel_radius: float = 90
side_guard_channel_height: float = 10
side_guard_hole_height: float = 15.0
side_guard_hole_diam: float = 1.5
side_guard_dovetail_height: float = 30.0
side_guard_slot_width: float = 22.0
side_guard_slot_angle: float = 18.0
# brass insert thickness
slot_thickness: float = 2.0
slot_width: float = 20.0
slot_tilt: float = 60
material: Material = Material.METAL_BRASS
material_side: Material = Material.PLASTIC_PLA
def __post_init__(self):
super().__init__(name="crown")
assert self.tilt_circ > self.base_circ
assert self.facet_width_upper / 2 > self.height / 2, "Top angle must be > 90 degrees"
assert self.side_guard_channel_radius > self.radius_lower
assert self.front_wing_angle < 180 / self.facets
assert self.front_wing_dh + self.front_wing_height < self.height
assert self.slot_phi < 2 * math.pi / self.facets
@property
def facet_width_lower(self):
return self.base_circ / self.facets
@property
def facet_width_upper(self):
return self.tilt_circ / self.facets
@property
def radius_lower(self):
return self.base_circ / (2 * math.pi)
@property
def radius_middle(self):
return self.tilt_circ / (2 * math.pi)
@property
def radius_upper(self):
return (self.tilt_circ + (self.tilt_circ - self.base_circ)) / (2 * math.pi)
@property
def radius_lower_front(self):
return self.front_base_circ / (2 * math.pi)
@property
def radius_middle_front(self):
return self.radius_lower_front + (self.radius_middle - self.radius_lower)
@property
def radius_upper_front(self):
return self.radius_lower_front + (self.radius_upper - self.radius_lower)
@property
def slot_r0(self):
return self.radius_lower + self.thickness / 2
@property
def slot_r1(self):
return self.radius_upper + self.thickness / 2
@property
def slot_h0(self) -> float:
"""
Phantom height formed by similar triangle, i.e. h0 in
(h0 + h) / r2 = h0 / r1
"""
rat = self.slot_r0 / (self.slot_r1 - self.slot_r0)
return self.height * rat
@property
def slot_outer_h0(self):
rat = (self.slot_r0 + self.side_guard_thickness) / (self.slot_r1 - self.slot_r0)
return self.height * rat
@property
def slot_theta(self) -> float:
"""
Cone tilt, related to other quantities by
h0 = r1 * cot theta
"""
h = self.height
return math.atan(self.slot_r0 / (self.height + self.slot_h0))
@property
def slot_phi(self) -> float:
"""
When a slice of the crown is expanded (via Gauss's Theorema Egregium),
it does not form a full circle. phi is the angle of one of the slices.
Note that on the cone itself, the angular slice is `2 pi / n` which `n`
is the number of sides.
"""
arc = self.slot_r0 * math.pi * 2 / self.facets
rho = self.slot_h0 / math.cos(self.slot_theta)
return arc / rho
def profile_base(self) -> Cq.Sketch:
# Generate a conical pentagonal shape
y0 = self.slot_h0 / math.cos(self.slot_theta)
yh = (self.height/2 + self.slot_h0) / math.cos(self.slot_theta)
yq = (self.height*3/4 + self.slot_h0) / math.cos(self.slot_theta)
y1 = (self.height + self.slot_h0) / math.cos(self.slot_theta)
phi2 = self.slot_phi / 2
return (
Cq.Sketch()
.segment(
(y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
(yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
)
.arc(
(yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
(yq * math.sin(phi2/2), -y0 + yq * math.cos(phi2/2)),
(0, y1 - y0),
)
.arc(
(-yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
(-yq * math.sin(phi2/2), -y0 + yq * math.cos(phi2/2)),
(0, y1 - y0),
)
.segment(
(-y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
(-yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
)
.arc(
(y0 * math.sin(phi2), -y0 + y0 * math.cos(phi2)),
(0, 0),
(-y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
)
.assemble()
)
@target(name="eye", kind=TargetKind.DXF)
def profile_eye(self) -> Cq.Sketch:
"""
deprecated
"""
dy = self.facet_width_upper * 0.1
y_tip = self.height - self.margin
eye = (
Cq.Sketch()
.segment(
(0, y_tip),
(dy, y_tip - dy),
)
.segment(
(0, y_tip),
(-dy, y_tip - dy),
)
.bezier([
(dy, y_tip - dy),
(dy/2, y_tip - dy*.6),
(dy/4, y_tip - dy/2),
(0, y_tip - dy/2),
])
.bezier([
(0, y_tip - dy/2),
(-dy/4, y_tip - dy/2),
(-dy/2, y_tip - dy*.6),
(-dy, y_tip - dy),
])
.assemble()
)
return eye
@target(name="dot", kind=TargetKind.DXF)
def profile_dot(self) -> Cq.Sketch:
return (
Cq.Sketch()
.circle(self.margin / 2)
)
def profile_front_wing(self, mirror: bool) -> Cq.Sketch:
"""
These two wings help the front profile attach
"""
hw = self.front_wing_height / math.cos(self.slot_theta)
hw0 = (self.front_wing_dh + self.slot_h0) / math.cos(self.slot_theta)
hw1 = hw0 + hw
y0 = self.slot_h0 / math.cos(self.slot_theta)
# Calculate angle of wing analogously to `this.slot_phi`. This arc's
# radius is hw0.
wing_arc = self.slot_r0 * math.radians(self.front_wing_angle)
phi_w = wing_arc / hw0
sign = -1 if mirror else 1
phi2 = self.slot_phi / 2
return (
Cq.Sketch()
.segment(
(sign * hw0 * math.sin(phi2), -y0 + hw0 * math.cos(phi2)),
(sign * hw1 * math.sin(phi2), -y0 + hw1 * math.cos(phi2)),
)
.segment(
(sign * hw0 * math.sin(phi2+phi_w), -y0 + hw0 * math.cos(phi2+phi_w)),
(sign * hw1 * math.sin(phi2+phi_w), -y0 + hw1 * math.cos(phi2+phi_w)),
)
.arc(
(sign * hw0 * math.sin(phi2), -y0 + hw0 * math.cos(phi2)),
(sign * hw0 * math.sin(phi2+phi_w/2), -y0 + hw0 * math.cos(phi2+phi_w/2)),
(sign * hw0 * math.sin(phi2+phi_w), -y0 + hw0 * math.cos(phi2+phi_w)),
)
.arc(
(sign * hw1 * math.sin(phi2), -y0 + hw1 * math.cos(phi2)),
(sign * hw1 * math.sin(phi2+phi_w/2), -y0 + hw1 * math.cos(phi2+phi_w/2)),
(sign * hw1 * math.sin(phi2+phi_w), -y0 + hw1 * math.cos(phi2+phi_w)),
)
.assemble()
)
@target(name="front", kind=TargetKind.DXF)
def profile_front(self) -> Cq.Sketch:
"""
Front profile slots into holes on the side guards
"""
profile_base = (
self.profile_base()
.boolean(self.profile_front_wing(False), mode='a')
.boolean(self.profile_front_wing(True), mode='a')
)
dx_l = self.facet_width_lower
dx_u = self.facet_width_upper
dy = self.height
window_length = dy / 5
window_height = self.margin / 2
window = (
Cq.Sketch()
.rect(window_length, window_height)
)
window_p1 = Cq.Location.from2d(
dx_u/2 - self.margin - window_length * 0.4,
dy/2 + self.margin/2,
math.degrees(math.atan2(dy/2, -dx_u/2) * 0.95),
)
window_p2 = Cq.Location.from2d(
dx_l/2 - self.margin + window_length * 0.15,
window_length/2 + self.margin,
math.degrees(math.atan2(dy/2, (dx_u-dx_l)/2)),
)
# Carve the scale
z = dy * 1/32 # "Pen" Thickness
scale_pan_x = dx_l / 2 * 0.6
scale_pan_y = dy / 2 * 0.7
pan_dx = dx_l * 1/4
pan_dy = dy * 1/16
scale_pan = (
Cq.Sketch()
.arc(
(- pan_dx/2, pan_dy),
(0, 0),
(+ pan_dx/2, pan_dy),
)
.segment(
(+pan_dx/2, pan_dy),
(+pan_dx/2 - z, pan_dy),
)
.arc(
(-pan_dx/2 + z, pan_dy),
(0, z),
(+pan_dx/2 - z, pan_dy),
)
.segment(
(-pan_dx/2, pan_dy),
(-pan_dx/2 + z, pan_dy),
)
.assemble()
)
loc_scale_pan = Cq.Location.from2d(scale_pan_x, scale_pan_y)
loc_scale_pan2 = Cq.Location.from2d(-scale_pan_x, scale_pan_y)
scale_base_y = dy / 2 * 0.36
scale_base_x = dx_l / 10
assert scale_base_y < scale_pan_y
assert scale_base_x < scale_pan_x
scale_body = (
Cq.Sketch()
.arc(
(scale_pan_x, scale_pan_y),
(0, scale_base_y),
(-scale_pan_x, scale_pan_y),
)
.segment(
(-scale_pan_x, scale_pan_y),
(-scale_pan_x+z, scale_pan_y+z),
)
.arc(
(scale_pan_x - z, scale_pan_y+z),
(0, scale_base_y + z),
(-scale_pan_x + z, scale_pan_y+z),
)
.segment(
(scale_pan_x, scale_pan_y),
(scale_pan_x-z, scale_pan_y+z),
)
.assemble()
.polygon([
(scale_base_x, scale_base_y + z/2),
(scale_base_x, self.margin),
(scale_base_x-z, self.margin),
(scale_base_x-z, scale_base_y-z),
(-scale_base_x+z, scale_base_y-z),
(-scale_base_x+z, self.margin),
(-scale_base_x, self.margin),
(-scale_base_x, scale_base_y + z/2),
], mode='a')
)
# Needle
needle_y_top = dy - self.margin
needle_y_mid = dy * 0.7
needle_dx = scale_base_x * 2
y_shoulder = needle_y_mid - z * 2
needle = (
Cq.Sketch()
.segment(
(0, needle_y_mid),
(z, y_shoulder),
)
.segment(
(z, y_shoulder),
(z, scale_base_y),
)
.segment(
(z, scale_base_y),
(-z, scale_base_y),
)
.segment(
(-z, y_shoulder),
(-z, scale_base_y),
)
.segment(
(-z, y_shoulder),
(0, needle_y_mid),
)
.assemble()
)
z2 = z * 2
y1 = needle_y_mid + z2
needle_head = (
Cq.Sketch()
.segment(
(z, needle_y_mid),
(z, y1),
)
.segment(
(-z, needle_y_mid),
(-z, y1),
)
# Outer edge
.bezier([
(0, needle_y_top),
(0, (needle_y_top + needle_y_mid)/2),
(needle_dx, (needle_y_top + needle_y_mid)/2),
(z, needle_y_mid),
])
.bezier([
(0, needle_y_top),
(0, (needle_y_top + needle_y_mid)/2),
(-needle_dx, (needle_y_top + needle_y_mid)/2),
(-z, needle_y_mid),
])
# Inner edge
.bezier([
(0, needle_y_top - z2),
(0, (needle_y_top + needle_y_mid)/2),
(needle_dx-z2*2, (needle_y_top + needle_y_mid)/2),
(z, y1),
])
.bezier([
(0, needle_y_top - z2),
(0, (needle_y_top + needle_y_mid)/2),
(-needle_dx+z2*2, (needle_y_top + needle_y_mid)/2),
(-z, y1),
])
.assemble()
)
return (
profile_base
.boolean(window.moved(window_p1), mode='s')
.boolean(window.moved(window_p1.flip_x()), mode='s')
.boolean(window.moved(window_p2), mode='s')
.boolean(window.moved(window_p2.flip_x()), mode='s')
.boolean(scale_pan.moved(loc_scale_pan), mode='s')
.boolean(scale_pan.moved(loc_scale_pan2), mode='s')
.boolean(scale_body, mode='s')
.boolean(needle, mode='s')
.boolean(needle_head, mode='s')
.clean()
)
@target(name="side-guard", kind=TargetKind.DXF)
def profile_side_guard(self) -> Cq.Sketch:
dx = self.facet_width_lower / 2
dy = self.height
# Main control points
p_mid = Cq.Location.from2d(0, 0.5 * dy)
p_mid_v = Cq.Location.from2d(10/57 * dx, 0)
p_top1 = Cq.Location.from2d(0.408 * dx, 5/24 * dy)
p_top1_v = Cq.Location.from2d(0.13 * dx, 0)
p_top2 = Cq.Location.from2d(0.737 * dx, 0.255 * dy)
p_top2_c1 = p_top2 * Cq.Location.from2d(-0.105 * dx, 0.033 * dy)
p_top2_c2 = p_top2 * Cq.Location.from2d(-0.053 * dx, -0.09 * dy)
p_top3 = Cq.Location.from2d(0.929 * dx, 0.145 * dy)
p_top3_v = Cq.Location.from2d(0.066 * dx, 0.033 * dy)
p_top4 = Cq.Location.from2d(0.85 * dx, 0.374 * dy)
p_top4_v = Cq.Location.from2d(-0.053 * dx, 0.008 * dy)
p_top5 = Cq.Location.from2d(0.54 * dx, 0.349 * dy)
p_top5_c1 = p_top5 * Cq.Location.from2d(0.103 * dx, 0.017 * dy)
p_top5_c2 = p_top5 * Cq.Location.from2d(0.158 * dx, 0.034 * dy)
p_base_c = Cq.Location.from2d(1.5 * dx, 0.55 * dy)
y0 = self.slot_outer_h0 / math.cos(self.slot_theta)
phi2 = self.slot_phi / 2
p_base = Cq.Location.from2d(y0 * math.sin(phi2), -y0 + y0 * math.cos(phi2))
bezier_groups = [
[
p_base,
p_base_c,
p_top5_c2,
p_top5,
],
[
p_top5,
p_top5_c1,
p_top4 * p_top4_v,
p_top4,
],
[
p_top4,
p_top4 * p_top4_v.inverse.scale(4),
p_top3 * p_top3_v,
p_top3,
],
[
p_top3,
p_top3 * p_top3_v.inverse,
p_top2_c2,
p_top2,
],
[
p_top2,
p_top2_c1,
p_top1 * p_top1_v,
p_top1,
],
[
p_top1,
p_top1 * p_top1_v.inverse,
p_mid * p_mid_v,
p_mid,
],
]
sketch = (
Cq.Sketch()
.arc(
p_base.to2d_pos(),
(0, 0),
p_base.flip_x().to2d_pos(),
)
)
for bezier_group in bezier_groups:
sketch = (
sketch
.bezier([p.to2d_pos() for p in bezier_group])
.bezier([p.flip_x().to2d_pos() for p in bezier_group])
)
return sketch.assemble()
def side_guard_dovetail(self) -> Cq.Solid:
"""
Generates a dovetail coupling for the side guard
"""
dx = self.side_guard_thickness / 2
wire = Cq.Wire.makePolygon([
(dx * 0.5, 0),
(dx * 0.7, dx),
(-dx * 0.7, dx),
(-dx * 0.5, 0),
], close=True)
return Cq.Solid.extrudeLinear(
wire,
[],
(0,0,dx + self.side_guard_dovetail_height),
).moved((0, 0, -dx))
def side_guard_frontal_slot(self) -> Cq.Workplane:
angle = 360 / self.facets
inner_d = self.thickness / 2 - self.slot_thickness / 2
outer_d = self.thickness / 2 + self.slot_thickness / 2
outer = Cq.Solid.makeCone(
radius1=self.radius_lower_front + outer_d,
radius2=self.radius_upper_front + outer_d,
height=self.height,
angleDegrees=angle,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower_front + inner_d,
radius2=self.radius_upper_front + inner_d,
height=self.height,
angleDegrees=angle,
)
shell = (
outer.cut(inner)
.rotate((0,0,0), (0,0,1), -angle/2)
)
# Generate the sector intersector
intersector = Cq.Solid.makeCylinder(
radius=self.radius_upper + self.side_guard_thickness,
height=self.front_wing_height,
angleDegrees=self.front_wing_angle,
).moved(Cq.Location(0,0,self.front_wing_dh,0,0,-self.front_wing_angle/2))
return shell * intersector
def side_guard(
self,
attach_left: AttachPoint,
attach_right: AttachPoint,
) -> Cq.Workplane:
"""
Constructs the side guard using a cone. Via Gauss's Theorema Egregium,
the surface of the cone can be deformed into a plane.
"""
angle_span = 360 / self.facets
outer = Cq.Solid.makeCone(
radius1=self.radius_lower + self.side_guard_thickness,
radius2=self.radius_upper + self.side_guard_thickness,
height=self.height,
angleDegrees=angle_span,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower,
radius2=self.radius_upper,
height=self.height,
angleDegrees=angle_span,
)
shell = (outer - inner).rotate((0,0,0), (0,0,1), -angle_span/2)
dx = math.sin(math.radians(angle_span / 2)) * (self.radius_middle + self.side_guard_thickness)
profile = (
Cq.Workplane('YZ')
.polyline([
(0, self.height),
(-dx, self.height / 2),
(-dx, 0),
(dx, 0),
(dx, self.height / 2),
])
.close()
.extrude(self.radius_upper + self.side_guard_thickness)
.val()
)
#channel = (
# Cq.Solid.makeCylinder(
# radius=self.side_guard_channel_radius + 1.0,
# height=self.side_guard_channel_height,
# ) - Cq.Solid.makeCylinder(
# radius=self.side_guard_channel_radius,
# height=self.side_guard_channel_height,
# )
#)
result = shell * profile# - channel
# Create the downward slots
for sign in [-1, 1]:
slot_box = Cq.Solid.makeBox(
length=self.height,
width=self.slot_width,
height=self.slot_thickness,
).moved(
Cq.Location(-self.slot_thickness,-self.slot_width/2, -self.slot_thickness/2)
)
# keyhole for threads to stay in place
slot_cyl = Cq.Solid.makeCylinder(
radius=self.slot_thickness/2,
height=self.height,
pnt=(0,0,self.slot_thickness/2),
dir=(1,0,0),
)
slot = slot_box + slot_cyl
slot = slot.moved(
Cq.Location.rot2d(sign * self.side_guard_slot_angle) *
Cq.Location(self.radius_lower + self.side_guard_thickness/2, 0, 0) *
Cq.Location(0,0,0,0,-180 + self.slot_tilt,0)
)
result = result - slot
radius_attach = self.radius_lower + self.side_guard_thickness / 2
# tilt the dovetail by radius differential
angle_tilt = math.degrees(math.atan2(self.radius_middle - self.radius_lower, self.height / 2))
dovetail = self.side_guard_dovetail()
loc_dovetail_left = Cq.Location.rot2d(angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0)
loc_dovetail_right = Cq.Location.rot2d(-angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0)
angle_slot = 180 / self.facets - self.front_wing_angle / 2
match attach_left:
case AttachPoint.DOVETAIL_IN:
loc_dovetail_left *= Cq.Location.rot2d(180)
result = result - dovetail.moved(loc_dovetail_left)
case AttachPoint.DOVETAIL_OUT:
result = result + dovetail.moved(loc_dovetail_left)
case AttachPoint.SLOT:
result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(angle_slot))
case AttachPoint.NONE:
pass
match attach_right:
case AttachPoint.DOVETAIL_IN:
result = result - dovetail.moved(loc_dovetail_right)
case AttachPoint.DOVETAIL_OUT:
loc_dovetail_right *= Cq.Location.rot2d(180)
result = result + dovetail.moved(loc_dovetail_right)
case AttachPoint.SLOT:
result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(-angle_slot))
case AttachPoint.NONE:
pass
# Remove parts below the horizontal
cut_h = self.radius_lower
result -= Cq.Solid.makeCylinder(
radius=self.radius_lower + self.side_guard_thickness,
height=cut_h).moved((0,0,-cut_h))
return result
@target(name="side_guard_1", angularTolerance=0.01)
def side_guard_1(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.SLOT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_2", angularTolerance=0.01)
def side_guard_2(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_3", angularTolerance=0.01)
def side_guard_3(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_4", angularTolerance=0.01)
def side_guard_4(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.SLOT,
)
def front_surrogate(self) -> Cq.Workplane:
"""
Create a surrogate cylindrical section structure for the front since we
cannot bend extrusions
"""
angle = 360 / 5
outer = Cq.Solid.makeCone(
radius1=self.radius_lower_front + self.thickness,
radius2=self.radius_upper_front + self.thickness,
height=self.height,
angleDegrees=angle,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower_front,
radius2=self.radius_upper_front,
height=self.height,
angleDegrees=angle,
)
shell = (
outer.cut(inner)
.rotate((0,0,0), (0,0,1), -angle/2)
)
dx = math.sin(math.radians(angle / 2)) * self.radius_middle_front
profile = (
Cq.Workplane('YZ')
.polyline([
(0, self.height),
(-dx, self.height / 2),
(-dx, 0),
(dx, 0),
(dx, self.height / 2),
])
.close()
.extrude(self.radius_upper_front + self.side_guard_thickness)
.val()
)
return shell * profile
def assembly(self) -> Cq.Assembly:
"""
New assembly using conformal mapping on the cone.
"""
side_guards = [
self.side_guard_1(),
self.side_guard_2(),
self.side_guard_3(),
self.side_guard_4(),
]
a = Cq.Assembly()
for i,side_guard in enumerate(side_guards):
angle = -(i+1) * 360 / self.facets
a = a.addS(
side_guard,
name=f"side-{i}",
material=self.material_side,
loc=Cq.Location(rz=angle)
)
a.addS(
self.front_surrogate(),
name="front",
material=self.material,
)
return a

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
import math
from dataclasses import dataclass, field
from pathlib import Path
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
import nhf.utils
@dataclass
class Epaulette(Model):
side: str
diam: float = 100.0
thickness_brass: float = 0.4 # 26 Gauge
thickness_fabric: float = 0.3
material: Material = Material.METAL_BRASS
def __post_init__(self):
super().__init__(name=f"epaulette-{self.side}")
def surface(self) -> Cq.Solid:
path = Path(__file__).resolve().parent / f"epaulette-{self.side}.dxf"
return (
Cq.importers.importDXF(path).wires().toPending().extrude(self.thickness_brass)
)
def assembly(self) -> Cq.Assembly:
assembly = (
Cq.Assembly()
.addS(
self.surface(),
name="surface",
material=self.material,
role=Role.DECORATION,
)
)
return assembly

View File

@ -0,0 +1,587 @@
import math
from dataclasses import dataclass, field
from typing import Tuple
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind
import nhf.utils
@dataclass
class Rod(Model):
width: float = 120.0
length: float = 550.0
length_tip: float = 100.0
width_tail: float = 60.0
margin: float = 10.0
thickness_top: float = 25.4 / 8
# The side which has mounted hinges must be thicker
thickness_side: float = 25.4 / 4
height_internal: float = 30.0
material_shell: Material = Material.WOOD_BIRCH
# Considering the glyph on the top ...
# counted from middle to the bottom
fac_bar_top: float = 0.1
# counted from bottom to top
fac_window_tsumi_bot: float = 0.63
fac_window_tsumi_top: float = 0.88
fac_window_footer_bot: float = 0.36
fac_window_footer_top: float = 0.6
# Considering the side ...
hinge_plate_pos: list[float] = field(default_factory=lambda: [0.1, 0.9])
hinge_plate_length: float = 30.0
hinge_hole_diam: float = 2.5
# Hole distance to axis
hinge_hole_axis_dist: float = 12.5 / 2
# Distance between holes
hinge_hole_sep: float = 15.89
# Consider the reference objects
ref_object_width: float = 50.0
ref_object_length: float = 50.0
def __post_init__(self):
super().__init__(name="rod")
self.loc_core = Cq.Location.from2d(self.length - self.length_tip, 0)
assert self.length_tip * 2 < self.length
#assert self.fac_bar_top + self.fac_window_tsumi_top < 1
assert self.fac_window_tsumi_bot < self.fac_window_tsumi_top
@property
def length_tail(self):
return self.length - self.length_tip
@property
def _reduced_tip_x(self):
return self.length_tip - self.margin
@property
def _reduced_y(self):
return self.width / 2 - self.margin
@property
def _reduced_tail_y(self):
return self.width_tail / 2 - self.margin
def profile_points(self) -> list[Tuple[str, Tuple[float, float]]]:
"""
Points in polygon line order, labaled
"""
return [
("tip", (self.length, 0)),
("mid_r", (self.length - self.length_tip, self.width/2)),
("bot_r", (0, self.width_tail / 2)),
("bot_l", (0, -self.width_tail / 2)),
("mid_l", (self.length - self.length_tip, -self.width/2)),
]
def _window_tip(self) -> Cq.Sketch:
dxh = self._reduced_tip_x
dy = self._reduced_y
return (
Cq.Sketch()
.segment(
(dxh, 0),
(dxh / 2, dy / 2),
)
.bezier([
(dxh / 2, dy / 2),
(dxh * 0.6, dy * 0.4),
(dxh * 0.6, -dy * 0.4),
(dxh / 2, -dy / 2),
])
.segment(
(dxh, 0),
)
.assemble()
.moved(self.loc_core.to2d_pos())
)
def _window_eye(self, refl: bool = False) -> Cq.Sketch:
sign = -1 if refl else 1
dxh = self._reduced_tip_x
xm = dxh * 0.45
dy = sign * self._reduced_y
fac = 0.05
p1 = Cq.Location.from2d(xm, sign * self.margin / 2)
p2 = Cq.Location.from2d(dxh * 0.1, sign * self.margin / 2)
p3 = Cq.Location.from2d(dxh * 0.15, dy * 0.55)
p4 = Cq.Location.from2d(dxh * 0.4, dy * 0.45)
d4 = Cq.Location.from2d(dxh * fac, -dy * fac)
return (
Cq.Sketch()
.segment(
p1.to2d_pos(),
p2.to2d_pos(),
)
.bezier([
p2.to2d_pos(),
(p2 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
(p3 * Cq.Location.from2d(-dxh * fac, -dy * fac)).to2d_pos(),
p3.to2d_pos(),
])
.bezier([
p3.to2d_pos(),
(p3 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
(p4 * d4.inverse).to2d_pos(),
p4.to2d_pos(),
])
.bezier([
p4.to2d_pos(),
(p4 * d4).to2d_pos(),
(p1 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
p1.to2d_pos(),
])
.assemble()
.moved(self.loc_core.to2d_pos())
)
def _window_bar(self) -> Cq.Sketch():
dxh = self._reduced_tip_x
dy = self._reduced_y
dyt = self._reduced_tail_y
dxt = self.length_tail
ext_fac = self.fac_bar_top
p_corner = Cq.Location.from2d(0, dy)
p_top = Cq.Location.from2d(0.3 * dxh, 0.7 * dy)
p_bot = Cq.Location.from2d(-ext_fac * dxt, dy + ext_fac * (dyt - dy))
p_top_int = p_corner * Cq.Location.from2d(.05 * dxh, -.2 * dy)
p_top_ctrl = Cq.Location.from2d(0, .3 * dy)
p_bot_int = p_corner * Cq.Location.from2d(-.15 * dxh, -.2 * dy)
p_bot_ctrl = Cq.Location.from2d(-.25 * dxh, .3 * dy)
return (
Cq.Sketch()
.segment(
p_corner.to2d_pos(),
p_top.to2d_pos(),
)
.segment(p_top_int.to2d_pos())
.bezier([
p_top_int.to2d_pos(),
p_top_ctrl.to2d_pos(),
p_top_ctrl.flip_y().to2d_pos(),
p_top_int.flip_y().to2d_pos(),
])
.segment(p_top.flip_y().to2d_pos())
.segment(p_corner.flip_y().to2d_pos())
.segment(p_bot.flip_y().to2d_pos())
.segment(p_bot_int.flip_y().to2d_pos())
.bezier([
p_bot_int.flip_y().to2d_pos(),
p_bot_ctrl.flip_y().to2d_pos(),
p_bot_ctrl.to2d_pos(),
p_bot_int.to2d_pos(),
])
.segment(p_bot.to2d_pos())
.segment(p_corner.to2d_pos())
.assemble()
.moved(self.loc_core.to2d_pos())
)
def _window_tsumi(self) -> Cq.Sketch:
dx = (self.fac_window_tsumi_top - self.fac_window_tsumi_bot) * self.length_tail
dy = 2 * self._reduced_y * 0.8
loc = Cq.Location(self.fac_window_tsumi_bot * self.length_tail, 0)
# Construction of the top part of the kanji
dx_top = dx * 0.3
x_top = dx - dx_top / 2
dy_top = dy
dy_eye = dy * 0.2
dy_border = (dy_top - 3 * dy_eye) / 4
# The skip must follow 3 * eye + 4 * border = dy_top
y_skip = dy_eye + dy_border
# Construction of the bottom part
x_bot = dx * 0.65
y3 = dy * 0.4
y2 = dy * 0.2
y1 = dy * 0.1
# x/y-centers of the legs
x_leg0 = x_bot / 14
dx_leg = x_bot / 7
y_leg = (y3 + y1) / 2
return (
Cq.Sketch()
.push([(x_top, 0)])
.rect(dx_top, dy_top)
.push([
(x_top, -y_skip),
(x_top, 0),
(x_top, y_skip),
])
.rect(dx_top / 3, dy_eye, mode='s')
# Construct the two sides
.push([
(x_bot / 2, (y2 + y1) / 2),
(x_bot / 2, -(y2 + y1) / 2),
])
.rect(x_bot, y2 - y1, mode='a')
.push([
(x_leg0 + dx_leg, y_leg),
(x_leg0 + 3 * dx_leg, y_leg),
(x_leg0 + 5 * dx_leg, y_leg),
(x_leg0 + dx_leg, -y_leg),
(x_leg0 + 3 * dx_leg, -y_leg),
(x_leg0 + 5 * dx_leg, -y_leg),
])
.rect(dx_leg, y3 - y1, mode='a')
.moved(loc)
)
def _window_footer(self) -> Cq.Sketch:
x_bot = self.fac_window_footer_bot * self.length_tail
dx = (self.fac_window_footer_top - self.fac_window_footer_bot) * self.length_tail
loc = Cq.Location(x_bot, 0)
dy = self._reduced_y * 0.8
# eyes
eye_y2 = dy * .5
eye_y1 = dy * .2
eye_width = eye_y2 - eye_y1
eye_x = dx - eye_width / 2
# bar polygon
bar_x0 = dx * 0.65
bar_dx = dx * 0.1
bar_x1 = bar_x0 + bar_dx
bar_x2 = bar_x0 + bar_dx * 2
bar_x3 = bar_x0 + bar_dx * 3
bar_y1 = dy * .75
assert bar_y1 > eye_y2
bar_y2 = dy * .9
assert bar_y1 < bar_y2
# Construction of the cross
cross_dx = dx * 0.7 / math.sqrt(2)
cross_dy = dy * 0.2
cross = (
Cq.Sketch()
.rect(cross_dx, cross_dy)
.rect(cross_dy, cross_dx, mode='a')
.moved(Cq.Location.from2d(dx * 0.5, 0, 45))
)
return (
Cq.Sketch()
# eyes
.push([
(eye_x, (eye_y1 + eye_y2)/2),
(eye_x, -(eye_y1 + eye_y2)/2),
])
.rect(eye_width, eye_width, mode='a')
# middle bar
.push([(0,0)])
.polygon([
(bar_x1, bar_y1),
(bar_x0, bar_y1),
(bar_x0, bar_y2),
(bar_x3, bar_y2),
(bar_x3, bar_y1),
(bar_x2, bar_y1),
(bar_x2, -bar_y1),
(bar_x3, -bar_y1),
(bar_x3, -bar_y2),
(bar_x0, -bar_y2),
(bar_x0, -bar_y1),
(bar_x1, -bar_y1),
], mode='a')
# cross
.boolean(cross, mode='a')
#.push([(0,0)])
#.rect(10, 10)
.moved(loc)
)
@target(name="bottom", kind=TargetKind.DXF)
def profile_bottom(self) -> Cq.Sketch:
return (
Cq.Sketch()
.polygon([p for _, p in self.profile_points()])
)
@target(name="top", kind=TargetKind.DXF)
def profile_top(self) -> Cq.Sketch:
return (
self.profile_bottom()
.boolean(self._window_tip(), mode='s')
.boolean(self._window_eye(True), mode='s')
.boolean(self._window_eye(False), mode='s')
.boolean(self._window_bar(), mode='s')
.boolean(self._window_tsumi(), mode='s')
.boolean(self._window_footer(), mode='s')
)
def surface_top(self) -> Cq.Workplane:
return (
Cq.Workplane('XY')
.placeSketch(self.profile_top())
.extrude(self.thickness_top)
)
def surface_bottom(self) -> Cq.Workplane:
surface = (
Cq.Workplane('XY')
.placeSketch(self.profile_bottom())
.extrude(self.thickness_top)
)
plane = surface.faces(">Z").workplane()
for (name, p) in self.profile_points():
plane.moveTo(*p).tagPlane(name)
return surface
# Properties of the side surfaces
@property
def length_edge_tip(self):
return math.sqrt(self.length_tip ** 2 + (self.width / 2) ** 2)
@property
def length_edge_tail(self):
dw = (self.width - self.width_tail) / 2
return math.sqrt(self.length_tail ** 2 + dw ** 2)
@property
def tip_incident_angle(self):
"""
Angle (measuring from vertical) at which the tip edge pieces must be
sanded in order to make them not collide into each other.
"""
return math.atan2(self.length_tip, self.width / 2)
@property
def shoulder_incident_angle(self) -> float:
angle_tip = math.atan2(self.width / 2, self.length_tip)
angle_tail = math.atan2((self.width - self.width_tail) / 2, self.length_tail)
return (angle_tip + angle_tail) / 2
@target(name="ref-tip")
def ref_tip(self) -> Cq.Workplane:
angle = self.tip_incident_angle
w = self.ref_object_width
drop = math.sin(angle) * w
profile = (
Cq.Sketch()
.polygon([
(0, 0),
(0, w),
(w, w),
(w - drop, 0),
])
)
return (
Cq.Workplane()
.placeSketch(profile)
.extrude(self.ref_object_length)
)
@target(name="ref-shoulder")
def ref_shoulder(self) -> Cq.Workplane:
angle = self.shoulder_incident_angle
w = self.ref_object_width
drop = math.sin(angle) * w
profile = (
Cq.Sketch()
.polygon([
(0, 0),
(0, w),
(w, w),
(w - drop, 0),
])
)
return (
Cq.Workplane()
.placeSketch(profile)
.extrude(self.ref_object_length)
)
@target(name="side-tip-2x", kind=TargetKind.DXF)
def profile_side_tip(self):
l = self.length_edge_tip
w = self.height_internal
return (
Cq.Sketch()
.push([(l/2, w/2)])
.rect(l, w)
)
@target(name="side-tail", kind=TargetKind.DXF)
def profile_side_tail(self):
"""
Plain side 2 with no hinge
"""
l = self.length_edge_tail
w = self.height_internal
return (
Cq.Sketch()
.push([(l/2, w/2)])
.rect(l, w)
)
@target(name="side-hinge-plate", kind=TargetKind.DXF)
def profile_side_hinge_plate(self):
l = self.hinge_plate_length
w = self.height_internal / 2
return (
Cq.Sketch()
.push([(0, w/2)])
.rect(l, w)
.push([
(self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
(-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
])
.circle(self.hinge_hole_diam / 2, mode='s')
)
@target(name="side-tail-hinged", kind=TargetKind.DXF)
def profile_side_tail_hinged(self):
"""
Plain side 2 with no hinge
"""
l = self.length_edge_tail
w = self.height_internal
# Holes for hinge
plate_pos = [
(t * l, w * 3/4) for t in self.hinge_plate_pos
]
hole_pos = [
(self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
(-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
]
return (
self.profile_side_tail()
.push(plate_pos)
.rect(self.hinge_plate_length, w/2, mode='s')
.push([
(hx + px, w/2 - hy)
for hx, hy in hole_pos
for px, _ in plate_pos
])
.circle(self.hinge_hole_diam / 2, mode='s')
)
@target(name="side-bot", kind=TargetKind.DXF)
def profile_side_bot(self):
l = self.width_tail - self.thickness_side * 2
w = self.height_internal
return (
Cq.Sketch()
.rect(l, w)
)
def surface_side_tip(self):
result = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_tip())
.extrude(self.thickness_side)
)
plane = result.faces(">Y").workplane()
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(-self.length_edge_tip, 0).tagPlane("top")
return result
def surface_side_tail(self):
result = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_tail())
.extrude(self.thickness_side)
)
plane = result.faces(">Y").workplane()
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(-self.length_edge_tail, 0).tagPlane("top")
return result
def surface_side_tail_hinged(self):
result = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_tail_hinged())
.extrude(self.thickness_side)
)
plane = result.faces(">Y").workplane()
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(-self.length_edge_tail, 0).tagPlane("top")
return result
def surface_side_bot(self):
result = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_bot())
.extrude(self.thickness_side)
)
plane = result.faces(">Y").workplane()
plane.moveTo(self.width_tail / 2, 0).tagPlane("bot")
plane.moveTo(-self.width_tail / 2, 0).tagPlane("top")
return result
@assembly()
def assembly(self) -> Cq.Assembly:
a = (
Cq.Assembly()
.addS(
self.surface_top(),
name="top",
material=self.material_shell,
role=Role.STRUCTURE | Role.DECORATION
)
.constrain("top", "Fixed")
.addS(
self.surface_bottom(),
name="bottom",
material=self.material_shell,
role=Role.STRUCTURE,
loc=Cq.Location(0, 0, -self.thickness_top - self.height_internal)
)
.constrain("bottom", "Fixed")
.addS(
self.surface_side_tip(),
name="side_tip_l",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?tip", "side_tip_l?top", "Plane")
.constrain("bottom?mid_l", "side_tip_l?bot", "Plane")
.addS(
self.surface_side_tip(),
name="side_tip_r",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?tip", "side_tip_r?bot", "Plane")
.constrain("bottom?mid_r", "side_tip_r?top", "Plane")
.addS(
self.surface_side_tail(),
name="side_tail_l",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?mid_l", "side_tail_l?top", "Plane")
.constrain("bottom?bot_l", "side_tail_l?bot", "Plane")
.addS(
self.surface_side_tail_hinged(),
name="side_tail_r",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?mid_r", "side_tail_r?bot", "Plane")
.constrain("bottom?bot_r", "side_tail_r?top", "Plane")
.addS(
self.surface_side_bot(),
name="side_bot",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?bot_l", "side_bot?top", "Plane")
.constrain("bottom?bot_r", "side_bot?bot", "Plane")
.solve()
)
return a

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="zehi.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.8706489"
inkscape:cx="171.86549"
inkscape:cy="207.68194"
inkscape:window-width="1640"
inkscape:window-height="962"
inkscape:window-x="20"
inkscape:window-y="40"
inkscape:window-maximized="0"
inkscape:current-layer="layer3"
showguides="true">
<sodipodi:guide
position="50,90.756846"
orientation="-1,0"
id="guide1"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="32.08421,55"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="60,78.19709"
orientation="-1,0"
id="guide3"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="40,74.842796"
orientation="-1,0"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="4.9999993,79.999999"
orientation="0,1"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="-9.7833569,45"
orientation="0,1"
id="guide6"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Outline"
inkscape:groupmode="layer"
id="layer1">
<path
id="path1"
style="fill:none;stroke:#000000;stroke-width:0.264453;stroke-opacity:1;-inkscape-stroke:none"
inkscape:label="outer"
d="M 50.000049 0.13229167 A 49.867775 49.867775 0 0 0 0.13229167 50.000049 A 49.867775 49.867775 0 0 0 50.000049 99.867806 A 49.867775 49.867775 0 0 0 99.867806 50.000049 A 49.867775 49.867775 0 0 0 50.000049 0.13229167 z M 50.000049 5.1190674 A 44.880997 44.880997 0 0 1 94.88103 50.000049 A 44.880997 44.880997 0 0 1 50.000049 94.88103 A 44.880997 44.880997 0 0 1 5.1190674 50.000049 A 44.880997 44.880997 0 0 1 50.000049 5.1190674 z " />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Cut"
style="display:none">
<circle
style="fill:none;stroke:#000000;stroke-width:0.264453;stroke-opacity:1;-inkscape-stroke:none"
id="circle14"
cx="50"
cy="50"
inkscape:label="outer"
r="49.867775" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Ze"
style="display:inline">
<path
id="path10"
style="fill:none;stroke:#144e16;stroke-width:0.290227;stroke-opacity:1;-inkscape-stroke:none"
d="M 50.000049,12.645223 A 37.354885,37.354885 0 0 0 13.002824,44.999837 h 2.546614 2.469617 63.965088 1.438155 3.574976 A 37.354885,37.354885 0 0 0 50.000049,12.645223 Z m 0,4.980575 a 32.374233,32.374233 0 0 1 22.160404,8.817549 H 27.842745 A 32.374233,32.374233 0 0 1 50.000049,17.625798 Z M 23.725167,31.201713 h 52.552864 a 32.374233,32.374233 0 0 1 4.547526,9.039758 H 19.177641 a 32.374233,32.374233 0 0 1 4.547526,-9.039758 z"
inkscape:label="top" />
<path
style="fill:none;stroke:#144e16;stroke-width:0.261252;stroke-opacity:1;-inkscape-stroke:none"
d="m 30.468424,65.042542 -8.764322,8.764322 a 37.141727,37.141731 0 0 0 28.295947,13.12168 37.141727,37.141731 0 0 0 23.236308,-8.258411 L 70.147139,75.252771 C 64.43218,79.841996 57.3295,82.357002 50.000049,82.386702 42.031806,82.356509 34.403936,79.387593 28.529008,74.129842 l 5.513359,-5.513358 z"
id="path14" />
<path
style="fill:none;stroke:#144e16;stroke-width:0.261252;stroke-opacity:1;-inkscape-stroke:none"
d="m 53.049475,54.972872 v 0.02687 H 13.231234 a 37.141727,37.141731 0 0 0 1.076937,5.000211 H 53.049475 V 75.366976 H 58.10343 V 67.69716 H 73.684908 V 62.643205 H 58.10343 v -2.64325 h 27.588497 a 37.141727,37.141731 0 0 0 1.076937,-5.000211 H 58.10343 v -0.02687 z"
id="path11" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Hi"
style="display:inline">
<path
id="rect6"
style="fill:none;stroke:#053efb;stroke-width:0.264583;stroke-opacity:1;-inkscape-stroke:none"
d="m 54.786837,19.999813 v 59.999955 h 5.213118 v -8.94364 H 79.70883 V 62.775496 H 59.999955 V 54.140365 H 79.70883 V 45.859733 H 59.999955 V 37.224601 H 79.70883 V 28.94397 H 59.999955 v -8.944157 z"
inkscape:label="right" />
<path
id="path9"
style="fill:none;stroke:#053efb;stroke-width:0.264583;stroke-opacity:1;-inkscape-stroke:none"
d="M 45.245359,79.999977 V 20.000022 h -5.213118 v 8.94364 H 20.323366 v 8.280632 h 19.708875 v 8.635131 H 20.323366 v 8.280632 h 19.708875 v 8.635132 H 20.323366 v 8.280631 h 19.708875 v 8.944157 z"
inkscape:label="left" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,3 @@
# Yasaka Kanako
This cosplay won a Judge's favourite award at TouhouFest 2025.

View File

@ -0,0 +1,37 @@
import nhf.touhou.yasaka_kanako.mirror as MM
import nhf.touhou.yasaka_kanako.onbashira as MO
import nhf.touhou.yasaka_kanako.shimenawa as MS
from nhf.build import Model, TargetKind, target, assembly, submodel
import nhf.utils
from dataclasses import dataclass, field
import cadquery as Cq
@dataclass
class Parameters(Model):
mirror: MM.Mirror = field(default_factory=lambda: MM.Mirror())
onbashira: MO.Onbashira = field(default_factory=lambda: MO.Onbashira())
shimenawa: MS.Shimenawa = field(default_factory=lambda: MS.Shimenawa())
def __post_init__(self):
super().__init__(name="yasaka-kanako")
@submodel(name="mirror")
def submodel_mirror(self) -> Model:
return self.mirror
@submodel(name="onbashira")
def submodel_onbashira(self) -> Model:
return self.onbashira
@submodel(name="shimenawa")
def submodel_shimenawa(self) -> Model:
return self.shimenawa
if __name__ == '__main__':
import sys
p = Parameters()
if len(sys.argv) == 1:
p.build_all()
sys.exit(0)

View File

@ -0,0 +1,211 @@
#define USE_MOTOR 1
#define USE_LED 1
#define USE_DISPLAY 0
// The mode switch button should be wired to the ground with an internal pullup resistor.
#define pinButtonMode 9
#define pinDiag 6
// Main LED strip setup
#define pinLED 3
#define NUM_LEDS 20
#define LED_PART 10
#define BRIGHTNESS 250
#define LED_TYPE WS2811
// Relay controlled motor
#define pinMotor 7
#if USE_LED
#include <FastLED.h>
int cycles = 100;
int cycle_duration = 100;
CRGB leds[NUM_LEDS];
CRGB color_red;
CRGB color_blue;
CRGB color_green;
#endif
#if USE_DISPLAY
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#endif
// Program state
bool stateButtonMode = false;
int programId = 0;
bool programChanged = true;
#define MAX_PROGRAMS 3
bool flag_motor = false;
bool flag_lighting = false;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(pinButtonMode, INPUT_PULLUP);
#if USE_LED
// Calculate colours
hsv2rgb_spectrum(CHSV(4, 255, 100), color_red);
hsv2rgb_spectrum(CHSV(170, 255, 100), color_blue);
hsv2rgb_spectrum(CHSV(90, 255, 100), color_green);
pinMode(pinLED, OUTPUT);
#endif
pinMode(pinDiag, OUTPUT);
#if USE_MOTOR
pinMode(pinMotor, OUTPUT);
#endif
#if USE_LED
// Main LED strip
FastLED.addLeds<LED_TYPE, pinLED, RGB>(leds, NUM_LEDS);
fill_solid(leds, NUM_LEDS, CRGB::White);
delay(500);
FastLED.show();
fill_solid(leds, NUM_LEDS, CRGB::Black);
delay(500);
#endif
#if USE_DISPLAY
Serial.begin(9600);
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
pinMode(pinLED, OUTPUT);
digitalWrite(pinLED, HIGH);
}
#endif
digitalWrite(LED_BUILTIN, HIGH);
}
void loop() {
// Detect a rising edge
int buttonState = !digitalRead(pinButtonMode);
if (buttonState && !stateButtonMode) {
programId = (programId + 1) % MAX_PROGRAMS;
programChanged = true;
stateButtonMode = true;
}
if (!buttonState) {
stateButtonMode = false;
}
switch (programId)
{
case 0:
program_off();
break;
case 1:
program_still();
break;
case 2:
program_rotate();
break;
default:
break;
}
if (programChanged) {
update_screen();
digitalWrite(LED_BUILTIN, LOW);
}
programChanged = false;
}
// Utility for updating LEDs
void fill_segmented(CRGB c1, CRGB c2)
{
#if USE_LED
//fill_solid(leds, LED_PART, c1);
fill_gradient_RGB(leds, LED_PART, CRGB::Black ,c1);
fill_gradient_RGB(leds + LED_PART, NUM_LEDS - LED_PART, CRGB::Black, c2);
FastLED.show();
#endif
}
void set_motor(bool flag)
{
#if USE_MOTOR
if (flag) {
digitalWrite(pinMotor, HIGH);
flag_motor = true;
}
else {
digitalWrite(pinMotor, LOW);
flag_motor = false;
}
#endif
}
// Update current display status
void update_screen()
{
#if USE_DISPLAY
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Yasaka K.");
display.print("P");
display.print(programId);
display.print(" ");
if (flag_motor) {
display.print("M");
}
if (flag_lighting) {
display.print("L");
}
display.display();
#endif
}
void program_off()
{
if (programChanged)
{
set_motor(false);
#if USE_LED
flag_lighting = false;
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
#endif
}
delay(cycle_duration);
}
void program_still()
{
if (programChanged)
{
set_motor(false);
#if USE_LED
flag_lighting = true;
fill_segmented(CRGB::Green, CRGB::Orange);
FastLED.show();
#endif
}
delay(cycle_duration);
}
void program_rotate()
{
if (programChanged)
{
set_motor(true);
}
#if USE_LED
flag_lighting = true;
fill_segmented(CRGB::Green, CRGB::Orange);
delay(cycle_duration/2);
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
delay(cycle_duration/2);
#endif
}

View File

@ -0,0 +1,186 @@
from dataclasses import dataclass, field
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.materials import Role, Material
import nhf.touhou.yasaka_kanako.onbashira as MO
import nhf.utils
@dataclass
class Mirror(Model):
"""
Kanako's mirror, made of three levels.
The mirror suface is sandwiched between two layers of wood. As such, its
dimensions have to sit in between that of the aperature on the surface, and
the outer walls. The width/height here refers to the outer edge's width and height
"""
width: float = 100.0
height: float = 120.0
inner_gap: float = 3.0
outer_gap: float = 3.0
core_thickness: float = 25.4 / 8
casing_thickness: float = 25.4 / 8
flange_r0: float = 8.0
flange_r1: float = 20.0
flange_y1: float = 12.0
flange_y2: float = 25.0
flange_hole_r: float = 8.0
wing_x1: float = 15.0
wing_x2: float = 24.0
wing_r1: float = 10.0
wing_r2: float = 16.0
tail_r0: float = 8.0
tail_r1: float = 13.0
tail_y1: float = 16.0
tail_y2: float = 29.0
# Necklace hole
hole_diam: float = 5.0
material_mirror: Material = Material.ACRYLIC_TRANSPARENT
material_casing: Material = Material.WOOD_BIRCH
@target(name="core", kind=TargetKind.DXF)
def profile_core(self) -> Cq.Sketch:
rx = self.width/2 - self.outer_gap
ry = self.height/2 - self.outer_gap
return Cq.Sketch().ellipse(rx, ry)
def core(self) -> Cq.Workplane:
return (
Cq.Workplane()
.placeSketch(self.profile_core())
.extrude(self.core_thickness)
)
@target(name="casing-bot", kind=TargetKind.DXF)
def profile_casing_bot(self) -> Cq.Sketch:
"""
Base of the casing with no holes carved out
"""
yt = self.height / 2 - self.outer_gap
yh = (self.flange_y1 + self.flange_y2) / 2
flange = (
Cq.Sketch()
.polygon([
(self.flange_r0, yt),
(self.flange_r0, yt + self.flange_y1),
(self.flange_r1, yt + self.flange_y1),
(self.flange_r1, yt + self.flange_y2),
(-self.flange_r1, yt + self.flange_y2),
(-self.flange_r1, yt + self.flange_y1),
(-self.flange_r0, yt + self.flange_y1),
(-self.flange_r0, yt),
])
.push([
(self.flange_hole_r, yt+yh),
(-self.flange_hole_r, yt+yh),
])
.circle(self.hole_diam/2, mode="s")
)
tail = (
Cq.Sketch()
.polygon([
(+self.tail_r0, -yt),
(+self.tail_r0, -yt - self.tail_y1),
(+self.tail_r1, -yt - self.tail_y1),
(+self.tail_r1, -yt - self.tail_y2),
(-self.tail_r1, -yt - self.tail_y2),
(-self.tail_r1, -yt - self.tail_y1),
(-self.tail_r0, -yt - self.tail_y1),
(-self.tail_r0, -yt),
])
)
return (
Cq.Sketch()
.ellipse(self.width/2, self.height/2)
.boolean(flange, mode="a")
.boolean(tail, mode="a")
.boolean(self.profile_wing(-1), mode="a")
.boolean(self.profile_wing(1), mode="a")
)
def casing_bot(self) -> Cq.Workplane:
return (
Cq.Workplane()
.placeSketch(self.profile_casing_bot())
.extrude(self.casing_thickness)
)
def profile_wing(self, sign: float=1) -> Cq.Sketch:
xt = self.width / 2 - self.outer_gap
return (
Cq.Sketch()
.polygon([
(sign*xt, self.wing_r1),
(sign*(xt+self.wing_x1), self.wing_r1),
(sign*(xt+self.wing_x1), self.wing_r2),
(sign*(xt+self.wing_x2), self.wing_r2),
(sign*(xt+self.wing_x2), -self.wing_r2),
(sign*(xt+self.wing_x1), -self.wing_r2),
(sign*(xt+self.wing_x1), -self.wing_r1),
(sign*xt, -self.wing_r1),
])
)
@target(name="casing-mid", kind=TargetKind.DXF)
def profile_casing_mid(self) -> Cq.Sketch:
rx = self.width/2 - self.outer_gap
ry = self.height/2 - self.outer_gap
return (
self.profile_casing_bot()
.ellipse(rx, ry, mode="s")
)
def casing_mid(self) -> Cq.Workplane:
return (
Cq.Workplane()
.placeSketch(self.profile_casing_mid())
.extrude(self.core_thickness)
)
@target(name="casing-top", kind=TargetKind.DXF)
def profile_casing_top(self) -> Cq.Sketch:
rx = self.width/2 - self.outer_gap - self.inner_gap
ry = self.height/2 - self.outer_gap - self.inner_gap
return (
self.profile_casing_bot()
.ellipse(rx, ry, mode="s")
)
def casing_top(self) -> Cq.Workplane:
return (
Cq.Workplane()
.placeSketch(self.profile_casing_top())
.extrude(self.casing_thickness)
)
@assembly()
def assembly(self) -> Cq.Assembly:
return (
Cq.Assembly()
.addS(
self.core(),
name="core",
material=self.material_mirror,
role=Role.DECORATION,
loc=Cq.Location(0, 0, self.casing_thickness)
)
.addS(
self.casing_bot(),
name="casing_bot",
material=self.material_casing,
role=Role.CASING,
)
.addS(
self.casing_mid(),
name="casing_mid",
material=self.material_casing,
role=Role.CASING,
loc=Cq.Location(0, 0, self.casing_thickness)
)
.addS(
self.casing_top(),
name="casing_top",
material=self.material_casing,
role=Role.CASING,
loc=Cq.Location(0, 0, self.core_thickness + self.casing_thickness)
)
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,264 @@
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.materials import Role, Material
import nhf.utils
from nhf.parts.fasteners import FlatHeadBolt, HexNut, Washer
from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650
from typing import Optional, Union
import math
from dataclasses import dataclass, field
import cadquery as Cq
NUT_COMMON = HexNut(
# FIXME: weigh
mass=0.0,
diam_thread=6.0,
pitch=1.0,
thickness=5.0,
width=9.89,
)
BOLT_COMMON = FlatHeadBolt(
# FIXME: weigh
mass=0.0,
diam_head=12.8,
height_head=2.8,
diam_thread=6.0,
height_thread=30.0,
pitch=1.0,
)
@dataclass
class Shimenawa(Model):
"""
The ring
"""
diam_inner: float = 43.0
diam_outer: float = 43.0 + 9 * 2
diam_hole_outer: float = 8.0
hole_ext: float = 2.0
hole_z: float = 15.0
pipe_fitting_angle_span: float = 6.0
pipe_joint_length: float = 120.0
pipe_joint_outer_thickness: float = 5.0
pipe_joint_inner_thickness: float = 4.0
pipe_joint_inner_angle_span: float = 120.0
pipe_joint_taper: float = 5.0
pipe_joint_taper_length: float = 10.0
ear_dr: float = 6.0
ear_hole_diam: float = 10.0
ear_radius: float = 15.0
ear_thickness: float = 10.0
main_circumference: float = 3600.0
material_fitting: Material = Material.PLASTIC_PLA
def __post_init__(self):
assert self.diam_inner < self.diam_outer
@property
def main_radius(self) -> float:
return self.main_circumference / (2 * math.pi)
@target(name="pipe-fitting-curved")
def pipe_fitting_curved(self) -> Cq.Workplane:
r_minor = self.diam_outer/2 + self.pipe_joint_outer_thickness
a1 = self.pipe_fitting_angle_span
outer = Cq.Solid.makeTorus(
radius1=self.main_radius,
radius2=r_minor,
)
inner = Cq.Solid.makeTorus(
radius1=self.main_radius,
radius2=self.diam_outer/2,
)
angle_intersector = Cq.Solid.makeCylinder(
radius=self.main_radius + r_minor,
height=r_minor*2,
angleDegrees=a1,
pnt=(0,0,-r_minor)
).rotate((0,0,0),(0,0,1),-a1/2)
result = (outer - inner) * angle_intersector
ear_outer = Cq.Solid.makeCylinder(
radius=self.ear_radius,
height=self.ear_thickness,
pnt=(0,-self.ear_thickness/2,0),
dir=(0,1,0),
)
ear_hole = Cq.Solid.makeCylinder(
radius=self.ear_hole_diam/2,
height=self.ear_thickness,
pnt=(-self.ear_dr,-self.ear_thickness/2,0),
dir=(0,1,0),
)
ear = (ear_outer - ear_hole).moved(self.main_radius - r_minor, 0, 0)
result += ear - inner
return result
@target(name="pipe-joint-outer")
def pipe_joint_outer(self) -> Cq.Workplane:
"""
Used to joint two pipes together (outside)
"""
r1 = self.diam_outer / 2 + self.pipe_joint_outer_thickness
h = self.pipe_joint_length
result = (
Cq.Workplane()
.cylinder(
radius=r1,
height=self.pipe_joint_length,
)
)
cut_interior = Cq.Solid.makeCylinder(
radius=self.diam_outer/2,
height=h,
pnt=(0, 0, -h/2)
)
rh = r1 + self.hole_ext
add_hole = Cq.Solid.makeCylinder(
radius=self.diam_hole_outer/2,
height=rh*2,
pnt=(-rh, 0, 0),
dir=(1, 0, 0),
)
cut_hole = Cq.Solid.makeCylinder(
radius=BOLT_COMMON.diam_thread/2,
height=rh*2,
pnt=(-rh, 0, 0),
dir=(1, 0, 0),
)
z = self.hole_z
result = (
result
+ add_hole.moved(0, 0, -z)
+ add_hole.moved(0, 0, z)
- cut_hole.moved(0, 0, -z)
- cut_hole.moved(0, 0, z)
- cut_interior
)
ear_outer = Cq.Solid.makeCylinder(
radius=self.ear_radius,
height=self.ear_thickness,
pnt=(0, r1, -self.ear_thickness/2),
)
ear_hole = Cq.Solid.makeCylinder(
radius=self.ear_hole_diam/2,
height=self.ear_thickness,
pnt=(0,r1+self.ear_dr,-self.ear_thickness/2),
)
ear = ear_outer - ear_hole - cut_interior
return result + ear
@target(name="pipe-joint-inner")
def pipe_joint_inner(self) -> Cq.Workplane:
"""
Used to joint two pipes together (inside)
"""
r1 = self.diam_inner / 2
r2 = r1 - self.pipe_joint_taper
r3 = r2 - self.pipe_joint_inner_thickness
h = self.pipe_joint_length
h0 = h - self.pipe_joint_taper_length*2
core = Cq.Solid.makeCylinder(
radius=r2,
height=h0/2,
)
centre_cut = Cq.Solid.makeCylinder(
radius=r3,
height=h0/2,
)
taper = Cq.Solid.makeCone(
radius1=r2,
radius2=r1,
height=(h - h0) / 2,
pnt=(0, 0, h0/2),
)
centre_cut_taper = Cq.Solid.makeCone(
radius1=r3,
radius2=r3 + self.pipe_joint_taper,
height=(h - h0) / 2,
pnt=(0, 0, h0/2),
)
angle_intersector = Cq.Solid.makeCylinder(
radius=r1,
height=h,
angleDegrees=self.pipe_joint_inner_angle_span
).rotate((0,0,0), (0,0,1), -self.pipe_joint_inner_angle_span/2)
result = (taper + core - centre_cut - centre_cut_taper) * angle_intersector
result += result.mirror("XY")
add_hole = Cq.Solid.makeCylinder(
radius=self.diam_hole_outer/2,
height=self.hole_ext,
pnt=(r3, 0, 0),
dir=(-1, 0, 0),
)
cut_hole = Cq.Solid.makeCylinder(
radius=BOLT_COMMON.diam_thread/2,
height=r1,
pnt=(0, 0, 0),
dir=(r1, 0, 0),
)
z = self.hole_z
# avoid collisions
nut_x = r3 - self.hole_ext - NUT_COMMON.thickness
nut = NUT_COMMON.generate().val().rotate((0,0,0),(0,1,0),90)
result = (
result
+ add_hole.moved(0, 0, z)
+ add_hole.moved(0, 0, -z)
- cut_hole.moved(0, 0, z)
- cut_hole.moved(0, 0, -z)
- nut.moved(nut_x, 0, z)
- nut.moved(nut_x, 0, -z)
)
return result
@assembly()
def assembly_pipe_joint(self) -> Cq.Assembly:
a = (
Cq.Assembly()
.addS(
self.pipe_joint_outer(),
name="joint_outer",
material=self.material_fitting,
role=Role.STRUCTURE,
)
.addS(
self.pipe_joint_inner(),
name="joint_inner1",
material=self.material_fitting,
role=Role.STRUCTURE,
)
.addS(
self.pipe_joint_inner(),
name="joint_inner2",
material=self.material_fitting,
role=Role.STRUCTURE,
loc=Cq.Location.rot2d(180),
)
)
return a
@assembly()
def assembly(self) -> Cq.Assembly:
a = (
Cq.Assembly()
.addS(
self.pipe_fitting_curved(),
name="fitting1",
material=self.material_fitting,
role=Role.STRUCTURE,
)
.add(
self.assembly_pipe_joint(),
name="pipe_joint",
)
)
return a

View File

@ -1,13 +1,11 @@
"""
Utility functions for cadquery objects
"""
import functools
import math
from typing import Optional
import functools, math
from typing import Optional, Union, Tuple, cast
import cadquery as Cq
from cadquery.occ_impl.solver import ConstraintSpec
from nhf import Role
from typing import Union, Tuple, cast
from nhf.materials import KEY_ITEM, KEY_MATERIAL
# Bug fixes
@ -55,6 +53,11 @@ def is2d(self: Cq.Location) -> bool:
return z == 0 and rx == 0 and ry == 0
Cq.Location.is2d = is2d
def scale(self: Cq.Location, fac: float) -> bool:
(x, y, z), (rx, ry, rz) = self.toTuple()
return Cq.Location(x*fac, y*fac, z*fac, rx, ry, rz)
Cq.Location.scale = scale
def to2d(self: Cq.Location) -> Tuple[Tuple[float, float], float]:
"""
Returns position and angle
@ -93,17 +96,24 @@ Cq.Location.with_angle_2d = with_angle_2d
def flip_x(self: Cq.Location) -> Cq.Location:
(x, y), a = self.to2d()
return Cq.Location.from2d(-x, y, 90 - a)
return Cq.Location.from2d(-x, y, 180 - a)
Cq.Location.flip_x = flip_x
def flip_y(self: Cq.Location) -> Cq.Location:
(x, y), a = self.to2d()
return Cq.Location.from2d(x, -y, -a)
Cq.Location.flip_y = flip_y
def boolean(self: Cq.Sketch, obj, **kwargs) -> Cq.Sketch:
def boolean(
self: Cq.Sketch,
obj: Union[Cq.Face, Cq.Sketch, Cq.Compound],
**kwargs) -> Cq.Sketch:
"""
Performs Boolean operation between a sketch and a sketch-like object
"""
return (
self
.reset()
# Has to be 0, 0. Translation doesn't work.
.push([(0, 0)])
.each(lambda _: obj, **kwargs)
)
@ -152,6 +162,15 @@ def tagPlane(self, tag: str,
Cq.Workplane.tagPlane = tagPlane
def tag_absolute(
self,
tag: str,
loc: Union[Cq.Location, Tuple[float, float, float]],
direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'):
return self.pushPoints([loc]).tagPlane(tag, direction=direction)
Cq.Workplane.tagAbsolute = tag_absolute
def make_sphere(r: float = 2) -> Cq.Solid:
"""
Makes a full sphere. The default function makes a hemisphere

845
poetry.lock generated
View File

@ -1,845 +0,0 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "anytree"
version = "2.12.1"
description = "Powerful and Lightweight Python Tree Data Structure with various plugins"
optional = false
python-versions = ">=3.7.2,<4"
files = [
{file = "anytree-2.12.1-py3-none-any.whl", hash = "sha256:5ea9e61caf96db1e5b3d0a914378d2cd83c269dfce1fb8242ce96589fa3382f0"},
{file = "anytree-2.12.1.tar.gz", hash = "sha256:244def434ccf31b668ed282954e5d315b4e066c4940b94aff4a7962d85947830"},
]
[package.dependencies]
six = "*"
[[package]]
name = "asttokens"
version = "2.4.1"
description = "Annotate AST trees with source code positions"
optional = false
python-versions = "*"
files = [
{file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"},
{file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"},
]
[package.dependencies]
six = ">=1.12.0"
[package.extras]
astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"]
test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
[[package]]
name = "build123d"
version = "0.5.0"
description = "A python CAD programming library"
optional = false
python-versions = ">=3.9"
files = [
{file = "build123d-0.5.0-py3-none-any.whl", hash = "sha256:d0a4e82cdb0e53ef21fca8d2c84124351d7c7070077b5efa173d789002c8194c"},
]
[package.dependencies]
anytree = ">=2.8.0,<3"
cadquery-ocp = ">=7.7.0"
ezdxf = ">=1.0.0,<2"
ipython = ">=8.0.0,<9"
numpy = ">=1.24.1,<2"
numpy-stl = ">=3.0.0,<4"
ocpsvg = "*"
py-lib3mf = ">=2.3.1"
svgpathtools = ">=1.5.1,<2"
trianglesolver = "*"
typing-extensions = ">=4.6.0,<5"
[[package]]
name = "cadquery"
version = "2.5.0.dev0"
description = "CadQuery is a parametric scripting language for creating and traversing CAD models"
optional = false
python-versions = ">=3.9"
files = []
develop = false
[package.dependencies]
cadquery-ocp = ">=7.7.0a0,<7.8"
casadi = "*"
ezdxf = "*"
multimethod = ">=1.11,<2.0"
nlopt = "*"
path = "*"
typish = "*"
[package.extras]
dev = ["black @ git+https://github.com/cadquery/black.git@cq", "docutils", "ipython", "pytest"]
ipython = ["ipython"]
[package.source]
type = "git"
url = "https://github.com/CadQuery/cadquery.git"
reference = "HEAD"
resolved_reference = "8ea37a71d40d383b55b8009c68987526f47a7613"
[[package]]
name = "cadquery-ocp"
version = "7.7.2"
description = "OCP+VTK wheel with shared library dependencies bundled."
optional = false
python-versions = "*"
files = [
{file = "cadquery_ocp-7.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f2faf716855f9a8372493e001e6e318da75b046472e7ee69881d2a4e6b65a04"},
{file = "cadquery_ocp-7.7.2-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:35e2c8ca923e4ba77b833357eb14c7e8a939f17285ef6acde7c26961989a8dfe"},
{file = "cadquery_ocp-7.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:4fc230711d52a9e7d71ab4c79c957287c7f2f85e5f09e036b573e6851df4d1ba"},
{file = "cadquery_ocp-7.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:488401fa4d070e3eb17729fb8b6494bd3577f0913c4e901e674c4d8fcd9ac1fe"},
{file = "cadquery_ocp-7.7.2-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:2be9408551c03e1c715e3b158b41ce458178ecf530ba644e3e884a5989feb611"},
{file = "cadquery_ocp-7.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:8e1f90250924ac50344bf0867e3d9a927f077583351b270b4e0e171033dd2046"},
{file = "cadquery_ocp-7.7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:78a7903267cd9986181b634a315ca66fac1bb9e6dbfaf60724d812b3a2cc77bf"},
{file = "cadquery_ocp-7.7.2-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:7ac2d83b4f4b3d7c35421a1d9f8fec2adc73b6ed6cac50c1ffcede5552e38e9b"},
{file = "cadquery_ocp-7.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9e5c8ef5aeb1d3fb8f0b207ed1a2b781d22dc3e9b9c0551f52342f5a6ee7fc5"},
{file = "cadquery_ocp-7.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7134e629559ba110a4d0cda53cb6a30c0af52524f7a6151e772d4b720ef508be"},
{file = "cadquery_ocp-7.7.2-cp38-cp38-manylinux_2_35_x86_64.whl", hash = "sha256:73183f141514e507c45a1e4ba1d25f1fdc604b20dd42bed7c8168f468632686b"},
{file = "cadquery_ocp-7.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:9e40840e1f4da5e0a8f1eb2168efc33ae65ac8e046c27a49936a2e9931fc1711"},
{file = "cadquery_ocp-7.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f6750c84f8af930f366ad22397141f3c332c39fb826edf3fe26c4e6938690f7"},
{file = "cadquery_ocp-7.7.2-cp39-cp39-manylinux_2_35_x86_64.whl", hash = "sha256:9f6c601d1db66353f0ef7c63fde70411573e917c16fb4c7cb5d92ed85b72f154"},
{file = "cadquery_ocp-7.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:feea223eaa2dfa33684f568b5ba2b02c35e96b5d894014f98927b5c08041a6be"},
]
[[package]]
name = "casadi"
version = "3.6.5"
description = "CasADi -- framework for algorithmic differentiation and numeric optimization"
optional = false
python-versions = "*"
files = [
{file = "casadi-3.6.5-cp27-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:6039081fdd1daf4ef7fa2b52814a954d75bfc03eb0dc62414e02af5d25746e8f"},
{file = "casadi-3.6.5-cp27-none-manylinux1_i686.whl", hash = "sha256:b5192dfabf6f5266b168b984d124dd3086c1c5a408c0743ff3a82290a8ccf3b5"},
{file = "casadi-3.6.5-cp27-none-manylinux2010_x86_64.whl", hash = "sha256:35b2ff6098e386a4d5e8bc681744e52bcd2f2f15cfa44c09814a8979b51a6794"},
{file = "casadi-3.6.5-cp27-none-win_amd64.whl", hash = "sha256:caf395d1e36bfb215b154e8df61583d534a07ddabb18cbe50f259b7692a41ac8"},
{file = "casadi-3.6.5-cp310-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:314886ef44bd01f1a98579e7784a3bed6e0584e88f9465cf9596af2523efb0dd"},
{file = "casadi-3.6.5-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:c6789c8060a99b329bb584d97c1eab6a5e4f3e2d2db391e6c2001c6323774990"},
{file = "casadi-3.6.5-cp310-none-manylinux2014_aarch64.whl", hash = "sha256:e40afb3c062817dd6ce2497cd001f00f107ee1ea41ec4d6ee9f9a5056d219e83"},
{file = "casadi-3.6.5-cp310-none-manylinux2014_i686.whl", hash = "sha256:ee5a4ed50d2becd0bd6d203c7a60ffad27c14a3e0ae357480de11c846a8dd928"},
{file = "casadi-3.6.5-cp310-none-manylinux2014_x86_64.whl", hash = "sha256:1ddb6e4afdd1da95d7d9d652ed973c1b7f50ef1454965a9170b657e223a2c73e"},
{file = "casadi-3.6.5-cp310-none-win_amd64.whl", hash = "sha256:e96ca81b00b9621007d45db1254fcf232d518ebcc802f42853f57b4df977c567"},
{file = "casadi-3.6.5-cp311-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:bebd3909db24ba711e094aacc0a2329b9903d422d73f61be851873731244b7d1"},
{file = "casadi-3.6.5-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:ccb962ea02b7d6d245d5cd40fb52c29e812040a45273c6eed32cb8fcff673dda"},
{file = "casadi-3.6.5-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:1ce199a4ea1d376edbe5399cd622a4564040c83f50c50114fe50a69a8ea81d92"},
{file = "casadi-3.6.5-cp311-none-manylinux2014_i686.whl", hash = "sha256:d12b67d467a5b2b0a909378ef7231fbc9af0da923baa13b1d5464d8471601ac3"},
{file = "casadi-3.6.5-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:3a3fb8af868f83d4a4f26d878c49f4acc4ed7ee92e731c73e650e5893418a634"},
{file = "casadi-3.6.5-cp311-none-win_amd64.whl", hash = "sha256:3bdd645151beda013af5fd019fb554756e7dac37541b9f120cdfba90405b2671"},
{file = "casadi-3.6.5-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:33afd1a4da0c86b4316953fe541635a8a7dc51703282e24a870ada13a46adb53"},
{file = "casadi-3.6.5-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:0d6ee0558b4ecdd8aa4aa70fd31528b135801f1086c28a9cb78d8e8242b7aedd"},
{file = "casadi-3.6.5-cp312-none-manylinux2014_i686.whl", hash = "sha256:be40e9897d80fb72a97e750b2143c32f63f8800cfb78f9b396d8ce7a913fca39"},
{file = "casadi-3.6.5-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:0118637823e292a9270133e02c9c6d3f3c7f75e8c91a6f6dc5275ade82dd1d9d"},
{file = "casadi-3.6.5-cp312-none-win_amd64.whl", hash = "sha256:fe2b64d777e36cc3f101220dd1e219a0e11c3e4ee2b5e708b30fea9a27107e41"},
{file = "casadi-3.6.5-cp35-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:a1ae36449adec534125d4af5be912b6fb9dafe74d1fee39f6c82263695e21ca5"},
{file = "casadi-3.6.5-cp35-none-manylinux1_i686.whl", hash = "sha256:32644c47fbfb643d5cf9769c7bbc94c6bdb9a40ea9c12c54af5e2754599c3186"},
{file = "casadi-3.6.5-cp35-none-manylinux2010_x86_64.whl", hash = "sha256:601b76b7afcb27b11563999f6ad1d9d2a2510ab3d00a6f4ce86a0bee97c9d17a"},
{file = "casadi-3.6.5-cp35-none-win_amd64.whl", hash = "sha256:febc645bcc0aed6d7a2bdb6e58b9a89cb8f74b19bc028c41cc807d75a5d54058"},
{file = "casadi-3.6.5-cp36-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:c98e68023c9e5905d9d6b99ae1fbbfe4b85ba9846b3685408bb498b20509f99a"},
{file = "casadi-3.6.5-cp36-none-manylinux2014_aarch64.whl", hash = "sha256:eb311088dca5359acc05aa4d8895bf99afaa16c7c04b27bf640ce4c2361b8cde"},
{file = "casadi-3.6.5-cp36-none-manylinux2014_i686.whl", hash = "sha256:bceb69bf9f04fded8a564eb64e298d19e945eaf4734f7145a5ee61cf9ac693e7"},
{file = "casadi-3.6.5-cp36-none-manylinux2014_x86_64.whl", hash = "sha256:c951031e26d987986dbc334492b2e6ef108077f11c00e178ff4007e4a9bf91d8"},
{file = "casadi-3.6.5-cp36-none-win_amd64.whl", hash = "sha256:e44af450ce944649932f9ef63ff00d2d21f642b506444418b4b20e69dba3adaf"},
{file = "casadi-3.6.5-cp37-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:c661fe88a93b7cc7ea42802aac76a674135cd65e3e564a6f08570dd3bea05201"},
{file = "casadi-3.6.5-cp37-none-manylinux2014_aarch64.whl", hash = "sha256:5266fc82e39352e26cb1a4e0a5c3deb32d09e6333be637bd78c273fa50f9012b"},
{file = "casadi-3.6.5-cp37-none-manylinux2014_i686.whl", hash = "sha256:02d6fb63c460abd99a450e861034d97568a8aec621fc0a4fed22f7494989c682"},
{file = "casadi-3.6.5-cp37-none-manylinux2014_x86_64.whl", hash = "sha256:5e8adffe2015cde370fc545b2d0fe731e96e583e4ea4c5f3044e818fea975cfc"},
{file = "casadi-3.6.5-cp37-none-win_amd64.whl", hash = "sha256:7ea8545579872b6f5412985dafec26b906b67bd4639a6c718b7e07f802af4e42"},
{file = "casadi-3.6.5-cp38-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:0a38bf808bf51368607c64307dd77a7363fbe8e5c910cd5c605546be60edfaff"},
{file = "casadi-3.6.5-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:f62f779481b30e5ea88392bdb8225e9545a21c4460dc3e96c2b782405b938d04"},
{file = "casadi-3.6.5-cp38-none-manylinux2014_aarch64.whl", hash = "sha256:deb2cb2bee8aba0c2cad03c832965b51ca305d0f8eb15de8b857ba86a76f0db0"},
{file = "casadi-3.6.5-cp38-none-manylinux2014_i686.whl", hash = "sha256:f6e10b66d6ae8216dab01532f7ad75cc9d66a95125d421b33d078a51ea0fc2a0"},
{file = "casadi-3.6.5-cp38-none-manylinux2014_x86_64.whl", hash = "sha256:f9e82658c910e3317535d769334260e0a24d97bbce68cadb72f592e9fcbafd61"},
{file = "casadi-3.6.5-cp38-none-win_amd64.whl", hash = "sha256:092e448e05feaed8958d684e896d909e756d199b84d3b9d0182da38cd3deebf6"},
{file = "casadi-3.6.5-cp39-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:f9c1de9a798767c00f89c27677b74059df4c9601d69270967b06d7fcff204b4d"},
{file = "casadi-3.6.5-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:83e3404de4449cb7382e49d811eec79cd370e64b97b5c94b155c604d7c523a40"},
{file = "casadi-3.6.5-cp39-none-manylinux2014_aarch64.whl", hash = "sha256:af95de5aa5942d627d43312834791623384c2ad6ba87928bf0e3cacc8a6698e8"},
{file = "casadi-3.6.5-cp39-none-manylinux2014_i686.whl", hash = "sha256:dbeb50726603454a1f85323cba7caf72524cd43ca0aeb1f286d07005a967ece9"},
{file = "casadi-3.6.5-cp39-none-manylinux2014_x86_64.whl", hash = "sha256:8bbfb2eb8cb6b9e2384814d6427e48bcf6df049bf7ed05b0a58bb311a1fbf18c"},
{file = "casadi-3.6.5-cp39-none-win_amd64.whl", hash = "sha256:0e4a4ec2e26ebeb22b0c129f2db3cf90f730cf9fbe98adb9a12720ff6ca1834a"},
{file = "casadi-3.6.5.tar.gz", hash = "sha256:409a5f6725eadea40fddfb8ba2321139b5252edac8bc115a72f68e648631d56a"},
]
[package.dependencies]
numpy = "*"
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "decorator"
version = "5.1.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.5"
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "executing"
version = "2.0.1"
description = "Get the currently executing AST node of a frame, and other information"
optional = false
python-versions = ">=3.5"
files = [
{file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"},
{file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"},
]
[package.extras]
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
[[package]]
name = "ezdxf"
version = "1.3.2"
description = "A Python package to create/manipulate DXF drawings."
optional = false
python-versions = ">=3.9"
files = [
{file = "ezdxf-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6f4eacaa8d55ddcbd64795409ff4f5e452c4b066f4e33b210bc4c6189c71ec6f"},
{file = "ezdxf-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35d1fa27f175d2b648f3aa5f31448b81ae8fe3627b0e908862a15983bdeb191b"},
{file = "ezdxf-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:240f7e894fe0364585d28e8f697c12e93db6fbb426c37d6a3f43a027c57d6dbf"},
{file = "ezdxf-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c20adceb7c78e1370f117615c245a293bc7fe65525457eeb287d24fa4cd96c8"},
{file = "ezdxf-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a311c455a84e7c2f03cefa0922fa4919d6950e9207e8e7175893507889852012"},
{file = "ezdxf-1.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8c2955db7f41596b7245441090d02b083cae060110fd595abc2f3347bfd3cb09"},
{file = "ezdxf-1.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:120273751ca4818d87a216cfd0f74d0fc73518b5ec052aa8c17bad9711463e48"},
{file = "ezdxf-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:90274032eb4b047af2b38f71bca749dc6bff2110bb2f4c818f5f48e6864e6a97"},
{file = "ezdxf-1.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:464689421c55e1c9d193da46ea461bfc82a1c0ab0007a37cbaefb44189385b04"},
{file = "ezdxf-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7a39234e9ccb072e2362b086f511706ce76ac5774ddb618fe7ca6710b5418f72"},
{file = "ezdxf-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:193f5146e6c8b93e6293248467d8b0c38fa12fc41b85507300f15e85b73ce219"},
{file = "ezdxf-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e37534530c9734c927f6afafe1f3f5a6fdbde2bbf438a661173ff0ba86de8937"},
{file = "ezdxf-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6118e375852f6db04b66c0111ded47c0e0acd42869a43aaa302815b947c5e8de"},
{file = "ezdxf-1.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:420b6d7f80fa1bff374c7fb611ba8aef071d5523dbab9ad3a64465f7b2ac82cc"},
{file = "ezdxf-1.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f60ada8f7b0d232a6d45cbfec4b205dc7a1beb94bb90a2518893e7a9b43681c6"},
{file = "ezdxf-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:06550cf39bf60f62a1db3ee43426a8c66485fc946a44324d921a901f7d35bfe7"},
{file = "ezdxf-1.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1a1bcda7d2d97f3aa3fb0db14006c91000ad51cd5aa16d51b73d42b3e88a794e"},
{file = "ezdxf-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cd36e1430b6150e071466f1bd712aad8552c986a165fcabd1c74b47cf72684d6"},
{file = "ezdxf-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e6a645036c3874c1693e6e2411647645ab67882e5c0c762f700e55ac9a0dc56"},
{file = "ezdxf-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c12e9602abc8444dc5606e0c39cb6826df17e7c1a01d576d586f0a39696d539d"},
{file = "ezdxf-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77aed29c3d14067c2e7986057b6fe6842167b89d6a35df5d1636b6627e1ea117"},
{file = "ezdxf-1.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e0881f8fb4fa6386ef963a657bc7291f5ec3029844ba6e7a905c9f9b713ccae"},
{file = "ezdxf-1.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fddf6cfd0bf7fe78273918986f917b4f515d9a6371ee1b8cf310d4cd879d33e9"},
{file = "ezdxf-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:d504f843c20e9b7c2d331352ac91710bd6ebd14cf56c576a3432dacdfdde7106"},
{file = "ezdxf-1.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bcd10a9ac39728d949d0edfd7eb460707d4b4620d37db41b4790c3c871dbab"},
{file = "ezdxf-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4795843993061f9a3127e41328c5c02483ba619fda53b91bbe1e764b4294ad31"},
{file = "ezdxf-1.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cfcb2bee332917b1f7353f30d8cfe1e24774034e86d1f1360eaa0675b2c402bf"},
{file = "ezdxf-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13acf2a25854d735b23ba569500aa9222ae34862a5dc39a3bb867089b884274"},
{file = "ezdxf-1.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f3fd73b9f654491864e37153d86ceb14cfae6cc78d0693259cea49bdcd935882"},
{file = "ezdxf-1.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5629cb3a21ccc3895b57a507f046951a76836b9aaafff7dd5c1cda67ef258271"},
{file = "ezdxf-1.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6615464a6b2a6af282716f0ab3f218e0a8abf27604e2cc638ee27285b29c8034"},
{file = "ezdxf-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:e4f3dd9c93623c25488f7cddbd2914a9a18b29fc32c7ae5a95a3915b149836dc"},
{file = "ezdxf-1.3.2-py3-none-any.whl", hash = "sha256:4451a04765323e93df943a0584db50f3851be0ca4aa8b8a4ee809faf492b3a5d"},
{file = "ezdxf-1.3.2.zip", hash = "sha256:ecaa9e69f20fb66245164f235e616dd0789a11ac8a72a0302780b77621e1c354"},
]
[package.dependencies]
fonttools = "*"
numpy = "*"
pyparsing = ">=2.0.1"
typing_extensions = ">=4.6.0"
[package.extras]
dev = ["Cython", "Pillow", "PyMuPDF (>=1.20.0)", "PySide6", "matplotlib", "pytest", "setuptools", "wheel"]
dev5 = ["Cython", "Pillow", "PyMuPDF (>=1.20.0)", "PyQt5", "matplotlib", "pytest", "setuptools", "wheel"]
draw = ["Pillow", "PyMuPDF (>=1.20.0)", "PySide6", "matplotlib"]
draw5 = ["Pillow", "PyMuPDF (>=1.20.0)", "PyQt5", "matplotlib"]
[[package]]
name = "fonttools"
version = "4.53.1"
description = "Tools to manipulate font files"
optional = false
python-versions = ">=3.8"
files = [
{file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"},
{file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"},
{file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"},
{file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"},
{file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"},
{file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"},
{file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"},
{file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"},
{file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"},
{file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"},
{file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"},
{file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"},
{file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"},
{file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"},
{file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"},
{file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"},
{file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"},
{file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"},
{file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"},
{file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"},
{file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"},
{file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"},
{file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"},
{file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"},
{file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"},
{file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"},
{file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"},
{file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"},
{file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"},
{file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"},
{file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"},
{file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"},
{file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"},
{file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"},
{file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"},
{file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"},
{file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"},
{file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"},
{file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"},
{file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"},
{file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"},
{file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"},
]
[package.extras]
all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"]
graphite = ["lz4 (>=1.7.4.2)"]
interpolatable = ["munkres", "pycairo", "scipy"]
lxml = ["lxml (>=4.0)"]
pathops = ["skia-pathops (>=0.5.0)"]
plot = ["matplotlib"]
repacker = ["uharfbuzz (>=0.23.0)"]
symfont = ["sympy"]
type1 = ["xattr"]
ufo = ["fs (>=2.2.0,<3)"]
unicode = ["unicodedata2 (>=15.1.0)"]
woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
[[package]]
name = "ipython"
version = "8.26.0"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.10"
files = [
{file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"},
{file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
prompt-toolkit = ">=3.0.41,<3.1.0"
pygments = ">=2.4.0"
stack-data = "*"
traitlets = ">=5.13.0"
typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
[package.extras]
all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"]
black = ["black"]
doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"]
kernel = ["ipykernel"]
matplotlib = ["matplotlib"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["ipywidgets", "notebook"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
[[package]]
name = "jedi"
version = "0.19.1"
description = "An autocompletion tool for Python that can be used for text editors."
optional = false
python-versions = ">=3.6"
files = [
{file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
{file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
]
[package.dependencies]
parso = ">=0.8.3,<0.9.0"
[package.extras]
docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
[[package]]
name = "matplotlib-inline"
version = "0.1.7"
description = "Inline Matplotlib backend for Jupyter"
optional = false
python-versions = ">=3.8"
files = [
{file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
{file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
]
[package.dependencies]
traitlets = "*"
[[package]]
name = "multimethod"
version = "1.12"
description = "Multiple argument dispatching."
optional = false
python-versions = ">=3.9"
files = [
{file = "multimethod-1.12-py3-none-any.whl", hash = "sha256:fd0c473c43558908d97cc06e4d68e8f69202f167db46f7b4e4058893e7dbdf60"},
{file = "multimethod-1.12.tar.gz", hash = "sha256:8db8ef2a8d2a247e3570cc23317680892fdf903d84c8c1053667c8e8f7671a67"},
]
[[package]]
name = "nlopt"
version = "2.7.1"
description = "Library for nonlinear optimization, wrapping many algorithms for global and local, constrained or unconstrained, optimization"
optional = false
python-versions = ">=3.6"
files = [
{file = "nlopt-2.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42b7883704e1285ff40d930699eb7fc7e1341229da33666b4163459cfdf89fb1"},
{file = "nlopt-2.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ba0862162248442fbf1f04b20a321c11ff40ff4442a12aaaafcdaff9abb0ab7"},
{file = "nlopt-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:426c18548d733640449d707c82eb57c09a5f01d4b064f87312808d194d227f24"},
{file = "nlopt-2.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7a12fe3cbfb36a6a18f84a1ac23ed3dda323860235381b3d2d182d8b771783ef"},
{file = "nlopt-2.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e1653de0060a42d6709423e6160888893bb688f4ff79aa0f1def4701ea25dd8"},
{file = "nlopt-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:88ec7cf491da150d497ecc61889bc7adb0af0ad05a67e925a4f5ac88e20f1b9c"},
{file = "nlopt-2.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:da0ac81b10f838afe7c1b99a2f895c31e05ca68328571fe430f382ce08cbfb07"},
{file = "nlopt-2.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:592ded3b34bb888cd99c5da3fb1c3c9269ddd996dade578a8ec325cd8b6be752"},
{file = "nlopt-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:1647131d53302e72f5c4851ab04a92401a342c3e0fcfaac0eda316f5e8f3b283"},
{file = "nlopt-2.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79791a2179d1cf708622eaeea76c88acbadc6af0d2f198df21a74473838686c3"},
{file = "nlopt-2.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad38bab99348f6c3bbf0d5f339b3fd77465b27ef44c330f4ba512a40b87b373"},
{file = "nlopt-2.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:479a415f522051f6d728a3279c013aab96a6eaf3c323a89582dcb07eb636f15f"},
{file = "nlopt-2.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d99f1d6217bc3ead6fa6fe84a923577003f9a5f760cd354a3f8dcd1e11d626ce"},
{file = "nlopt-2.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f9370bd37788b4ac792cf161835f1e4e9bbad8bfb5a76f75a295ae38dcd8d0"},
{file = "nlopt-2.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8e7b65cf3a751e822b02f28b65d0c548052523fa6333619af3f24fec60a6b6bd"},
{file = "nlopt-2.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:087ff54de5ec0375fd18f843b36e9a8590c0f1e194bb45d3119ba844aeb836dd"},
{file = "nlopt-2.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4a05448f0ffebbab7a6a822297430e018c848652280e6efa13484e210291d5c"},
{file = "nlopt-2.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:757c41210f3ab6173e5c508c79c7833e33cf90a068d098b1e13d277432120b81"},
]
[package.dependencies]
numpy = ">=1.14"
[[package]]
name = "numpy"
version = "1.26.4"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.9"
files = [
{file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
{file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
{file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"},
{file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"},
{file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"},
{file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"},
{file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"},
{file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"},
{file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"},
{file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"},
{file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"},
{file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"},
{file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"},
{file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"},
{file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"},
{file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"},
{file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"},
{file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"},
{file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"},
{file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"},
{file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"},
{file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"},
{file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"},
{file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"},
{file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"},
{file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"},
{file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"},
{file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"},
{file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"},
{file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"},
{file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"},
{file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"},
{file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"},
{file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"},
{file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"},
{file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"},
]
[[package]]
name = "numpy-stl"
version = "3.1.1"
description = "Library to make reading, writing and modifying both binary and ascii STL files easy."
optional = false
python-versions = ">3.6.0"
files = [
{file = "numpy-stl-3.1.1.tar.gz", hash = "sha256:f78eea62c80938bf53ea914fa5b6c92f448f0eab5609e0e5a737dde039404334"},
{file = "numpy_stl-3.1.1-py3-none-any.whl", hash = "sha256:b0b7f4455c29d26d3dc0eed894f5b17c64e4019b056d0060be48f93680f6e6d3"},
]
[package.dependencies]
numpy = "*"
python-utils = ">=3.4.5"
[[package]]
name = "ocpsvg"
version = "0.2.1"
description = ""
optional = false
python-versions = ">=3.9"
files = [
{file = "ocpsvg-0.2.1-py3-none-any.whl", hash = "sha256:e04c6fc6578a9a5c51ddfd9af9c10b9294c9cd3781e90b4ff693b58777fe6106"},
{file = "ocpsvg-0.2.1.tar.gz", hash = "sha256:8a089249b52b0bff99cca9698af3d74ef678ced2359316f8c2a04aee9baafb46"},
]
[package.dependencies]
cadquery-ocp = ">=7.7.0"
svgelements = ">=1.9.1,<2"
svgpathtools = ">=1.5.1,<2"
[package.extras]
dev = ["pytest"]
[[package]]
name = "parso"
version = "0.8.4"
description = "A Python Parser"
optional = false
python-versions = ">=3.6"
files = [
{file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"},
{file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"},
]
[package.extras]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["docopt", "pytest"]
[[package]]
name = "path"
version = "16.14.0"
description = "A module wrapper for os.path"
optional = false
python-versions = ">=3.8"
files = [
{file = "path-16.14.0-py3-none-any.whl", hash = "sha256:8ee37703cbdc7cc83835ed4ecc6b638226fb2b43b7b45f26b620589981a109a5"},
{file = "path-16.14.0.tar.gz", hash = "sha256:dbaaa7efd4602fd6ba8d82890dc7823d69e5de740a6e842d9919b0faaf2b6a8e"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["appdirs", "more-itertools", "packaging", "pygments", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "pywin32"]
[[package]]
name = "pexpect"
version = "4.9.0"
description = "Pexpect allows easy control of interactive console applications."
optional = false
python-versions = "*"
files = [
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
]
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "prompt-toolkit"
version = "3.0.47"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"},
{file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"},
]
[package.dependencies]
wcwidth = "*"
[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
optional = false
python-versions = "*"
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
[[package]]
name = "pure-eval"
version = "0.2.2"
description = "Safely evaluate AST nodes without side effects"
optional = false
python-versions = "*"
files = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
]
[package.extras]
tests = ["pytest"]
[[package]]
name = "py-lib3mf"
version = "2.3.1"
description = "A python package for Lib3MF tools"
optional = false
python-versions = ">=3.9"
files = [
{file = "py_lib3mf-2.3.1-py3-none-any.whl", hash = "sha256:86a870ef386debba9b74683d3a08125a34c153aaa65e967f61677cc5a0a65e24"},
]
[[package]]
name = "pygments"
version = "2.18.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyparsing"
version = "3.1.2"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
optional = false
python-versions = ">=3.6.8"
files = [
{file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
{file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
]
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "python-utils"
version = "3.8.2"
description = "Python Utils is a module with some convenient utilities not included with the standard Python install"
optional = false
python-versions = ">3.8.0"
files = [
{file = "python-utils-3.8.2.tar.gz", hash = "sha256:c5d161e4ca58ce3f8c540f035e018850b261a41e7cb98f6ccf8e1deb7174a1f1"},
{file = "python_utils-3.8.2-py2.py3-none-any.whl", hash = "sha256:ad0ccdbd6f856d015cace07f74828b9840b5c4072d9e868a7f6a14fd195555a8"},
]
[package.dependencies]
typing-extensions = ">3.10.0.2"
[package.extras]
docs = ["mock", "python-utils", "sphinx"]
loguru = ["loguru"]
tests = ["flake8", "loguru", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mypy", "sphinx", "types-setuptools"]
[[package]]
name = "scipy"
version = "1.14.0"
description = "Fundamental algorithms for scientific computing in Python"
optional = false
python-versions = ">=3.10"
files = [
{file = "scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484"},
{file = "scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6"},
{file = "scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7"},
{file = "scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1"},
{file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0"},
{file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0"},
{file = "scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d"},
{file = "scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359"},
{file = "scipy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d056a8709ccda6cf36cdd2eac597d13bc03dba38360f418560a93050c76a16e"},
{file = "scipy-1.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f0a50da861a7ec4573b7c716b2ebdcdf142b66b756a0d392c236ae568b3a93fb"},
{file = "scipy-1.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94c164a9e2498e68308e6e148646e486d979f7fcdb8b4cf34b5441894bdb9caf"},
{file = "scipy-1.14.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a7d46c3e0aea5c064e734c3eac5cf9eb1f8c4ceee756262f2c7327c4c2691c86"},
{file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eee2989868e274aae26125345584254d97c56194c072ed96cb433f32f692ed8"},
{file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3154691b9f7ed73778d746da2df67a19d046a6c8087c8b385bc4cdb2cfca74"},
{file = "scipy-1.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c40003d880f39c11c1edbae8144e3813904b10514cd3d3d00c277ae996488cdb"},
{file = "scipy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b083c8940028bb7e0b4172acafda6df762da1927b9091f9611b0bcd8676f2bc"},
{file = "scipy-1.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff2438ea1330e06e53c424893ec0072640dac00f29c6a43a575cbae4c99b2b9"},
{file = "scipy-1.14.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bbc0471b5f22c11c389075d091d3885693fd3f5e9a54ce051b46308bc787e5d4"},
{file = "scipy-1.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:64b2ff514a98cf2bb734a9f90d32dc89dc6ad4a4a36a312cd0d6327170339eb0"},
{file = "scipy-1.14.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:7d3da42fbbbb860211a811782504f38ae7aaec9de8764a9bef6b262de7a2b50f"},
{file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d91db2c41dd6c20646af280355d41dfa1ec7eead235642178bd57635a3f82209"},
{file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a01cc03bcdc777c9da3cfdcc74b5a75caffb48a6c39c8450a9a05f82c4250a14"},
{file = "scipy-1.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65df4da3c12a2bb9ad52b86b4dcf46813e869afb006e58be0f516bc370165159"},
{file = "scipy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c4161597c75043f7154238ef419c29a64ac4a7c889d588ea77690ac4d0d9b20"},
{file = "scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b"},
]
[package.dependencies]
numpy = ">=1.23.5,<2.3"
[package.extras]
dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"]
doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"]
test = ["Cython", "array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "stack-data"
version = "0.6.3"
description = "Extract data from python stack frames and tracebacks for informative displays"
optional = false
python-versions = "*"
files = [
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
]
[package.dependencies]
asttokens = ">=2.1.0"
executing = ">=1.2.0"
pure-eval = "*"
[package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "svgelements"
version = "1.9.6"
description = "Svg Elements Parsing"
optional = false
python-versions = "*"
files = [
{file = "svgelements-1.9.6-py2.py3-none-any.whl", hash = "sha256:8a5cf2cc066d98e713d5b875b1d6e5eeb9b92e855e835ebd7caab2713ae1dcad"},
{file = "svgelements-1.9.6.tar.gz", hash = "sha256:7c02ad6404cd3d1771fd50e40fbfc0550b0893933466f86a6eb815f3ba3f37f7"},
]
[[package]]
name = "svgpathtools"
version = "1.6.1"
description = "A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves."
optional = false
python-versions = "*"
files = [
{file = "svgpathtools-1.6.1-py2.py3-none-any.whl", hash = "sha256:39967f9a817b8a12cc6dd1646fc162d522fca6c3fd5f8c94913c15ee4cb3a906"},
{file = "svgpathtools-1.6.1.tar.gz", hash = "sha256:7054e6de1953e295bf565d535d585695453b09f8db4a2f7c4853348732097a3e"},
]
[package.dependencies]
numpy = "*"
scipy = "*"
svgwrite = "*"
[[package]]
name = "svgwrite"
version = "1.4.3"
description = "A Python library to create SVG drawings."
optional = false
python-versions = ">=3.6"
files = [
{file = "svgwrite-1.4.3-py3-none-any.whl", hash = "sha256:bb6b2b5450f1edbfa597d924f9ac2dd099e625562e492021d7dd614f65f8a22d"},
{file = "svgwrite-1.4.3.zip", hash = "sha256:a8fbdfd4443302a6619a7f76bc937fc683daf2628d9b737c891ec08b8ce524c3"},
]
[[package]]
name = "traitlets"
version = "5.14.3"
description = "Traitlets Python configuration system"
optional = false
python-versions = ">=3.8"
files = [
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
]
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
[[package]]
name = "trianglesolver"
version = "1.2"
description = "Find all the sides and angles of a triangle, if you know some of the sides and/or angles. (Uses the Law of Sines and Law of Cosines.)"
optional = false
python-versions = "*"
files = [
{file = "trianglesolver-1.2-py3-none-any.whl", hash = "sha256:aa0903c3708b4e2b496f06d490cae72c6ff6274b00d1edce420fcfa3b2b76682"},
{file = "trianglesolver-1.2.tar.gz", hash = "sha256:4af18aade579d5c0d64389b3e65aeaf06cff26319762ccd859e3268559a76aea"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "typish"
version = "1.9.3"
description = "Functionality for types"
optional = false
python-versions = "*"
files = [
{file = "typish-1.9.3-py3-none-any.whl", hash = "sha256:03cfee5e6eb856dbf90244e18f4e4c41044c8790d5779f4e775f63f982e2f896"},
]
[package.extras]
test = ["codecov", "coverage", "mypy", "nptyping (>=1.3.0)", "numpy", "pycodestyle", "pylint", "pytest"]
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "6fc2644e7778ba22f8f5f2bcb2ca54f03b325f62c8a3fcd1c265a17561d874b8"

View File

@ -1,21 +1,34 @@
[tool.poetry]
[project]
name = "nhf"
version = "0.1.0"
description = "NorCal Hakkero Factory No. 1 Cosplay Designs"
authors = ["Leni Aniva <v@leni.sh>"]
authors = [{ name = "Leni Aniva", email = "aniva@stanford.edu" }]
requires-python = ">=3.10,<3.13"
readme = "README.md"
dependencies = [
"cadquery==2.5.2",
"numpy>=2,<3",
"colorama>=0.4.6,<0.5",
"multimethod~=1.12",
"scipy>=1.14.0,<2",
"typish>=1.9.3,<2",
]
[tool.poetry.dependencies]
python = "^3.10"
cadquery = {git = "https://github.com/CadQuery/cadquery.git"}
build123d = "^0.5.0"
numpy = "^1.26.4"
colorama = "^0.4.6"
[dependency-groups]
dev = [
"cq-editor",
"pyqt5>=5.15.11,<6",
"logbook>=1.8.0,<2",
"spyder>=5,<6",
"pyqtgraph>=0.13.7,<0.14",
"unittest-parallel>=1.7.4",
]
# cadquery dependency
multimethod = "^1.12"
scipy = "^1.14.0"
[tool.uv]
[tool.uv.sources]
cq-editor = { git = "https://github.com/CadQuery/CQ-editor.git" }
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
requires = ["hatchling"]
build-backend = "hatchling.build"

2724
uv.lock Normal file

File diff suppressed because it is too large Load Diff