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.
Files changed (91) hide show
  1. {compas_timber-2.1.2/src/compas_timber.egg-info → compas_timber-2.2.0}/PKG-INFO +1 -1
  2. {compas_timber-2.1.2 → compas_timber-2.2.0}/pyproject.toml +1 -1
  3. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/__init__.py +1 -1
  4. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/__init__.py +6 -0
  5. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/butt_joint.py +113 -20
  6. compas_timber-2.2.0/src/compas_timber/connections/i_simple_scarf.py +294 -0
  7. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_butt.py +36 -34
  8. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_french_ridge_lap.py +2 -2
  9. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_miter.py +111 -14
  10. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_butt.py +4 -4
  11. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/utilities.py +124 -0
  12. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/x_notch.py +5 -5
  13. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/beam.py +6 -6
  14. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/panel.py +18 -8
  15. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/plate.py +18 -8
  16. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/plate_geometry.py +30 -4
  17. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/__init__.py +3 -1
  18. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/lap.py +9 -7
  19. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/pocket.py +8 -6
  20. compas_timber-2.2.0/src/compas_timber/fabrication/simple_scarf.py +435 -0
  21. {compas_timber-2.1.2 → compas_timber-2.2.0/src/compas_timber.egg-info}/PKG-INFO +1 -1
  22. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/SOURCES.txt +2 -0
  23. {compas_timber-2.1.2 → compas_timber-2.2.0}/LICENSE +0 -0
  24. {compas_timber-2.1.2 → compas_timber-2.2.0}/README.md +0 -0
  25. {compas_timber-2.1.2 → compas_timber-2.2.0}/setup.cfg +0 -0
  26. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/base.py +0 -0
  27. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/btlx/__init__.py +0 -0
  28. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/btlx/reader.py +0 -0
  29. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/ball_node.py +0 -0
  30. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/cluster.py +0 -0
  31. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/joint.py +0 -0
  32. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/joint_candidate.py +0 -0
  33. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_lap.py +0 -0
  34. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/l_tenon_mortise.py +0 -0
  35. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/lap_joint.py +0 -0
  36. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/mortise_tenon.py +0 -0
  37. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/oligina.py +0 -0
  38. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/panel_butt_joint.py +0 -0
  39. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/panel_joint.py +0 -0
  40. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/panel_miter_joint.py +0 -0
  41. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/plate_butt_joint.py +0 -0
  42. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/plate_joint.py +0 -0
  43. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/plate_miter_joint.py +0 -0
  44. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/solver.py +0 -0
  45. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_birdsmouth.py +0 -0
  46. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_dovetail.py +0 -0
  47. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_lap.py +0 -0
  48. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_step_joint.py +0 -0
  49. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/t_tenon_mortise.py +0 -0
  50. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/x_lap.py +0 -0
  51. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/connections/y_butt.py +0 -0
  52. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/__init__.py +0 -0
  53. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/fastener.py +0 -0
  54. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/fasteners/__init__.py +0 -0
  55. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/fasteners/ball_node_fastener.py +0 -0
  56. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/fasteners/plate_fastener.py +0 -0
  57. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/elements/features.py +0 -0
  58. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/errors.py +0 -0
  59. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/btlx.py +0 -0
  60. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/double_cut.py +0 -0
  61. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/dovetail_mortise.py +0 -0
  62. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/dovetail_tenon.py +0 -0
  63. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/drilling.py +0 -0
  64. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/free_contour.py +0 -0
  65. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/french_ridge_lap.py +0 -0
  66. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/jack_cut.py +0 -0
  67. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/longitudinal_cut.py +0 -0
  68. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/mortise.py +0 -0
  69. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/slot.py +0 -0
  70. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/step_joint.py +0 -0
  71. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/step_joint_notch.py +0 -0
  72. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/tenon.py +0 -0
  73. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/fabrication/text.py +0 -0
  74. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/geometry.py +0 -0
  75. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/model.py +0 -0
  76. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/panel_features/__init__.py +0 -0
  77. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/panel_features/opening.py +0 -0
  78. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/panel_features/panel_connection_interface.py +0 -0
  79. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/panel_features/panel_features.py +0 -0
  80. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/planning/__init__.py +0 -0
  81. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/planning/nesting.py +0 -0
  82. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/planning/sequencer.py +0 -0
  83. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/rhino/__init__.py +0 -0
  84. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/rhino/install.py +0 -0
  85. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/structural.py +0 -0
  86. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/utils/__init__.py +0 -0
  87. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber/utils/r_tree.py +0 -0
  88. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/dependency_links.txt +0 -0
  89. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/not-zip-safe +0 -0
  90. {compas_timber-2.1.2 → compas_timber-2.2.0}/src/compas_timber.egg-info/requires.txt +0 -0
  91. {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.1.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.1.2"
99
+ current_version = "2.2.0"
100
100
  message = "Bump version to {new_version}"
101
101
  commit = true
102
102
  tag = true
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import compas
3
3
 
4
- __version__ = "2.1.2"
4
+ __version__ = "2.2.0"
5
5
 
6
6
  HERE = os.path.dirname(__file__)
7
7
  HOME = os.path.abspath(os.path.join(HERE, "..", ".."))
@@ -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 `butt_plane` is provided.
38
- butt_plane : :class:`~compas.geometry.Plane`, optional
39
- The plane used to cut the main beam. If not provided, the closest side of the cross beam will be used.
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["butt_plane"] = self.butt_plane
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
- butt_plane: Optional[Plane] = None,
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.force_pocket = force_pocket
94
- self.conical_tool = conical_tool
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
- if self._butt_plane is None:
125
- cutting_plane = self.cross_beam.ref_sides[self.cross_beam_ref_side_index]
126
- cutting_plane.xaxis = -cutting_plane.xaxis
127
- if self.mill_depth:
128
- cutting_plane.translate(cutting_plane.normal * self.mill_depth)
129
- self._butt_plane = Plane.from_frame(cutting_plane)
130
- return self._butt_plane
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