Compare commits

..

46 Commits

Author SHA1 Message Date
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 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 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
26 changed files with 9194 additions and 4898 deletions

View File

@ -1,6 +1,7 @@
# 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.
## Development
@ -15,6 +16,12 @@ and this should succeed
python3 -c "import nhf"
```
To visualize an object, create a file `visualize.py`, and run `cq-editor`:
``` sh
python3 -m cq_editor visualize.py
```
## Testing
Run all tests with

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

@ -84,6 +84,7 @@ 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)
METAL_BRASS = 8.5, _color('gold1', 0.8)
def __init__(self, density: float, color: Cq.Color):
self.density = density
@ -116,6 +117,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 +127,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

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

@ -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)
)

3905
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,15 +6,22 @@ authors = ["Leni Aniva <v@leni.sh>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
cadquery = {git = "https://github.com/CadQuery/cadquery.git"}
build123d = "^0.5.0"
numpy = "^1.26.4"
python = ">=3.10,<3.13"
cadquery = "2.5.2"
numpy = ">=2,<3"
colorama = "^0.4.6"
# cadquery dependency
multimethod = "^1.12"
scipy = "^1.14.0"
typish = "^1.9.3"
[tool.poetry.group.dev.dependencies]
cq-editor = {git = "https://github.com/CadQuery/CQ-editor.git"}
pyqt5 = "^5.15.11"
logbook = "^1.8.0"
spyder = "^5"
pyqtgraph = "^0.13.7"
[build-system]
requires = ["poetry-core"]