From 0d4998fd824a40123846e31532d6fa3be42feb75 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Mon, 17 Jun 2024 14:40:59 +0200 Subject: [PATCH] Modifications + dimension offset (#26) * removed plugin manager debug setting * introduced wrapper for cadwork dimensions * added ElementDelta helper * added alternative ctors to Dimension * changelog * simplified dimension drawing * updated dependency version * removed unnecessary docstring * shorter __str__ * make use of line_offset when drawing linear dimensions --- CHANGELOG.md | 3 + requirements.txt | 2 +- src/compas_cadwork/datamodel/__init__.py | 2 + src/compas_cadwork/datamodel/dimension.py | 145 ++++++++++++++++++ src/compas_cadwork/datamodel/element.py | 11 +- src/compas_cadwork/scene/__init__.py | 4 - src/compas_cadwork/scene/instructionobject.py | 14 +- src/compas_cadwork/utilities/__init__.py | 13 +- src/compas_cadwork/utilities/dimensions.py | 1 + src/compas_cadwork/utilities/events.py | 35 ++++- 10 files changed, 205 insertions(+), 25 deletions(-) create mode 100644 src/compas_cadwork/datamodel/dimension.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e86b5..1a853e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added new types `Dimension` and `AnchorPoint` in `compas_cadwork.datamodel`. +* Added `from_element` and `from_id` to `Dimension`. + ### Changed * Exporting now with ifc4 and bimwood property set (true). diff --git a/requirements.txt b/requirements.txt index 9c5accf..f1ef68b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ compas>=2.0, <3.0 -cwapi3d>=30.319.1 +cwapi3d>=30.498.0 diff --git a/src/compas_cadwork/datamodel/__init__.py b/src/compas_cadwork/datamodel/__init__.py index d853cce..6e875d8 100644 --- a/src/compas_cadwork/datamodel/__init__.py +++ b/src/compas_cadwork/datamodel/__init__.py @@ -3,6 +3,7 @@ from .element import ElementGroup from .element import ElementGroupingType from .element import ATTR_INSTRUCTION_ID +from .dimension import Dimension __all__ = [ @@ -11,4 +12,5 @@ "ElementGroup", "ElementGroupingType", "ATTR_INSTRUCTION_ID", + "Dimension", ] diff --git a/src/compas_cadwork/datamodel/dimension.py b/src/compas_cadwork/datamodel/dimension.py new file mode 100644 index 0000000..9d34ea8 --- /dev/null +++ b/src/compas_cadwork/datamodel/dimension.py @@ -0,0 +1,145 @@ +from __future__ import annotations +from dataclasses import dataclass + +from dimension_controller import get_dimension_points +from dimension_controller import get_plane_normal +from dimension_controller import get_plane_xl +from dimension_controller import get_segment_distance +from dimension_controller import get_segment_direction + +from compas.geometry import Frame +from compas.geometry import Point +from compas.geometry import Vector +from compas.tolerance import Tolerance + +from compas_cadwork.conversions import point_to_compas +from compas_cadwork.conversions import vector_to_compas +from .element import Element +from .element import ElementType + + +TOL = Tolerance(unit="MM", absolute=1e-3, relative=1e-3) + + +@dataclass +class AnchorPoint: + """Anchor point of a cadwork measurement. There may me 2 or more anchor points in a measurement. + + Attributes + ---------- + location : Point + The location of the anchor point in 3d space. + distance : float + The distance of the anchor point from the measurement line. + direction : Vector + The direction of the anchor point from the measurement line. + + """ + + location: Point + distance: float + direction: Vector + + def __eq__(self, other: AnchorPoint) -> bool: + if not isinstance(other, AnchorPoint): + return False + + if not TOL.is_allclose([*self.location], [*other.location]): + return False + + if not TOL.is_allclose([*self.direction], [*other.direction]): + return False + + return TOL.is_close(self.distance, other.distance) + + +class Dimension(Element): + """Represents a cadwork dimension""" + + def __init__(self, id): + super().__init__(id, ElementType.DIMENSION) + self._frame = None + # not lazy-instantiating this so that it can be used to compare the modified instances of the same dimension + # otherwise, the anchors values that are compared depend on the time `anchors` was first accessed + self.anchors = self._init_anchors() + + def __str__(self) -> str: + return f"Dimension id:{self.id} length:{self.length:.0f} anchors:{len(self.anchors)}" + + def __hash__(self): + return hash(self.cadwork_guid) + + def __eq__(self, other: Dimension): + if not isinstance(other, Dimension): + return False + + if self.cadwork_guid != other.cadwork_guid: + return False + + for point_self, point_other in zip(self.anchors, other.anchors): + if point_self != point_other: + return False + return True + + @property + def frame(self): + if not self._frame: + zaxis = -self.text_normal + xaxis = vector_to_compas(get_plane_xl(self.id)) + yaxis = xaxis.cross(zaxis).unitized() + self._frame = Frame(self.anchors[0].location, xaxis, yaxis) + return self._frame + + @property + def text_normal(self): + return vector_to_compas(get_plane_normal(self.id)) + + @property + def length(self): + start: Point = self.anchors[0].location + end: Point = self.anchors[-1].location + return start.distance_to_point(end) + + def _init_anchors(self): + anchors = [] + for index, point in enumerate(get_dimension_points(self.id)): + distance = get_segment_distance(self.id, index) + direction = get_segment_direction(self.id, index) + anchors.append(AnchorPoint(point_to_compas(point), distance, vector_to_compas(direction))) + return tuple(anchors) + + @classmethod + def from_id(cls, element_id: int) -> Dimension: + """Creates a dimension object from an element id. + + This is an override of :method:`Element.from_id`. + + Parameters + ---------- + element_id : int + The id of the element to create the dimension from. + + Returns + ------- + :class:`Dimension` + The dimension object created from the element id. + + """ + return cls(id=element_id) + + @classmethod + def from_element(cls, element: Element) -> Dimension: + """Creates a dimension object from an element. + + Parameters + ---------- + element : :class:`Element` + The element to create the dimension from. + + Returns + ------- + :class:`Dimension` + The dimension object created from the element. + + """ + return cls(id=element.id) diff --git a/src/compas_cadwork/datamodel/element.py b/src/compas_cadwork/datamodel/element.py index 6dad838..8bdcf08 100644 --- a/src/compas_cadwork/datamodel/element.py +++ b/src/compas_cadwork/datamodel/element.py @@ -43,12 +43,6 @@ ATTR_INSTRUCTION_ID = 666 -class StrEnum(str, Enum): - """Why do *I* have to do this?""" - - pass - - class ElementGroupingType(IntEnum): """CADWork Element Grouping Type""" @@ -60,7 +54,7 @@ def to_cadwork(self): return cadwork.element_grouping_type(self.value) -class ElementType(StrEnum): +class ElementType(str, Enum): """CADWork Element type""" BEAM = auto() # Stab @@ -70,6 +64,7 @@ class ElementType(StrEnum): LINE = auto() INSTALLATION_ROUND = auto() INSTALLATION_STRAIGHT = auto() + DIMENSION = auto() OTHER = auto() @@ -82,10 +77,12 @@ class ElementType(StrEnum): "Installation rechteckig": ElementType.INSTALLATION_STRAIGHT, "Fläche": ElementType.SURFACE, "Installation rund": ElementType.INSTALLATION_ROUND, + "Measurement": ElementType.DIMENSION, }, "en": { "Beam": ElementType.BEAM, "Plate": ElementType.PLATE, + "Bemassung": ElementType.DIMENSION, }, } diff --git a/src/compas_cadwork/scene/__init__.py b/src/compas_cadwork/scene/__init__.py index deb6c05..b98aef3 100644 --- a/src/compas_cadwork/scene/__init__.py +++ b/src/compas_cadwork/scene/__init__.py @@ -1,5 +1,4 @@ from compas.plugins import plugin -from compas.plugins import PluginManager from compas.scene import register from compas_monosashi.sequencer import Text3d @@ -21,9 +20,6 @@ CONTEXT = "cadwork" -# TODO: remove -PluginManager.DEBUG = True - @plugin(category="drawing-utils", requires=[CONTEXT]) def clear(*args, **kwargs): diff --git a/src/compas_cadwork/scene/instructionobject.py b/src/compas_cadwork/scene/instructionobject.py index 8947ae0..0fc1c55 100644 --- a/src/compas_cadwork/scene/instructionobject.py +++ b/src/compas_cadwork/scene/instructionobject.py @@ -1,5 +1,4 @@ import cadwork -from compas.geometry import Vector from compas_monosashi.sequencer import LinearDimension from compas_monosashi.sequencer import Model3d from compas_monosashi.sequencer import Text3d @@ -127,18 +126,13 @@ def draw(self, *args, **kwargs): cadwork element ID of the added dimension. """ - - direction = Vector.from_start_end( - self.linear_dimension.start, self.linear_dimension.end - ).unitized() # why is this even needed? - text_plane_normal = self.linear_dimension.location.normal * -1.0 - text_plane_origin = self.linear_dimension.location.point.copy() - # text_plane_origin.z += self.linear_dimension.offset + inst_frame = self.linear_dimension.location + distance_vector = inst_frame.point + self.linear_dimension.line_offset element_id = create_dimension( - vector_to_cadwork(direction), + vector_to_cadwork(inst_frame.xaxis), vector_to_cadwork(text_plane_normal), - point_to_cadwork(text_plane_origin), + vector_to_cadwork(distance_vector), [point_to_cadwork(point) for point in self.linear_dimension.points], ) element = self.add_element(element_id) diff --git a/src/compas_cadwork/utilities/__init__.py b/src/compas_cadwork/utilities/__init__.py index db12ddf..e50e2e0 100644 --- a/src/compas_cadwork/utilities/__init__.py +++ b/src/compas_cadwork/utilities/__init__.py @@ -4,6 +4,7 @@ from typing import Generator from enum import auto +from enum import Enum import cadwork import utility_controller as uc @@ -14,7 +15,7 @@ from compas.geometry import Point from compas_cadwork.datamodel import Element from compas_cadwork.datamodel import ElementGroup -from compas_cadwork.datamodel.element import StrEnum +from compas_cadwork.datamodel import Dimension from compas_cadwork.conversions import point_to_compas from .ifc_export import IFCExporter @@ -22,7 +23,7 @@ from .dimensions import get_dimension_data -class CameraView(StrEnum): +class CameraView(str, Enum): """The view direction to which cadwork camera should be set in viewport. These coinside with the standard axes. @@ -166,6 +167,13 @@ def get_filename() -> str: return uc.get_3d_file_name() +def get_dimensions(): + result = [] + for dim in filter(lambda element: element.is_linear_dimension, get_all_elements(include_instructions=True)): + result.append(Dimension(dim.id)) + return result + + def get_element_groups(is_wall_frame: bool = True) -> Dict[str, ElementGroup]: """Returns a dictionary mapping names of the available building subgroups to their elements. @@ -393,4 +401,5 @@ def save_project_file(): "zoom_active_elements", "get_dimension_data", "get_bounding_box_from_cadwork_object", + "get_dimensions", ] diff --git a/src/compas_cadwork/utilities/dimensions.py b/src/compas_cadwork/utilities/dimensions.py index c214788..0fd0dbe 100644 --- a/src/compas_cadwork/utilities/dimensions.py +++ b/src/compas_cadwork/utilities/dimensions.py @@ -1,3 +1,4 @@ +# TODO: Once shifting anchor points is no longer required, remove this module as replaced by datamodel.dimension from typing import List from typing import Tuple from typing import Union diff --git a/src/compas_cadwork/utilities/events.py b/src/compas_cadwork/utilities/events.py index 2c315e4..051fc47 100644 --- a/src/compas_cadwork/utilities/events.py +++ b/src/compas_cadwork/utilities/events.py @@ -1,13 +1,15 @@ from compas_cadwork.datamodel import Element from . import get_all_element_ids +from . import get_dimensions class ElementDelta: """Helper for detecting changes in the available element collection""" def __init__(self): - self._known_element_ids = set(get_all_element_ids()) + self._known_element_ids = None + self.reset() def check_for_changed_elements(self): """Returns a list of element ids added to the file database since the last call. @@ -26,3 +28,34 @@ def check_for_changed_elements(self): def reset(self): """Reset the known element ids""" self._known_element_ids = set(get_all_element_ids()) + + +class DimensionsDelta: + """Helper for detecting edits to the dimensions in the document + + TODO: check if and how this can be merged with ElementDelta, seems this could do both jobs, question is just the reset point. + + """ + + def __init__(self): + self._known_dimensions = None + self.reset() + + def check_for_changed_dimensions(self): + """Returns a list of dimensions that existed but were modified since the last call to :method:`reset`. + + Returns + ------- + list(:class:`compas_cadwork.datamodel.Dimension`) + List of modified dimensions. + """ + # Changes will contain additions as well, since the objects are compared as a whole.. + # However, addtions need to be handled separately. Therefore, new ids are filtered out. + current_dimensions = get_dimensions() + changes = set(current_dimensions) - self._known_dimensions + additions = set([m.id for m in current_dimensions]) - set([m.id for m in self._known_dimensions]) + return list(filter(lambda m: m.id not in additions, changes)) + + def reset(self): + """Reset the known dimensions. Any changed dimensions after this call will be considered modifications.""" + self._known_dimensions = set(get_dimensions())