Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Moved the element-type dispatch used by `compute_topologies()` out of `connections/solver.py` into a new `candidate_dispatch.py` module to avoid a circular import between `solver.py` and the modules it dispatches to (`joint_candidate.py`, `compas_timber.elements`).
* Changed connection-candidate handlers in `candidate_dispatch.py` to register the element-type pair they support via a `@_register(TypeA, TypeB)` decorator next to their definition, instead of a separate mapping.
* Fixed `PlateMiterJoint` bug where parallel plates failed to join.
* Fixed `Pocket`, `Lap`, and `BTLxPart.shape_strings` calling the old `compas.geometry.brep` `BrepFace` API (`.nurbssurface`, `.frame_at`), which no longer exists on `compas_brep`'s `BrepFace`; `face.surface` now returns a `Plane` directly for planar faces, with a fallback to `surface.frame_at()` only for genuinely curved faces. Also fixed `face.surface` not accounting for `face.is_reversed` (opposite faces of a box reported identical normals instead of opposite ones), and `Pocket`/`Lap`'s `_get_optimal_ref_side_index` unpacking `edge.curve` (already a `Line`) as if it needed `Line(*curve.points)`.
* `Pocket.apply()` now raises a clear `FeatureApplicationError` when `start_depth` is negative (the pocket volume lies entirely outside the element's material on the ref_side's outward side) instead of letting a corrupted or erased geometry reach the boolean subtraction.
* Fixed bug where the `TimberModel.connect_adjacent_beams/plates/panels()` methods would not clear all existing joint candidates, including for other element types.

### Removed
Expand Down
9 changes: 8 additions & 1 deletion src/compas_timber/fabrication/btlx.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,14 @@ def shape_strings(self):
scaled_geometry = self.element.geometry.scaled(self._scale_factor)
for face in scaled_geometry.faces:
pts = []
frame = face.surface.frame_at(0.5, 0.5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! I think these should be fixed upstream. could you check if gramaziokohler/compas_brep#8
solves these issues?

it should be as easy as calling face.from_at() which should now get you the properly oriented frame.

surface = face.surface
# planar faces (the common case) return a Plane, which has no frame_at (parameter-independent
# anyway); only genuinely curved faces (NurbsSurface, etc.) need parametric evaluation.
frame = Frame.from_plane(surface) if isinstance(surface, Plane) else surface.frame_at(0.5, 0.5)
if face.is_reversed:
# `is_reversed` faces store the surface with an inverted normal relative to the actual
# face orientation; flip yaxis (not xaxis) so zaxis (normal) flips while xaxis is preserved.
frame = Frame(frame.point, frame.xaxis, -frame.yaxis)
edges = face.boundary.edges[1:]
pts = [face.boundary.edges[0].start_vertex.point, face.boundary.edges[0].end_vertex.point]
overflow = len(edges)
Expand Down
15 changes: 10 additions & 5 deletions src/compas_timber/fabrication/lap.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,15 @@ def from_volume_and_beam(cls, volume, beam, machining_limits=None, ref_side_inde
volume = volume.to_mesh()
planes = [volume.face_plane(i) for i in range(volume.number_of_faces())]
elif isinstance(volume, Brep):
volume_surfaces = [face.nurbssurface for face in volume.faces]
volume_frames = [surface.frame_at(0, 0) for surface in volume_surfaces]
planes = [Plane.from_frame(frame) for frame in volume_frames]
# a 6-face lap volume is always a box, so every face is planar: `face.surface` already returns
# a Plane directly, no need to go through a NurbsSurface/frame_at (which planar faces don't have).
# `is_reversed` faces store the surface with an inverted normal relative to the actual face orientation.
planes = []
for face in volume.faces:
plane = face.surface
if face.is_reversed:
plane.normal = -plane.normal
planes.append(plane)

else:
raise ValueError("Volume must be either a Mesh, Brep, or Polyhedron.")
Expand Down Expand Up @@ -572,8 +578,7 @@ def _get_optimal_ref_side_index(element, volume):
# get the optimal reference side index based on the volume. The optimal reference side is the one with the most intersections with the volume edges.
# get the volume edges
if isinstance(volume, Brep):
volume_curve = [edge.curve for edge in volume.edges]
volume_edges = [Line(*curve.points) for curve in volume_curve]
volume_edges = [edge.curve for edge in volume.edges]
else:
volume_edges = [volume.edge_line(edge) for edge in volume.edges()]

Expand Down
31 changes: 26 additions & 5 deletions src/compas_timber/fabrication/pocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from compas.datastructures import Mesh
from compas.geometry import Frame
from compas.geometry import Line
from compas.geometry import Plane
from compas.geometry import Point
from compas.geometry import Polyhedron
Expand Down Expand Up @@ -342,8 +341,15 @@ def from_volume_and_element(
volume = volume.to_mesh()
planes = [volume.face_plane(i) for i in range(volume.number_of_faces())]
elif isinstance(volume, Brep):
volume_frames = [face.frame_at(0,0) for face in volume.faces]
planes = [Plane.from_frame(frame) for frame in volume_frames]
# a 6-face pocket volume is always a box, so every face is planar: `face.surface` already returns
# a Plane directly, no need to go through a NurbsSurface/frame_at (which planar faces don't have).
# `is_reversed` faces store the surface with an inverted normal relative to the actual face orientation.
planes = []
for face in volume.faces:
plane = face.surface
if face.is_reversed:
plane.normal = -plane.normal
planes.append(plane)
else:
raise ValueError("Volume must be either a Mesh, Brep, or Polyhedron.")

Expand Down Expand Up @@ -452,8 +458,7 @@ def _get_optimal_ref_side_index(element, volume) -> int:
# get the optimal reference side index based on the volume. The optimal reference side is the one with the most intersections with the volume edges.
# get the volume edges
if isinstance(volume, Brep):
volume_curve = [edge.curve for edge in volume.edges]
volume_edges = [Line(*curve.points) for curve in volume_curve]
volume_edges = [edge.curve for edge in volume.edges]
else:
volume_edges = [volume.edge_line(edge) for edge in volume.edges()]

Expand Down Expand Up @@ -546,6 +551,22 @@ def apply(self, geometry: Brep, element: TimberElement) -> Brep:
The resulting geometry after processing

"""
# a negative start_depth means the "bottom" plane of the pocket volume sits outside the
# element's material on the ref_side's outward side (the volume passed to
# from_volume_and_element never actually reached the material, e.g. a plate's shank
# embedded entirely in a beam it doesn't share a face with). face_limited_top=False then
# pins the "top" plane to the element's own ref_side, so the resulting hexahedron spans
# from the ref_side surface *outward* rather than into the material - subtracting it can
# corrupt (or entirely erase) the element's geometry instead of being a no-op. There's
# nothing to cut in that case, so skip it rather than let it reach the boolean below.
if self.start_depth < -TOL.absolute:
raise FeatureApplicationError(
None,
geometry.transformed(element.modeltransformation),
"Pocket's start_depth ({:.4f}) is negative: the pocket volume lies entirely outside "
Comment on lines +562 to +566
"{}'s material on the ref_side's outward side, so there is nothing to cut.".format(self.start_depth, element),
)

# get the pocket volume as a polyhedron
polyhedron_volume = self.volume_from_params_and_element(element)
polyhedron_volume.transform(element.transformation_to_local())
Expand Down
Loading