compas-timber 2.1.2__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {compas_timber-2.1.2/src/compas_timber.egg-info → compas_timber-2.2.0}/PKG-INFO +1 -1
- {compas_timber-2.1.2 → compas_timber-2.2.0}/pyproject.toml +1 -1
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/__init__.py +1 -1
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/__init__.py +6 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/butt_joint.py +113 -20
- compas_timber-2.2.0/src/compas_timber/connections/i_simple_scarf.py +294 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_butt.py +36 -34
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_french_ridge_lap.py +2 -2
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_miter.py +111 -14
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_butt.py +4 -4
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/utilities.py +124 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/x_notch.py +5 -5
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/beam.py +6 -6
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/panel.py +18 -8
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/plate.py +18 -8
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/plate_geometry.py +30 -4
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/__init__.py +3 -1
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/lap.py +9 -7
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/pocket.py +8 -6
- compas_timber-2.2.0/src/compas_timber/fabrication/simple_scarf.py +435 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0/src/compas_timber.egg-info}/PKG-INFO +1 -1
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/SOURCES.txt +2 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/LICENSE +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/README.md +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/setup.cfg +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/base.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/btlx/__init__.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/btlx/reader.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/ball_node.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/cluster.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/joint_candidate.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_lap.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_tenon_mortise.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/lap_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/mortise_tenon.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/oligina.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/panel_butt_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/panel_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/panel_miter_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/plate_butt_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/plate_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/plate_miter_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/solver.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_birdsmouth.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_dovetail.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_lap.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_step_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_tenon_mortise.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/x_lap.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/y_butt.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/__init__.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/fastener.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/fasteners/__init__.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/fasteners/ball_node_fastener.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/fasteners/plate_fastener.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/features.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/errors.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/btlx.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/double_cut.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/dovetail_mortise.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/dovetail_tenon.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/drilling.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/free_contour.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/french_ridge_lap.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/jack_cut.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/longitudinal_cut.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/mortise.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/slot.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/step_joint.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/step_joint_notch.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/tenon.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/text.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/geometry.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/model.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/panel_features/__init__.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/panel_features/opening.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/panel_features/panel_connection_interface.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/panel_features/panel_features.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/planning/__init__.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/planning/nesting.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/planning/sequencer.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/rhino/__init__.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/rhino/install.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/structural.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/utils/__init__.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/utils/r_tree.py +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/dependency_links.txt +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/not-zip-safe +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/requires.txt +0 -0
- {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compas_timber
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: COMPAS package for modeling, designing and fabricating timber assemblies.
|
|
5
5
|
Author-email: Aleksandra Anna Apolinarska <apolinarska@arch.ethz.ch>, Chen Kasirer <kasirer@arch.ethz.ch>, Gonzalo Casas <casas@arch.ethz.ch>, Jonas Haldemann <haldemann@arch.ethz.ch>, Oliver Appling Bucklin <bucklin@arch.ethz.ch>, "Aurèle L. Gheyselinck" <gheyselinck@arch.ethz.ch>, Panayiotis Papacharalambous <papacharalambous@arch.ethz.ch>, Anastasiia Stryzhevska <astryzhevska@arch.ethz.ch>, Jelle Feringa <jelleferinga@gmail.com>, Joseph Kenny <jk6372@princeton.edu>, Beverly Lytle <lytle@arch.ethz.ch>, Eric Gozzi <eric.gozzi@arch.ethz.ch>, Rodrigo Arca Zimmermann <rodrigo.arca@gmail.com>, Nicolas Benjamin Boscoboinik <boscoboinik@arch.ethz.ch>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -96,7 +96,7 @@ where = ["src"]
|
|
|
96
96
|
# ============================================================================
|
|
97
97
|
|
|
98
98
|
[tool.bumpversion]
|
|
99
|
-
current_version = "2.
|
|
99
|
+
current_version = "2.2.0"
|
|
100
100
|
message = "Bump version to {new_version}"
|
|
101
101
|
commit = true
|
|
102
102
|
tag = true
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from .joint import Joint
|
|
2
2
|
from .butt_joint import ButtJoint
|
|
3
|
+
from .i_simple_scarf import ISimpleScarf
|
|
3
4
|
from .l_butt import LButtJoint
|
|
4
5
|
from .l_lap import LLapJoint
|
|
5
6
|
from .l_miter import LMiterJoint
|
|
7
|
+
from .l_miter import MiterPlaneSpec
|
|
6
8
|
from .l_french_ridge_lap import LFrenchRidgeLapJoint
|
|
7
9
|
from .lap_joint import LapJoint
|
|
8
10
|
from .joint_candidate import JointCandidate
|
|
@@ -24,6 +26,7 @@ from .l_tenon_mortise import LTenonMortiseJoint
|
|
|
24
26
|
from .ball_node import BallNodeJoint
|
|
25
27
|
from .y_butt import YButtJoint
|
|
26
28
|
from .oligina import TOliGinaJoint
|
|
29
|
+
from .butt_joint import CutPlaneSpec
|
|
27
30
|
from .utilities import beam_ref_side_incidence
|
|
28
31
|
from .utilities import beam_ref_side_incidence_with_vector
|
|
29
32
|
from .utilities import point_centerline_towards_joint
|
|
@@ -46,6 +49,7 @@ __all__ = [
|
|
|
46
49
|
"LapJoint",
|
|
47
50
|
"ButtJoint",
|
|
48
51
|
"TButtJoint",
|
|
52
|
+
"ISimpleScarf",
|
|
49
53
|
"LButtJoint",
|
|
50
54
|
"TButtJoint",
|
|
51
55
|
"TStepJoint",
|
|
@@ -69,6 +73,8 @@ __all__ = [
|
|
|
69
73
|
"LTenonMortiseJoint",
|
|
70
74
|
"YButtJoint",
|
|
71
75
|
"TOliGinaJoint",
|
|
76
|
+
"CutPlaneSpec",
|
|
77
|
+
"MiterPlaneSpec",
|
|
72
78
|
"beam_ref_side_incidence",
|
|
73
79
|
"beam_ref_side_incidence_with_vector",
|
|
74
80
|
"point_centerline_towards_joint",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import math
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
7
|
+
from compas.data import Data
|
|
6
8
|
from compas.geometry import Plane
|
|
7
9
|
from compas.geometry import Polyhedron
|
|
10
|
+
from compas.geometry import dot_vectors
|
|
8
11
|
|
|
9
12
|
from compas_timber.errors import BeamJoiningError
|
|
10
13
|
from compas_timber.fabrication import JackRafterCutProxy
|
|
@@ -15,11 +18,91 @@ from compas_timber.geometry import polyhedron_from_box_planes
|
|
|
15
18
|
from .joint import Joint
|
|
16
19
|
from .solver import JointTopology
|
|
17
20
|
from .utilities import beam_ref_side_incidence
|
|
21
|
+
from .utilities import decompose_plane_to_ref_side
|
|
22
|
+
from .utilities import plane_from_ref_side_angle_offset
|
|
18
23
|
|
|
19
24
|
if TYPE_CHECKING:
|
|
20
25
|
from compas_timber.elements.beam import Beam
|
|
21
26
|
|
|
22
27
|
|
|
28
|
+
class CutPlaneSpec(Data):
|
|
29
|
+
"""A cutting plane stored relative to a beam's reference side.
|
|
30
|
+
|
|
31
|
+
Encodes a world-coordinate plane as a ``(ref_side_index, angle, offset)`` triple, matching the
|
|
32
|
+
parameterisation of :func:`plane_from_ref_side_angle_offset`. The encoded plane's line of
|
|
33
|
+
intersection with the reference side stays parallel to the beam's centerline (x-axis constraint).
|
|
34
|
+
Use the named constructors :meth:`from_butt_plane` and :meth:`from_back_plane` to build instances
|
|
35
|
+
from world-coordinate planes, and :meth:`to_plane` to reconstruct the plane at query time.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def __data__(self):
|
|
41
|
+
return {"ref_side_index": self.ref_side_index, "angle": self.angle, "offset": self.offset}
|
|
42
|
+
|
|
43
|
+
def __init__(self, ref_side_index: int, angle: float = 0.0, offset: float = 0.0):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.ref_side_index = ref_side_index
|
|
46
|
+
self.angle = angle
|
|
47
|
+
self.offset = offset
|
|
48
|
+
|
|
49
|
+
def to_plane(self, beam: Beam) -> Plane:
|
|
50
|
+
"""Reconstruct the world-coordinate plane relative to `beam`."""
|
|
51
|
+
ref_side = beam.ref_sides[self.ref_side_index]
|
|
52
|
+
return plane_from_ref_side_angle_offset(ref_side, self.angle, self.offset)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_butt_plane(cls, main_beam: Beam, cross_beam: Beam, plane: Plane) -> CutPlaneSpec:
|
|
56
|
+
"""Encode `plane` relative to the cross beam's face that is closest to the main beam.
|
|
57
|
+
|
|
58
|
+
Use this when the plane is intended to cut the **main beam** (i.e. as
|
|
59
|
+
:attr:`~compas_timber.connections.ButtJoint.butt_plane`).
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
main_beam
|
|
64
|
+
Main beam of the joint.
|
|
65
|
+
cross_beam
|
|
66
|
+
Cross beam of the joint.
|
|
67
|
+
plane
|
|
68
|
+
Cutting plane in world coordinates. Its normal must be perpendicular to the cross beam's
|
|
69
|
+
centerline axis.
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
if not dot_vectors(cross_beam.frame.xaxis, plane.normal) == 0:
|
|
73
|
+
raise ValueError("plane normal must be perpendicular to cross_beam centerline axis")
|
|
74
|
+
ref_side_dict = beam_ref_side_incidence(main_beam, cross_beam, ignore_ends=True)
|
|
75
|
+
ref_side_index = min(ref_side_dict, key=lambda k: ref_side_dict[k])
|
|
76
|
+
ref_side = cross_beam.ref_sides[ref_side_index]
|
|
77
|
+
angle, offset = decompose_plane_to_ref_side(ref_side, plane, plane_name="butt_plane", reference_name="cross_beam")
|
|
78
|
+
return cls(ref_side_index, angle, offset)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_back_plane(cls, main_beam: Beam, cross_beam: Beam, plane: Plane) -> CutPlaneSpec:
|
|
82
|
+
"""Encode `plane` relative to the back face of the main beam (the face opposite the cross beam).
|
|
83
|
+
|
|
84
|
+
Use this when the plane is intended to cut the **cross beam** from behind the main beam (i.e. as
|
|
85
|
+
:attr:`~compas_timber.connections.LButtJoint.back_plane`).
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
main_beam
|
|
90
|
+
Main beam of the joint.
|
|
91
|
+
cross_beam
|
|
92
|
+
Cross beam of the joint.
|
|
93
|
+
plane
|
|
94
|
+
Cutting plane in world coordinates. Its normal must be perpendicular to the main beam's
|
|
95
|
+
centerline axis.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
ref_side_dict = beam_ref_side_incidence(cross_beam, main_beam, ignore_ends=True)
|
|
99
|
+
facing_side_index = min(ref_side_dict, key=lambda k: ref_side_dict[k])
|
|
100
|
+
back_side_index = (facing_side_index + 2) % 4 # opposite face of main_beam
|
|
101
|
+
ref_side = main_beam.ref_sides[back_side_index]
|
|
102
|
+
angle, offset = decompose_plane_to_ref_side(ref_side, plane, plane_name="back_plane", reference_name="main_beam")
|
|
103
|
+
return cls(back_side_index, angle, offset)
|
|
104
|
+
|
|
105
|
+
|
|
23
106
|
class ButtJoint(Joint):
|
|
24
107
|
"""Represents an L-Butt type joint which joins two beam in their ends, trimming the main beam.
|
|
25
108
|
|
|
@@ -34,9 +117,10 @@ class ButtJoint(Joint):
|
|
|
34
117
|
cross_beam : :class:`~compas_timber.elements.Beam`
|
|
35
118
|
The cross beam to be joined.
|
|
36
119
|
mill_depth : float
|
|
37
|
-
The depth of the pocket to be milled in the cross beam. This will be ignored if `
|
|
38
|
-
|
|
39
|
-
|
|
120
|
+
The depth of the pocket to be milled in the cross beam. This will be ignored if `butt_plane_spec` is provided.
|
|
121
|
+
butt_plane_spec : :class:`~compas_timber.connections.JointCutPlane`, optional
|
|
122
|
+
Overrides the plane used to cut the main beam. Build with
|
|
123
|
+
:meth:`~compas_timber.connections.JointCutPlane.from_butt_plane`.
|
|
40
124
|
force_pocket : bool
|
|
41
125
|
If `True` applies a `:~compas_timber.fabrication.Pocket` feature instead of a `:~compas_timber.fabrication.Lap` on the cross beam. Default is `False`.
|
|
42
126
|
conical_tool : bool
|
|
@@ -52,16 +136,14 @@ class ButtJoint(Joint):
|
|
|
52
136
|
A list containing the main beam and the cross beam.
|
|
53
137
|
mill_depth : float
|
|
54
138
|
The depth of the pocket to be milled in the cross beam.
|
|
55
|
-
butt_plane : :class:`~compas.geometry.Plane`, optional
|
|
56
|
-
The plane used to cut the main beam. If not provided, the closest side of the cross beam will be used.
|
|
57
139
|
force_pocket : bool
|
|
58
140
|
If `True` applies a `:~compas_timber.fabrication.Pocket` feature instead of a `:~compas_timber.fabrication.Lap` on the cross beam. Default is `False`.
|
|
59
141
|
conical_tool : bool
|
|
60
142
|
If `True` it can apply smaller than 90 degrees angles to the TiltSide parameters of the `:~compas_timber.fabrication.Pocket` feature. Default is `False`.
|
|
61
|
-
features: list[BTLxProcessing]
|
|
143
|
+
features: list[:class:`~compas_timber.fabrication.BTLxProcessing`]
|
|
62
144
|
List of features to be applied to the cross beam and main beam.
|
|
63
145
|
cross_beam_ref_side_index : int
|
|
64
|
-
The index of the side of the cross beam relative to the main beam
|
|
146
|
+
The index of the side of the cross beam relative to the main beam.
|
|
65
147
|
main_beam_ref_side_index : int
|
|
66
148
|
The index of the side of the main beam relative to the cross beam.
|
|
67
149
|
|
|
@@ -73,7 +155,7 @@ class ButtJoint(Joint):
|
|
|
73
155
|
def __data__(self):
|
|
74
156
|
data = super(ButtJoint, self).__data__
|
|
75
157
|
data["mill_depth"] = self.mill_depth
|
|
76
|
-
data["
|
|
158
|
+
data["butt_plane_spec"] = self._butt_plane_spec
|
|
77
159
|
data["force_pocket"] = self.force_pocket
|
|
78
160
|
data["conical_tool"] = self.conical_tool
|
|
79
161
|
return data
|
|
@@ -83,17 +165,17 @@ class ButtJoint(Joint):
|
|
|
83
165
|
main_beam: Beam = None,
|
|
84
166
|
cross_beam: Beam = None,
|
|
85
167
|
mill_depth: Optional[float] = None,
|
|
86
|
-
|
|
168
|
+
butt_plane_spec: Optional[CutPlaneSpec] = None,
|
|
87
169
|
force_pocket: bool = False,
|
|
88
170
|
conical_tool: bool = False,
|
|
89
171
|
**kwargs,
|
|
90
172
|
):
|
|
91
173
|
super(ButtJoint, self).__init__(elements=(main_beam, cross_beam), **kwargs)
|
|
92
|
-
self.mill_depth = mill_depth
|
|
93
|
-
self.
|
|
94
|
-
self.
|
|
174
|
+
self.mill_depth: float = mill_depth or 0.0
|
|
175
|
+
self._butt_plane_spec: Optional[CutPlaneSpec] = butt_plane_spec
|
|
176
|
+
self.force_pocket: bool = force_pocket
|
|
177
|
+
self.conical_tool: bool = conical_tool
|
|
95
178
|
self.features = []
|
|
96
|
-
self._butt_plane = butt_plane
|
|
97
179
|
|
|
98
180
|
@property
|
|
99
181
|
def main_beam(self):
|
|
@@ -121,13 +203,24 @@ class ButtJoint(Joint):
|
|
|
121
203
|
|
|
122
204
|
@property
|
|
123
205
|
def butt_plane(self) -> Plane:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
206
|
+
"""The plane used to cut the main beam.
|
|
207
|
+
|
|
208
|
+
If a :class:`~compas_timber.connections.JointCutPlane` override is set, it is resolved against the cross beam.
|
|
209
|
+
Otherwise defaults to the cross beam's side closest to the main beam, offset outward by `mill_depth`.
|
|
210
|
+
"""
|
|
211
|
+
if self._butt_plane_spec is not None:
|
|
212
|
+
return self._butt_plane_spec.to_plane(self.cross_beam)
|
|
213
|
+
# default: the cross beam's closest side, facing the main beam, offset by mill_depth
|
|
214
|
+
ref_side = self.cross_beam.ref_sides[self.cross_beam_ref_side_index]
|
|
215
|
+
return plane_from_ref_side_angle_offset(ref_side, math.pi, self.mill_depth)
|
|
216
|
+
|
|
217
|
+
def _back_cutting_plane(self) -> Plane:
|
|
218
|
+
"""The plane used to extend/cut the cross beam when `modify_cross` is True.
|
|
219
|
+
|
|
220
|
+
Defaults to the side of the main beam opposite the one facing the cross beam. `LButtJoint` overrides this to
|
|
221
|
+
support a user-defined `back_plane`; this is not a general `ButtJoint` concept.
|
|
222
|
+
"""
|
|
223
|
+
return Plane.from_frame(self.main_beam.opp_side(self.main_beam_ref_side_index))
|
|
131
224
|
|
|
132
225
|
def add_extensions(self):
|
|
133
226
|
"""Calculates and adds the necessary extensions to the beams.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import List
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
from compas.geometry import Plane
|
|
9
|
+
from compas.tolerance import TOL
|
|
10
|
+
|
|
11
|
+
from compas_timber.errors import BeamJoiningError
|
|
12
|
+
from compas_timber.fabrication import SimpleScarf
|
|
13
|
+
|
|
14
|
+
from .joint import Joint
|
|
15
|
+
from .solver import JointTopology
|
|
16
|
+
from .utilities import beam_ref_side_incidence_with_vector
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from compas.geometry import Frame
|
|
20
|
+
from compas.geometry import Point
|
|
21
|
+
|
|
22
|
+
from compas_timber.elements import Beam
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ISimpleScarf(Joint):
|
|
26
|
+
"""Represents a Simple Scarf joint for two parallel beams (Topology I).
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
main_beam : :class:`~compas_timber.elements.Beam`, optional
|
|
31
|
+
The first beam of the joint.
|
|
32
|
+
cross_beam : :class:`~compas_timber.elements.Beam`, optional
|
|
33
|
+
The second beam of the joint, parallel to the main beam.
|
|
34
|
+
length : float, optional
|
|
35
|
+
The length of the scarf overlap along the beam axis. If not provided,
|
|
36
|
+
defaults to ``DEFAULT_LENGTH_TO_HEIGHT_RATIO`` times the beam height.
|
|
37
|
+
depth_ref_side : float, optional
|
|
38
|
+
The cut depth on the reference side of the beam. If not provided,
|
|
39
|
+
defaults to ``DEFAULT_DEPTH_TO_HEIGHT_RATIO`` times the beam height.
|
|
40
|
+
depth_opp_side : float, optional
|
|
41
|
+
The cut depth on the opposite side of the beam. If not provided,
|
|
42
|
+
defaults to ``DEFAULT_DEPTH_TO_HEIGHT_RATIO`` times the beam height.
|
|
43
|
+
num_drill_hole : int, optional
|
|
44
|
+
Number of drill holes through the joint (0, 1, or 2). Defaults to 0.
|
|
45
|
+
drill_hole_diam : float, optional
|
|
46
|
+
Diameter of the drill holes in mm. Defaults to 20.0.
|
|
47
|
+
ref_side_index : int, optional
|
|
48
|
+
Zero-based index of the reference side (RS1–RS6 → 0–5). Defaults to 0.
|
|
49
|
+
|
|
50
|
+
Attributes
|
|
51
|
+
----------
|
|
52
|
+
main_beam : :class:`~compas_timber.elements.Beam`
|
|
53
|
+
The first beam of the joint (alias for ``element_a``).
|
|
54
|
+
cross_beam : :class:`~compas_timber.elements.Beam`
|
|
55
|
+
The second beam of the joint (alias for ``element_b``).
|
|
56
|
+
main_beam_ref_side_index : int
|
|
57
|
+
Reference side index for the main beam. Equal to ``ref_side_index``.
|
|
58
|
+
cross_beam_ref_side_index : int
|
|
59
|
+
Reference side index for the cross beam, resolved automatically from
|
|
60
|
+
the face most aligned with the main beam's reference side normal.
|
|
61
|
+
origin : :class:`~compas.geometry.Point`
|
|
62
|
+
The origin point of the joint (alias for ``location``).
|
|
63
|
+
length : float
|
|
64
|
+
The length of the scarf overlap.
|
|
65
|
+
depth_ref_side : float
|
|
66
|
+
The cut depth on the reference side.
|
|
67
|
+
depth_opp_side : float
|
|
68
|
+
The cut depth on the opposite side.
|
|
69
|
+
num_drill_hole : int
|
|
70
|
+
Number of drill holes (0, 1, or 2).
|
|
71
|
+
drill_hole_diam : float
|
|
72
|
+
Diameter of the drill holes in mm.
|
|
73
|
+
ref_side_index : int
|
|
74
|
+
Zero-based index of the reference side used for the main beam.
|
|
75
|
+
features : list
|
|
76
|
+
List of :class:`~compas_timber.fabrication.SimpleScarf` features
|
|
77
|
+
added to the beams by this joint.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
SUPPORTED_TOPOLOGY = JointTopology.TOPO_I
|
|
82
|
+
|
|
83
|
+
# Scarf length defaults to 3x the beam height — a common rule of thumb
|
|
84
|
+
# that provides enough glue/contact area for structural performance.
|
|
85
|
+
DEFAULT_LENGTH_TO_HEIGHT_RATIO = 3
|
|
86
|
+
# Depth on each face defaults to 1/4 of the beam height,
|
|
87
|
+
# leaving the middle half as the straight (uncut) section.
|
|
88
|
+
DEFAULT_DEPTH_TO_HEIGHT_RATIO = 0.25
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def __data__(self):
|
|
92
|
+
data = super(ISimpleScarf, self).__data__
|
|
93
|
+
data["main_beam_guid"] = self.main_beam_guid
|
|
94
|
+
data["cross_beam_guid"] = self.cross_beam_guid
|
|
95
|
+
data["length"] = self.length
|
|
96
|
+
data["depth_ref_side"] = self.depth_ref_side
|
|
97
|
+
data["depth_opp_side"] = self.depth_opp_side
|
|
98
|
+
data["num_drill_hole"] = self.num_drill_hole
|
|
99
|
+
data["drill_hole_diam"] = self.drill_hole_diam
|
|
100
|
+
data["ref_side_index"] = self.ref_side_index
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
main_beam: Optional[Beam] = None,
|
|
106
|
+
cross_beam: Optional[Beam] = None,
|
|
107
|
+
length: Optional[float] = None,
|
|
108
|
+
depth_ref_side: Optional[float] = None,
|
|
109
|
+
depth_opp_side: Optional[float] = None,
|
|
110
|
+
num_drill_hole: int = 0,
|
|
111
|
+
drill_hole_diam: float = 20.0,
|
|
112
|
+
ref_side_index: int = 0,
|
|
113
|
+
**kwargs,
|
|
114
|
+
):
|
|
115
|
+
super(ISimpleScarf, self).__init__(elements=(main_beam, cross_beam), **kwargs)
|
|
116
|
+
self.main_beam_guid = kwargs.get("main_beam_guid") or (str(main_beam.guid) if main_beam else None)
|
|
117
|
+
self.cross_beam_guid = kwargs.get("cross_beam_guid") or (str(cross_beam.guid) if cross_beam else None)
|
|
118
|
+
|
|
119
|
+
self.length = length
|
|
120
|
+
self.depth_ref_side = depth_ref_side
|
|
121
|
+
self.depth_opp_side = depth_opp_side
|
|
122
|
+
self.num_drill_hole = num_drill_hole
|
|
123
|
+
self.drill_hole_diam = drill_hole_diam
|
|
124
|
+
self.ref_side_index = ref_side_index
|
|
125
|
+
|
|
126
|
+
self.features = []
|
|
127
|
+
|
|
128
|
+
if self.main_beam and self.cross_beam:
|
|
129
|
+
self._set_unset_attributes()
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def main_beam(self) -> Beam:
|
|
133
|
+
return self.element_a
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def cross_beam(self) -> Beam:
|
|
137
|
+
return self.element_b
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def main_beam_ref_side_index(self) -> int:
|
|
141
|
+
return self.ref_side_index
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def cross_beam_ref_side_index(self) -> int:
|
|
145
|
+
ref_side_dict = beam_ref_side_incidence_with_vector(self.cross_beam, self.main_beam.ref_sides[self.main_beam_ref_side_index].normal, ignore_ends=True)
|
|
146
|
+
return max(ref_side_dict, key=ref_side_dict.get)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def origin(self) -> Point:
|
|
150
|
+
return self.location
|
|
151
|
+
|
|
152
|
+
def _get_beam_side(self, beam: Beam) -> str:
|
|
153
|
+
"""Finds whether the scarf end of the beam is its start or end.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
beam : :class:`~compas_timber.elements.Beam`
|
|
158
|
+
The beam to evaluate.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
str
|
|
163
|
+
``"start"`` or ``"end"`` depending on which endpoint of the beam
|
|
164
|
+
is closest to the joint origin.
|
|
165
|
+
"""
|
|
166
|
+
side, _ = beam.endpoint_closest_to_point(self.origin)
|
|
167
|
+
return side
|
|
168
|
+
|
|
169
|
+
def extension_plane(self, beam: Beam) -> Tuple[int, Frame]:
|
|
170
|
+
"""Returns the end-face index and frame used to compute the blank extension for a beam.
|
|
171
|
+
|
|
172
|
+
The end face is RS5 (index 4) when the scarf sits at the beam start,
|
|
173
|
+
or RS6 (index 5) when it sits at the beam end.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
beam : :class:`~compas_timber.elements.Beam`
|
|
178
|
+
The beam for which to determine the extension plane.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
tuple(int, :class:`~compas.geometry.Frame`)
|
|
183
|
+
A tuple of ``(ext_side_index, frame)`` where ``ext_side_index`` is
|
|
184
|
+
the zero-based index of the end face (4 or 5) and ``frame`` is the
|
|
185
|
+
corresponding reference-side frame.
|
|
186
|
+
"""
|
|
187
|
+
ext_side_index = 4 if self._get_beam_side(beam) == "start" else 5
|
|
188
|
+
return ext_side_index, beam.ref_sides[ext_side_index]
|
|
189
|
+
|
|
190
|
+
def add_extensions(self) -> None:
|
|
191
|
+
"""Calculates and adds the required blank extensions to both beams."""
|
|
192
|
+
assert self.main_beam and self.cross_beam
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
_, main_extension_frame = self.extension_plane(self.main_beam)
|
|
196
|
+
main_extension_frame.translate(main_extension_frame.normal * self.length / 2)
|
|
197
|
+
|
|
198
|
+
_, cross_extension_frame = self.extension_plane(self.cross_beam)
|
|
199
|
+
cross_extension_frame.translate(cross_extension_frame.normal * self.length / 2)
|
|
200
|
+
|
|
201
|
+
start_a, end_a = self.main_beam.extension_to_plane(Plane.from_frame(main_extension_frame))
|
|
202
|
+
start_b, end_b = self.cross_beam.extension_to_plane(Plane.from_frame(cross_extension_frame))
|
|
203
|
+
|
|
204
|
+
except AttributeError as ae:
|
|
205
|
+
raise BeamJoiningError(self.elements, self, debug_info=str(ae), debug_geometries=[main_extension_frame])
|
|
206
|
+
except Exception as ex:
|
|
207
|
+
raise BeamJoiningError(self.elements, self, debug_info=str(ex))
|
|
208
|
+
|
|
209
|
+
self.main_beam.add_blank_extension(start_a, end_a, self.guid)
|
|
210
|
+
self.cross_beam.add_blank_extension(start_b, end_b, self.guid)
|
|
211
|
+
|
|
212
|
+
def add_features(self) -> None:
|
|
213
|
+
"""Generates and assigns the SimpleScarf fabrication features.
|
|
214
|
+
|
|
215
|
+
Raises
|
|
216
|
+
------
|
|
217
|
+
AssertionError
|
|
218
|
+
If either beam is not set.
|
|
219
|
+
"""
|
|
220
|
+
assert self.main_beam and self.cross_beam
|
|
221
|
+
|
|
222
|
+
if self.features:
|
|
223
|
+
self.main_beam.remove_features(self.features)
|
|
224
|
+
self.cross_beam.remove_features(self.features)
|
|
225
|
+
self.features = []
|
|
226
|
+
|
|
227
|
+
main_feature = SimpleScarf.from_beam_and_side(
|
|
228
|
+
self.main_beam,
|
|
229
|
+
side=self._get_beam_side(self.main_beam),
|
|
230
|
+
length=self.length,
|
|
231
|
+
depth_ref_side=self.depth_ref_side,
|
|
232
|
+
depth_opp_side=self.depth_opp_side,
|
|
233
|
+
num_drill_hole=self.num_drill_hole,
|
|
234
|
+
drill_hole_diam=self.drill_hole_diam,
|
|
235
|
+
ref_side_index=self.ref_side_index,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
cross_feature = SimpleScarf.from_beam_and_side(
|
|
239
|
+
self.cross_beam,
|
|
240
|
+
side=self._get_beam_side(self.cross_beam),
|
|
241
|
+
length=self.length,
|
|
242
|
+
depth_ref_side=self.depth_opp_side,
|
|
243
|
+
depth_opp_side=self.depth_ref_side,
|
|
244
|
+
num_drill_hole=self.num_drill_hole,
|
|
245
|
+
drill_hole_diam=self.drill_hole_diam,
|
|
246
|
+
ref_side_index=self.cross_beam_ref_side_index,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.main_beam.add_features(main_feature)
|
|
250
|
+
self.cross_beam.add_features(cross_feature)
|
|
251
|
+
self.features.extend([main_feature, cross_feature])
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def check_elements_compatibility(cls, elements: List[Beam], raise_error: bool = False) -> bool:
|
|
255
|
+
"""Checks if the two beams are parallel, as required for an ISimpleScarf joint.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
elements : list(:class:`~compas_timber.elements.Beam`)
|
|
260
|
+
The two beams to check.
|
|
261
|
+
raise_error : bool, optional
|
|
262
|
+
If ``True``, raise a :class:`~compas_timber.errors.BeamJoiningError`
|
|
263
|
+
instead of returning ``False``. Defaults to ``False``.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
bool
|
|
268
|
+
``True`` if the beams are parallel, ``False`` otherwise
|
|
269
|
+
(when ``raise_error`` is ``False``).
|
|
270
|
+
|
|
271
|
+
Raises
|
|
272
|
+
------
|
|
273
|
+
:class:`~compas_timber.errors.BeamJoiningError`
|
|
274
|
+
If the beams are not parallel and ``raise_error`` is ``True``.
|
|
275
|
+
"""
|
|
276
|
+
dot = abs(elements[0].centerline.direction.dot(elements[1].centerline.direction))
|
|
277
|
+
if not TOL.is_close(dot, 1):
|
|
278
|
+
if not raise_error:
|
|
279
|
+
return False
|
|
280
|
+
raise BeamJoiningError(elements, cls, debug_info="The two beams are not parallel to create a Simple Scarf joint.")
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
def _set_unset_attributes(self) -> None:
|
|
284
|
+
"""Sets attributes that were not provided at initialization based on the geometry of the beams."""
|
|
285
|
+
width, height = self.main_beam.get_dimensions_relative_to_side(self.ref_side_index)
|
|
286
|
+
|
|
287
|
+
if self.length is None:
|
|
288
|
+
self.length = height * self.DEFAULT_LENGTH_TO_HEIGHT_RATIO
|
|
289
|
+
|
|
290
|
+
if self.depth_ref_side is None:
|
|
291
|
+
self.depth_ref_side = height * self.DEFAULT_DEPTH_TO_HEIGHT_RATIO
|
|
292
|
+
|
|
293
|
+
if self.depth_opp_side is None:
|
|
294
|
+
self.depth_opp_side = height * self.DEFAULT_DEPTH_TO_HEIGHT_RATIO
|