compas-masonry 0.2.1__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ __version__ = "0.2.1"
2
+
3
+ __all_plugins__ = ["compas_masonry.scene"]
@@ -0,0 +1,2 @@
1
+ if __name__ == "__main__":
2
+ pass
File without changes
@@ -0,0 +1,203 @@
1
+ #! python3
2
+ # venv: brg-csd
3
+ # r: compas_masonry
4
+
5
+ from typing import Union
6
+
7
+ import Eto.Drawing as drawing # type: ignore
8
+ import Eto.Forms as forms # type: ignore
9
+ import Rhino # type: ignore
10
+ import Rhino.UI # type: ignore
11
+ from pydantic import BaseModel
12
+ from pydantic import Field
13
+
14
+ from compas.colors import Color
15
+ from compas_session.settings import Settings
16
+
17
+
18
+ def field_ge(field):
19
+ """Get the 'greater than or equal to' constraint from a Pydantic field.
20
+
21
+ Parameters
22
+ ----------
23
+ field : ModelField
24
+ The Pydantic model field.
25
+
26
+ Returns
27
+ -------
28
+ float or int or None
29
+ The 'greater than or equal to' constraint if it exists, otherwise None.
30
+
31
+ """
32
+ ge = None
33
+ for m in field.metadata:
34
+ if hasattr(m, "ge"):
35
+ ge = m.ge
36
+ break
37
+ return ge
38
+
39
+
40
+ def field_le(field):
41
+ """Get the 'less than or equal to' constraint from a Pydantic field.
42
+
43
+ Parameters
44
+ ----------
45
+ field : ModelField
46
+ The Pydantic model field.
47
+
48
+ Returns
49
+ -------
50
+ float or int or None
51
+ The 'less than or equal to' constraint if it exists, otherwise None.
52
+
53
+ """
54
+ le = None
55
+ for m in field.metadata:
56
+ if hasattr(m, "le"):
57
+ le = m.le
58
+ break
59
+ return le
60
+
61
+
62
+ class SettingsForm(forms.Dialog[bool]):
63
+ """A dynamic form for editing Pydantic models.
64
+
65
+ Parameters
66
+ ----------
67
+ model_cls : Type[BaseModel]
68
+ The Pydantic model class to create the form for.
69
+ width : int, optional
70
+ The width of the form, by default 400.
71
+
72
+ """
73
+
74
+ def __init__(self, model: Union[Settings, BaseModel], title: str = "Settings", width: int = 400):
75
+ super().__init__()
76
+ self.ClientSize = drawing.Size(width, -1)
77
+ self.model = model
78
+ self.model_cls = type(model)
79
+ self.controls = {}
80
+ self.Title = title
81
+
82
+ self.Content = self._build_layout()
83
+
84
+ self.DefaultButton = self.ok_button
85
+ self.AbortButton = self.cancel_button
86
+
87
+ def _build_layout(self):
88
+ layout = forms.DynamicLayout()
89
+ layout.Padding = drawing.Padding(10)
90
+ layout.Spacing = drawing.Size(10, 10)
91
+
92
+ for name, field in self.model_cls.model_fields.items():
93
+ if issubclass(field.annotation, BaseModel): # type: ignore
94
+ continue
95
+
96
+ text = field.title or name.replace("_", " ").capitalize()
97
+ label = forms.Label()
98
+ label.Text = text
99
+ control = self._create_control(name, field, field.default)
100
+ self.controls[name] = control
101
+ layout.AddRow(label, control)
102
+
103
+ layout.AddRow(None)
104
+
105
+ self.ok_button = forms.Button()
106
+ self.ok_button.Text = "OK"
107
+ self.ok_button.Click += self._on_ok
108
+
109
+ self.cancel_button = forms.Button()
110
+ self.cancel_button.Text = "Cancel"
111
+ self.cancel_button.Click += self._on_cancel
112
+
113
+ layout.AddRow(None, self.ok_button, self.cancel_button)
114
+ return layout
115
+
116
+ def _create_control(self, name, field, default):
117
+ if field.annotation is str:
118
+ value = getattr(self.model, name, default) or ""
119
+ control = forms.TextBox()
120
+ control.Text = value
121
+
122
+ elif field.annotation is int:
123
+ value = getattr(self.model, name, default) or 0
124
+ control = forms.NumericUpDown()
125
+ control.Value = value
126
+ control.MinValue = field_ge(field)
127
+ control.MaxValue = field_le(field)
128
+
129
+ elif field.annotation is float:
130
+ value = getattr(self.model, name, default) or 0.0
131
+ control = forms.NumericUpDown()
132
+ control.Value = value
133
+ control.DecimalPlaces = 3
134
+ control.MinValue = field_ge(field)
135
+ control.MaxValue = field_le(field)
136
+
137
+ elif field.annotation is bool:
138
+ value = getattr(self.model, name, default) or False
139
+ control = forms.CheckBox()
140
+ control.Checked = value
141
+
142
+ elif (
143
+ field.annotation is tuple
144
+ and hasattr(field, "default")
145
+ and isinstance(field.default, tuple)
146
+ and len(field.default) == 4
147
+ and all(isinstance(x, int) for x in field.default)
148
+ ):
149
+ value = getattr(self.model, name, default) or (255, 255, 255, 255)
150
+ color = drawing.Color.FromArgb(*value)
151
+ control = forms.ColorPicker()
152
+ control.Value = color
153
+
154
+ else:
155
+ value = getattr(self.model, name, default) or ""
156
+ control = forms.TextBox()
157
+ control.Text = str(value)
158
+
159
+ return control
160
+
161
+ def _on_ok(self, sender, e):
162
+ values = {}
163
+ for name, control in self.controls.items():
164
+ if isinstance(control, forms.TextBox):
165
+ values[name] = control.Text
166
+ elif isinstance(control, forms.NumericUpDown):
167
+ values[name] = control.Value
168
+ elif isinstance(control, forms.CheckBox):
169
+ values[name] = control.Checked
170
+ elif isinstance(control, forms.ColorPicker):
171
+ c = control.Value
172
+ values[name] = (c.R, c.G, c.B, c.A)
173
+ try:
174
+ for key, value in values.items():
175
+ setattr(self.model, key, value)
176
+ self.Close(True)
177
+ except Exception as ex:
178
+ forms.MessageBox.Show(str(ex), "Validation Error")
179
+
180
+ def _on_cancel(self, sender, e):
181
+ self.Close(False)
182
+
183
+ def show(self):
184
+ return self.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)
185
+
186
+
187
+ # =============================================================================
188
+ # Run as main
189
+ # =============================================================================
190
+
191
+ if __name__ == "__main__":
192
+
193
+ class TestSettings(BaseModel):
194
+ name: str = Field("Default", title="Name", description="Enter your name")
195
+ age: int = Field(30, title="Age", ge=0, le=120)
196
+ is_active: bool = Field(True, title="Active")
197
+ color: tuple = Field(Color.white().rgba255, title="Color", description="Pick a color")
198
+
199
+ settings = TestSettings(name="Alice", age=28, is_active=True, color=Color.red().rgba255)
200
+ dialog = SettingsForm(settings) # type: ignore
201
+
202
+ if dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow):
203
+ print(dialog.model)
@@ -0,0 +1,13 @@
1
+ from compas.plugins import plugin
2
+ from compas.scene.context import register
3
+
4
+ from compas_tna.diagrams import FormDiagram
5
+ from .formobject import RhinoFormDiagramObject
6
+
7
+ # this clashes with TNA and/or RV
8
+ # we need a strategy for this
9
+
10
+
11
+ @plugin(category="factories", pluggable_name="register_scene_objects", requires=["Rhino"])
12
+ def register_scene_objects_rhino():
13
+ register(FormDiagram, RhinoFormDiagramObject, context="Rhino")
@@ -0,0 +1,94 @@
1
+ from typing import Optional
2
+
3
+ import scriptcontext as sc # type: ignore
4
+
5
+ from compas.colors import Color
6
+ from compas.datastructures import Mesh
7
+ from compas.scene.descriptors.color import ColorAttribute
8
+ from compas_masonry.session import MasonrySession as Session
9
+ from compas_rhino.conversions import transformation_to_rhino
10
+ from compas_rhino.conversions import vertices_and_faces_to_rhino
11
+ from compas_rhino.scene import RhinoSceneObject
12
+ from compas_tna.envelope import Envelope
13
+
14
+
15
+ class RhinoEnvelopeObject(RhinoSceneObject):
16
+ session: Session = Session()
17
+
18
+ color = ColorAttribute(default=Color.black())
19
+
20
+ def __init__(
21
+ self,
22
+ color: Optional[Color] = None,
23
+ show_intrados=True,
24
+ show_middle=False,
25
+ show_extrados=True,
26
+ **kwargs,
27
+ ):
28
+ super().__init__(**kwargs)
29
+ self.color = color or self.color
30
+ self.show_intrados = show_intrados
31
+ self.show_middle = show_middle
32
+ self.show_extrados = show_extrados
33
+
34
+ @property
35
+ def settings(self) -> dict:
36
+ settings = super().settings
37
+ settings["color"] = self.color
38
+ settings["show_intrados"] = self.show_intrados
39
+ settings["show_middle"] = self.show_middle
40
+ settings["show_extrados"] = self.show_extrados
41
+ return settings
42
+
43
+ @property
44
+ def envelope(self) -> Envelope:
45
+ return self.item # type: ignore
46
+
47
+ @envelope.setter
48
+ def envelope(self, envelope: Envelope) -> None:
49
+ self._item = envelope
50
+ self._transformation = None
51
+
52
+ def draw(self):
53
+ self._guids = []
54
+
55
+ if self.show_intrados:
56
+ self.draw_intrados()
57
+
58
+ if self.show_middle:
59
+ self.draw_middle()
60
+
61
+ if self.show_extrados:
62
+ self.draw_extrados()
63
+
64
+ return self._guids
65
+
66
+ def _draw_mesh(self, mesh: Mesh, name: str) -> None:
67
+ attr = self.compile_attributes(name=name, color=self.color)
68
+ vertex_index = mesh.vertex_index()
69
+ vertices = [mesh.vertex_attributes(vertex, "xyz") for vertex in mesh.vertices()]
70
+ faces = [[vertex_index[vertex] for vertex in mesh.face_vertices(face)] for face in mesh.faces()]
71
+ geometry = vertices_and_faces_to_rhino(vertices, faces, color=self.color, disjoint=True)
72
+ geometry.Transform(transformation_to_rhino(self.worldtransformation))
73
+ guid = sc.doc.Objects.AddMesh(geometry, attr)
74
+ if self.group:
75
+ self.add_to_group(self.group, [guid])
76
+ return guid
77
+
78
+ def draw_intrados(self):
79
+ if not self.envelope.intrados: # type: ignore
80
+ return
81
+ guid = self._draw_mesh(self.envelope.intrados, name="Intrados") # type: ignore
82
+ self._guids.append(guid)
83
+
84
+ def draw_middle(self):
85
+ if not self.envelope.middle: # type: ignore
86
+ return
87
+ guid = self._draw_mesh(self.envelope.middle, name="Middle") # type: ignore
88
+ self._guids.append(guid)
89
+
90
+ def draw_extrados(self):
91
+ if not self.envelope.extrados: # type: ignore
92
+ return
93
+ guid = self._draw_mesh(self.envelope.extrados, name="Extrados") # type: ignore
94
+ self._guids.append(guid)
@@ -0,0 +1,509 @@
1
+ from math import sqrt
2
+ from typing import Optional
3
+
4
+ import rhinoscriptsyntax as rs # type: ignore
5
+ import scriptcontext as sc # type: ignore
6
+
7
+ import compas_rhino.conversions
8
+ from compas.colors import Color
9
+ from compas.geometry import Cylinder
10
+ from compas.geometry import Line
11
+ from compas.geometry import Sphere
12
+ from compas.geometry import Vector
13
+ from compas.scene.descriptors.color import ColorAttribute
14
+ from compas.scene.descriptors.colordict import ColorDictAttribute
15
+ from compas_masonry.session import MasonrySession as Session
16
+ from compas_rui.scene import RUIMeshObject
17
+ from compas_tna.diagrams import FormDiagram
18
+
19
+
20
+ class RhinoFormDiagramObject(RUIMeshObject):
21
+ session: Session = Session()
22
+
23
+ vertexcolor = ColorDictAttribute(default=Color.white())
24
+ edgecolor = ColorDictAttribute(default=Color.black())
25
+ facecolor = ColorDictAttribute(default=Color.white())
26
+
27
+ freecolor = ColorAttribute(default=Color.white())
28
+ supportcolor = ColorAttribute(default=Color.red())
29
+ fixedcolor = ColorAttribute(default=Color.cyan())
30
+
31
+ residualcolor = ColorAttribute(default=Color.cyan())
32
+ reactioncolor = ColorAttribute(default=Color.green())
33
+
34
+ loadcolor = ColorAttribute(default=Color.green().darkened(50))
35
+ compressioncolor = ColorAttribute(default=Color.blue())
36
+ tensioncolor = ColorAttribute(default=Color.red())
37
+
38
+ ecrackcolor = ColorAttribute(default=Color.green())
39
+ icrackcolor = ColorAttribute(default=Color.blue())
40
+
41
+ boundscolor = ColorAttribute(default=Color.magenta())
42
+
43
+ def __init__(
44
+ self,
45
+ disjoint=True,
46
+ show_vertices=True,
47
+ show_edges=True,
48
+ show_faces=False,
49
+ vertexgroup="RhinoVAULT::FormDiagram::Vertices",
50
+ edgegroup="RhinoVAULT::FormDiagram::Edges",
51
+ facegroup="RhinoVAULT::FormDiagram::Faces",
52
+ layer="RhinoVAULT::FormDiagram",
53
+ show_supports=True,
54
+ show_fixed=True,
55
+ show_free=False,
56
+ loadgroup="RhinoVAULT::FormDiagram::Loads",
57
+ forcegroup="RhinoVAULT::FormDiagram::Forces",
58
+ reactiongroup="RhinoVAULT::FormDiagram::Reactions",
59
+ residualgroup="RhinoVAULT::FormDiagram::Residuals",
60
+ crackgroup="RhinoVAULT::FormDiagram::Cracks",
61
+ boundsgroup="RhinoVAULT::FormDiagram::Bounds",
62
+ **kwargs,
63
+ ):
64
+ super().__init__(
65
+ disjoint=disjoint,
66
+ show_vertices=show_vertices,
67
+ show_edges=show_edges,
68
+ show_faces=show_faces,
69
+ vertexgroup=vertexgroup,
70
+ edgegroup=edgegroup,
71
+ facegroup=facegroup,
72
+ layer=layer,
73
+ **kwargs,
74
+ )
75
+
76
+ self.show_supports = show_supports
77
+ self.show_fixed = show_fixed
78
+ self.show_free = show_free
79
+
80
+ self.loadgroup = loadgroup
81
+ self.forcegroup = forcegroup
82
+ self.reactiongroup = reactiongroup
83
+ self.residualgroup = residualgroup
84
+ self.crackgroup = crackgroup
85
+ self.boundsgroup = boundsgroup
86
+
87
+ # =============================================================================
88
+ # Properties
89
+ # =============================================================================
90
+
91
+ @property
92
+ def settings(self):
93
+ settings = super().settings
94
+ settings["show_supports"] = self.show_supports
95
+ settings["show_fixed"] = self.show_fixed
96
+ settings["show_free"] = self.show_free
97
+ return settings
98
+
99
+ @property
100
+ def diagram(self) -> FormDiagram:
101
+ return self.mesh # type: ignore
102
+
103
+ @diagram.setter
104
+ def diagram(self, diagram: FormDiagram) -> None:
105
+ self.mesh = diagram
106
+
107
+ # =============================================================================
108
+ # Helpers
109
+ # =============================================================================
110
+
111
+ def vertices(self, **kwargs) -> list[int]:
112
+ return list(self.diagram.vertices()) # type: ignore
113
+
114
+ def edges(self, **kwargs) -> list[tuple[int, int]]:
115
+ return list(self.diagram.edges_where(_is_edge=True)) # type: ignore
116
+
117
+ def faces(self, **kwargs) -> list[int]:
118
+ return list(self.diagram.faces_where(_is_loaded=True)) # type: ignore
119
+
120
+ def supports(self) -> list[int]:
121
+ return list(self.diagram.vertices_where(is_support=True)) # type: ignore
122
+
123
+ def vertex_is_support(self, vertex) -> bool:
124
+ return bool(self.diagram.vertex_attribute(vertex, "is_support"))
125
+
126
+ def vertex_is_fixed(self, vertex) -> bool:
127
+ return bool(self.diagram.vertex_attribute(vertex, "is_fixed"))
128
+
129
+ def vertex_residual(self, vertex) -> Vector:
130
+ return Vector(*self.diagram.vertex_attributes(vertex, ["_rx", "_ry", "_rz"])) # type: ignore
131
+
132
+ def vertex_weight(self, vertex) -> float:
133
+ weight = 0
134
+ thickness = self.diagram.vertex_attribute(vertex, "t")
135
+ if thickness:
136
+ area = self.diagram.vertex_area(vertex)
137
+ weight = area * thickness
138
+ return weight
139
+
140
+ def vertex_load(self, vertex) -> Vector:
141
+ return Vector(*self.diagram.vertex_attributes(vertex, ["px", "py", "pz"])) # type: ignore
142
+
143
+ def vertex_bound(self, vertex) -> Optional[Line]:
144
+ ub = self.diagram.vertex_attribute(vertex, "ub")
145
+ lb = self.diagram.vertex_attribute(vertex, "lb")
146
+ if ub and lb:
147
+ point = self.diagram.vertex_point(vertex)
148
+ a = point.copy()
149
+ a.z = ub
150
+ b = point.copy()
151
+ b.z = lb
152
+ return Line(a, b)
153
+
154
+ def vertex_is_on_upper_bound(self, vertex, tol=1e-6) -> bool:
155
+ ub = self.diagram.vertex_attribute(vertex, "ub")
156
+ if ub is None:
157
+ return False
158
+ point = self.diagram.vertex_point(vertex)
159
+ return abs(point.z - ub) < tol
160
+
161
+ def vertex_is_on_lower_bound(self, vertex, tol=1e-6) -> bool:
162
+ lb = self.diagram.vertex_attribute(vertex, "lb")
163
+ if lb is None:
164
+ return False
165
+ point = self.diagram.vertex_point(vertex)
166
+ return abs(point.z - lb) < tol
167
+
168
+ def forces(self) -> list[float]:
169
+ Q = [self.diagram.edge_attribute(edge, "q") or 0.0 for edge in self.edges()]
170
+ L = [self.diagram.edge_length(edge) or 0.0 for edge in self.edges()]
171
+ return [q * l for q, l in zip(Q, L)] # noqa: E741
172
+
173
+ def edge_force(self, edge) -> float:
174
+ q = self.diagram.edge_attribute(edge, "q") or 0.0
175
+ l = self.diagram.edge_length(edge) or 0.0 # noqa: E741
176
+ return q * l
177
+
178
+ def compute_vertex_color(self, vertex) -> Color:
179
+ if self.vertex_is_support(vertex):
180
+ color = self.supportcolor
181
+ elif self.vertex_is_fixed(vertex):
182
+ color = self.fixedcolor
183
+ else:
184
+ color = self.freecolor
185
+ return color # type: ignore
186
+
187
+ def compute_visible_vertices(self) -> list[int]:
188
+ vertices = []
189
+ if self.show_free:
190
+ vertices += list(self.diagram.vertices_where(is_support=False, is_fixed=False))
191
+ if self.show_fixed:
192
+ vertices += list(self.diagram.vertices_where(is_fixed=True))
193
+ if self.show_supports:
194
+ vertices += list(self.diagram.vertices_where(is_support=True))
195
+ return vertices
196
+
197
+ def compute_edge_colors(self, tol=1e-3) -> list[Color]:
198
+ forces = self.forces()
199
+ magnitudes = [abs(f) for f in forces]
200
+ fmin = min(magnitudes)
201
+ fmax = max(magnitudes)
202
+
203
+ colors = []
204
+
205
+ if fmax - fmin >= tol:
206
+ for magnitude in magnitudes:
207
+ colors.append(Color.from_i((magnitude - fmin) / (fmax - fmin)))
208
+
209
+ return colors
210
+
211
+ def compute_pipe_colors(self, tol=1e-3) -> dict[tuple[int, int], Color]:
212
+ edges = self.edges()
213
+ forces = [self.edge_force(edge) for edge in edges]
214
+ magnitudes = [abs(f) for f in forces]
215
+ fmin = min(magnitudes)
216
+ fmax = max(magnitudes)
217
+
218
+ edge_color = {}
219
+
220
+ if fmax - fmin >= tol:
221
+ for edge, force, magnitude in zip(edges, forces, magnitudes):
222
+ edge_color[edge] = Color.from_i((magnitude - fmin) / (fmax - fmin))
223
+
224
+ return edge_color
225
+
226
+ # =============================================================================
227
+ # Names
228
+ # =============================================================================
229
+
230
+ def vertex_load_name(self, vertex) -> str:
231
+ return f"{self.diagram.name}.vertex.{vertex}.load"
232
+
233
+ def vertex_selfweight_name(self, vertex) -> str:
234
+ return f"{self.diagram.name}.vertex.{vertex}.selfweight"
235
+
236
+ def vertex_reaction_name(self, vertex) -> str:
237
+ return f"{self.diagram.name}.vertex.{vertex}.reaction"
238
+
239
+ def vertex_residual_name(self, vertex) -> str:
240
+ return f"{self.diagram.name}.vertex.{vertex}.residual"
241
+
242
+ def vertex_bound_name(self, vertex) -> str:
243
+ return f"{self.diagram.name}.vertex.{vertex}.bound"
244
+
245
+ def vertex_crack_name(self, vertex) -> str:
246
+ return f"{self.diagram.name}.vertex.{vertex}.crack"
247
+
248
+ def edge_pipe_name(self, edge) -> str:
249
+ return f"{self.diagram.name}.edge.{edge}.pipe"
250
+
251
+ # =============================================================================
252
+ # Clear
253
+ # =============================================================================
254
+
255
+ # =============================================================================
256
+ # Draw
257
+ # =============================================================================
258
+
259
+ def draw(self):
260
+ faces = []
261
+ if self.show_faces:
262
+ faces += list(self.faces())
263
+ if faces:
264
+ self.show_faces = faces
265
+
266
+ for vertex in self.vertices():
267
+ self.vertexcolor[vertex] = self.compute_vertex_color(vertex)
268
+
269
+ super().draw()
270
+
271
+ if self.session.settings.formdiagram.show_reactions:
272
+ self.draw_reactions()
273
+
274
+ if self.session.settings.formdiagram.show_residuals:
275
+ self.draw_residuals()
276
+
277
+ if self.session.settings.formdiagram.show_pipes:
278
+ self.draw_pipes()
279
+
280
+ if self.session.settings.formdiagram.show_loads:
281
+ self.draw_loads()
282
+
283
+ if self.session.settings.formdiagram.show_bounds:
284
+ self.draw_bounds()
285
+
286
+ if self.session.settings.formdiagram.show_cracks:
287
+ self.draw_cracks()
288
+
289
+ return self.guids
290
+
291
+ def draw_vertices(self):
292
+ if self.show_vertices is True:
293
+ self.show_vertices = self.compute_visible_vertices()
294
+
295
+ for vertex in self.vertices():
296
+ self.vertexcolor[vertex] = self.compute_vertex_color(vertex)
297
+
298
+ return super().draw_vertices()
299
+
300
+ def draw_edges(self):
301
+ if self.show_edges is True:
302
+ self.show_edges = self.edges()
303
+
304
+ return super().draw_edges()
305
+
306
+ def draw_faces(self):
307
+ if self.show_faces:
308
+ self.show_faces = self.faces()
309
+
310
+ return super().draw_faces()
311
+
312
+ # =============================================================================
313
+ # Redraw
314
+ # =============================================================================
315
+
316
+ def redraw_vertices(self):
317
+ rs.EnableRedraw(False)
318
+ self.clear_vertices()
319
+ self.draw_vertices()
320
+ rs.EnableRedraw(True)
321
+ rs.Redraw()
322
+
323
+ def redraw_edges(self):
324
+ rs.EnableRedraw(False)
325
+ self.clear_edges()
326
+ self.draw_edges()
327
+ rs.EnableRedraw(True)
328
+ rs.Redraw()
329
+
330
+ def redraw_faces(self):
331
+ rs.EnableRedraw(False)
332
+ self.clear_faces()
333
+ self.draw_faces()
334
+ rs.EnableRedraw(True)
335
+ rs.Redraw()
336
+
337
+ def redraw(self):
338
+ rs.EnableRedraw(False)
339
+ self.clear()
340
+ self.draw()
341
+ rs.EnableRedraw(True)
342
+ rs.Redraw()
343
+
344
+ # =============================================================================
345
+ # Structural
346
+ # =============================================================================
347
+
348
+ def draw_loads(self):
349
+ guids = []
350
+ color = self.loadcolor
351
+ scale = self.session.settings.formdiagram.scale_loads
352
+ tol = self.session.settings.formdiagram.tol_vectors
353
+
354
+ for vertex in self.diagram.vertices_where(is_support=False):
355
+ load = self.vertex_load(vertex)
356
+ if load is not None:
357
+ vector = load * scale
358
+ if vector.length > tol:
359
+ name = self.vertex_load_name(vertex)
360
+ attr = self.compile_attributes(name=name, color=color, arrow="end")
361
+ point = self.diagram.vertex_point(vertex)
362
+ line = Line.from_point_and_vector(point + vector, point)
363
+ guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr)
364
+ guids.append(guid)
365
+
366
+ if guids:
367
+ if self.loadgroup:
368
+ self.add_to_group(self.loadgroup, guids)
369
+ elif self.group:
370
+ self.add_to_group(self.group, guids)
371
+
372
+ self._guids += guids
373
+ return guids
374
+
375
+ def draw_pipes(self):
376
+ guids = []
377
+ scale = self.session.settings.formdiagram.scale_pipes
378
+ tol = self.session.settings.formdiagram.tol_pipes
379
+
380
+ for edge in self.edges():
381
+ force = self.edge_force(edge)
382
+ if force:
383
+ line = self.diagram.edge_line(edge)
384
+ radius = sqrt(abs(force)) * scale
385
+ color = self.compressioncolor
386
+ if radius > tol:
387
+ pipe = Cylinder.from_line_and_radius(line, radius)
388
+ name = self.edge_pipe_name(edge)
389
+ attr = self.compile_attributes(name=name, color=color)
390
+ guid = sc.doc.Objects.AddBrep(compas_rhino.conversions.cylinder_to_rhino_brep(pipe), attr)
391
+ guids.append(guid)
392
+
393
+ if guids:
394
+ if self.forcegroup:
395
+ self.add_to_group(self.forcegroup, guids)
396
+ elif self.group:
397
+ self.add_to_group(self.group, guids)
398
+
399
+ self._guids += guids
400
+ return guids
401
+
402
+ def draw_reactions(self):
403
+ guids = []
404
+ scale = self.session.settings.formdiagram.scale_reactions
405
+ tol = self.session.settings.formdiagram.tol_vectors
406
+
407
+ for vertex in self.supports():
408
+ residual = self.vertex_residual(vertex)
409
+ vector = residual * -scale
410
+ if vector.length > tol:
411
+ name = self.vertex_reaction_name(vertex)
412
+ attr = self.compile_attributes(name=name, color=self.reactioncolor, arrow="end")
413
+ point = self.diagram.vertex_point(vertex)
414
+ line = Line.from_point_and_vector(point, vector)
415
+ guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr)
416
+ guids.append(guid)
417
+
418
+ if guids:
419
+ if self.reactiongroup:
420
+ self.add_to_group(self.reactiongroup, guids)
421
+ elif self.group:
422
+ self.add_to_group(self.group, guids)
423
+
424
+ self._guids += guids
425
+ return guids
426
+
427
+ def draw_residuals(self):
428
+ guids = []
429
+ scale = self.session.settings.formdiagram.scale_residuals
430
+ tol = self.session.settings.formdiagram.tol_vectors
431
+
432
+ for vertex in self.diagram.vertices_where(is_support=False):
433
+ residual = self.vertex_residual(vertex)
434
+ vector = residual * scale
435
+ if vector.length > tol:
436
+ name = self.vertex_residual_name(vertex)
437
+ attr = self.compile_attributes(name=name, color=self.residualcolor, arrow="end")
438
+ point = self.diagram.vertex_point(vertex)
439
+ line = Line.from_point_and_vector(point, vector)
440
+ guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr)
441
+ guids.append(guid)
442
+
443
+ if guids:
444
+ if self.residualgroup:
445
+ self.add_to_group(self.residualgroup, guids)
446
+ elif self.group:
447
+ self.add_to_group(self.group, guids)
448
+
449
+ self._guids += guids
450
+ return guids
451
+
452
+ # =============================================================================
453
+ # Envelope
454
+ # =============================================================================
455
+
456
+ def draw_bounds(self):
457
+ guids = []
458
+
459
+ for vertex in self.vertices():
460
+ bound = self.vertex_bound(vertex)
461
+ if bound:
462
+ name = self.vertex_bound_name(vertex)
463
+ attr = self.compile_attributes(name=name, color=self.boundscolor)
464
+
465
+ guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(bound), attr)
466
+ guids.append(guid)
467
+
468
+ guid = sc.doc.Objects.AddPoint(compas_rhino.conversions.point_to_rhino(bound.start), attr)
469
+ guids.append(guid)
470
+
471
+ guid = sc.doc.Objects.AddPoint(compas_rhino.conversions.point_to_rhino(bound.end), attr)
472
+ guids.append(guid)
473
+
474
+ if guids:
475
+ if self.boundsgroup:
476
+ self.add_to_group(self.boundsgroup, guids)
477
+ elif self.group:
478
+ self.add_to_group(self.group, guids)
479
+
480
+ self._guids += guids
481
+ return guids
482
+
483
+ def draw_cracks(self):
484
+ guids = []
485
+
486
+ for vertex in self.vertices():
487
+ if self.vertex_is_on_lower_bound(vertex):
488
+ name = self.vertex_crack_name(vertex)
489
+ attr = self.compile_attributes(name=name, color=self.icrackcolor)
490
+ elif self.vertex_is_on_upper_bound(vertex):
491
+ name = self.vertex_crack_name(vertex)
492
+ attr = self.compile_attributes(name=name, color=self.ecrackcolor)
493
+ else:
494
+ continue
495
+
496
+ point = self.diagram.vertex_point(vertex)
497
+ radius = self.session.settings.formdiagram.crack_radius
498
+ sphere = Sphere(radius, point=point)
499
+ guid = sc.doc.Objects.AddSphere(compas_rhino.conversions.sphere_to_rhino(sphere), attr)
500
+ guids.append(guid)
501
+
502
+ if guids:
503
+ if self.crackgroup:
504
+ self.add_to_group(self.crackgroup, guids)
505
+ elif self.group:
506
+ self.add_to_group(self.group, guids)
507
+
508
+ self._guids += guids
509
+ return guids
@@ -0,0 +1,10 @@
1
+ from compas_session.lazyload import LazyLoadSession
2
+
3
+ from .settings import MasonrySettings
4
+
5
+
6
+ class MasonrySession(LazyLoadSession):
7
+ settingsclass = MasonrySettings
8
+ settings: MasonrySettings # type: ignore
9
+
10
+ # set a fail message
@@ -0,0 +1,53 @@
1
+ from pydantic import BaseModel
2
+ from pydantic import Field
3
+
4
+ from compas_session.settings import Settings
5
+
6
+
7
+ class FormDiagramSettings(BaseModel):
8
+ show_reactions: bool = Field(default=True, title="Show Reactions")
9
+ show_residuals: bool = Field(default=False, title="Show Residuals")
10
+ show_pipes: bool = Field(default=False, title="Show Pipes")
11
+ show_loads: bool = Field(default=False, title="Show Loads")
12
+ show_bounds: bool = Field(default=False, title="Show Bounds")
13
+ show_cracks: bool = Field(default=False, title="Show Cracks")
14
+
15
+ scale_reactions: float = Field(default=0.01, ge=1e-6, le=1e3, title="Scale Reactions")
16
+ scale_residuals: float = Field(default=1.0, ge=1e-6, le=1e3, title="Scale Residuals")
17
+ scale_pipes: float = Field(default=0.01, ge=1e-6, le=1e3, title="Scale Pipes")
18
+ scale_loads: float = Field(default=1.0, ge=1e-6, le=1e3, title="Scale Loads")
19
+
20
+ tol_vectors: float = Field(default=1e-3, ge=1e-6, le=1e3, title="Tolerance Vectors")
21
+ tol_pipes: float = Field(default=1e-2, ge=1e-6, le=1e3, title="Tolerance Pipes")
22
+
23
+ crack_radius: float = Field(default=0.1, ge=0.01, le=100, title="Crack Radius")
24
+
25
+
26
+ class EnvelopeSettings(BaseModel):
27
+ show_intrados: bool = Field(default=True, title="Show Intrados")
28
+ show_middle: bool = Field(default=False, title="Show Middle")
29
+ show_extrados: bool = Field(default=True, title="Show Extrados")
30
+
31
+
32
+ class BlockModelSettings(BaseModel):
33
+ tol_contacts: float = Field(default=1e-3, ge=1e-6, le=1e3, title="Tolerance Contacts")
34
+ amin_contacts: float = Field(default=1e-2, ge=1e-6, le=90, title="Minimum Angle Contacts")
35
+
36
+ show_blocks: bool = Field(default=True, title="Show Blocks")
37
+ show_supports: bool = Field(default=True, title="Show Supports")
38
+ show_contacts: bool = Field(default=False, title="Show Contacts")
39
+ show_interactions: bool = Field(default=False, title="Show Interactions")
40
+ show_selfweight: bool = Field(default=False, title="Show Selfweight")
41
+ show_reactions: bool = Field(default=False, title="Show Reactions")
42
+
43
+ scale_selfweight: float = Field(default=1.0, ge=1e-6, le=1e3, title="Scale Selfweight")
44
+ scale_reactions: float = Field(default=0.01, ge=1e-6, le=1e3, title="Scale Reactions")
45
+
46
+
47
+ class MasonrySettings(Settings):
48
+ autoupdate: bool = Field(default=True, title="Auto Update")
49
+ autosave: bool = Field(default=False, title="Auto Save")
50
+
51
+ formdiagram: FormDiagramSettings = FormDiagramSettings()
52
+ envelope: EnvelopeSettings = EnvelopeSettings()
53
+ blockmodel: BlockModelSettings = BlockModelSettings()
@@ -0,0 +1,44 @@
1
+ #! python3
2
+ # venv: brg-csd
3
+ # r: compas_masonry
4
+
5
+ import pathlib
6
+
7
+ import Eto.Drawing # type: ignore
8
+ import Eto.Forms # type: ignore
9
+ import Rhino # type: ignore
10
+ import Rhino.UI # type: ignore
11
+ import System # type: ignore
12
+
13
+ pluginfile = Rhino.PlugIns.PlugIn.PathFromId(System.Guid("4384e04f-2997-429b-b3fc-53e7fe78703e"))
14
+ shared = pathlib.Path(str(pluginfile)).parent / "shared"
15
+
16
+
17
+ class SplashForm(Eto.Forms.Dialog[bool]):
18
+ def __init__(self, title, url, width=800, height=450):
19
+ super().__init__()
20
+
21
+ self.Title = title
22
+ self.Padding = Eto.Drawing.Padding(0)
23
+ self.Resizable = False
24
+ self.ClientSize = Eto.Drawing.Size(width, height)
25
+ self.WindowStyle = Eto.Forms.WindowStyle.NONE # type: ignore
26
+
27
+ webview = Eto.Forms.WebView()
28
+ webview.Size = Eto.Drawing.Size(width, height)
29
+ webview.Url = System.Uri(url)
30
+ webview.BrowserContextMenuEnabled = False
31
+ webview.DocumentLoading += self.action
32
+
33
+ layout = Eto.Forms.DynamicLayout()
34
+ layout.BeginVertical()
35
+ layout.AddRow(webview)
36
+ layout.EndVertical()
37
+ self.Content = layout
38
+
39
+ def action(self, sender, e):
40
+ if e.Uri.Scheme == "action" and e.Uri.Host == "close":
41
+ self.Close()
42
+
43
+ def show(self):
44
+ return self.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: compas_masonry
3
+ Version: 0.2.1
4
+ Summary: COMPAS Masonry is a masonry assessment toolbox for Rhino based on the COMPAS framework.
5
+ Author-email: tom van mele <tom.v.mele@gmail.com>
6
+ License: MIT License
7
+
8
+ ETH Zurich - Block Research Group
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://blockresearchgroup.github.io/compas_masonry
29
+ Project-URL: Documentation, https://blockresearchgroup.github.io/compas_masonry
30
+ Project-URL: Repository, https://github.com/blockresearchgroup/compas_masonry
31
+ Project-URL: Changelog, https://github.com/blockresearchgroup/compas_masonry/blob/main/CHANGELOG.md
32
+ Project-URL: Issues, https://github.com/blockresearchgroup/compas_masonry/issues
33
+ Project-URL: Forum, https://forum.compas-framework.org/
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Topic :: Scientific/Engineering
36
+ Classifier: Operating System :: Unix
37
+ Classifier: Operating System :: POSIX
38
+ Classifier: Operating System :: Microsoft :: Windows
39
+ Classifier: Programming Language :: Python
40
+ Classifier: Programming Language :: Python :: 3
41
+ Classifier: Programming Language :: Python :: 3.9
42
+ Classifier: Programming Language :: Python :: 3.10
43
+ Classifier: Programming Language :: Python :: 3.11
44
+ Requires-Python: >=3.9
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE
47
+ Requires-Dist: cvxpy
48
+ Requires-Dist: compas>=2.14.1
49
+ Requires-Dist: compas_cgal>=0.9.1
50
+ Requires-Dist: compas_dem>=0.4
51
+ Requires-Dist: compas_libigl>=0.7.6
52
+ Requires-Dist: compas_rui>=0.6
53
+ Requires-Dist: compas_session>=0.7
54
+ Requires-Dist: compas_tna>=0.7
55
+ Requires-Dist: compas_tno>=0.3
56
+ Provides-Extra: dev
57
+ Requires-Dist: bump-my-version; extra == "dev"
58
+ Requires-Dist: compas_invocations2; extra == "dev"
59
+ Requires-Dist: invoke>=0.14; extra == "dev"
60
+ Requires-Dist: ruff; extra == "dev"
61
+ Requires-Dist: sphinx_compas2_theme; extra == "dev"
62
+ Requires-Dist: twine; extra == "dev"
63
+ Requires-Dist: wheel; extra == "dev"
64
+ Dynamic: license-file
65
+
66
+ # COMPAS Masonry
67
+
68
+ > [!WARNING]
69
+ > This plugin is under active development,
70
+ > and its functionality will frequently change.
71
+
72
+ ![COMPAS Masonry](compas-Masonry.png)
73
+
74
+ COMPAS Masonry is a plugin for Rhino for the assessment of masonry structures,
75
+ and for the stability analsis of Discrete Element Models in general.
76
+
77
+ The current version of this plugin is based on COMPAS 2 and is available for Rhino 8.
78
+
79
+ ## Installation
80
+
81
+ To install COMPAS-Masonry, use Rhino's package manager Yak.
82
+ To open Yak, type `PackageManager` on the Rhino command line.
83
+
84
+ ![COMPAS-Masonry installation with Yak](/gitbook/.gitbook/assets/COMPAS-Masonry_yak.png)
85
+
86
+ ## COMPAS Packages
87
+
88
+ COMPAS-Masonry uses the following COMPAS packages:
89
+
90
+ * [compas](https://github.com/compas-dev/compas)
91
+ * [compas_assembly](https://github.com/blockresearchgroup/compas_assembly)
92
+ * [compas_model](https://github.com/blockresearchgroup/compas_model)
93
+ * [compas_rui](https://github.com/blockresearchgroup/compas_rui)
94
+ * [compas_session](https://github.com/blockresearchgroup/compas_session)
95
+
96
+ After installing RhinoVAULT with Yak, these requirements will be installed automatically if necessary.
97
+ The tool might be unresponsive during this process, which might take up to 1 or 2 mins.
98
+ The packages are installed in a separate virtual environment named `COMPAS-Masonry`.
99
+
100
+ > [!NOTE]
101
+ > Note that COMPAS-Masonry currently doesn't include any solvers
102
+ > (such as [compas_cra](https://github.com/blockresearchgroup/compas_cra)) yet.
103
+ > This is because they have dependencies that are still difficult to install in Rhino.
104
+
105
+ ## User Interface
106
+
107
+ COMPAS-Masonry defines the following Rhino commands:
108
+
109
+ * `Masonry`
110
+ * `Masonry_session_undo`
111
+ * `Masonry_session_redo`
112
+ * `Masonry_session_open`
113
+ * `Masonry_session_save`
114
+ * `Masonry_scene_clear`
115
+ * `Masonry_scene_redraw`
116
+ * `Masonry_settings`
117
+
118
+ These commands can be executed using the Rhino command line (simply start typing the command name),
119
+ or with the corresponding buttons of the COMPAS-Masonry toolbar.
120
+
121
+ ![COMPAS-Masonry toolbar](/gitbook/.gitbook/assets/COMPAS-Masonry_toolbar.png)
122
+
123
+ If the toolbar is not visible after installing COMPAS-Masonry,
124
+ you can load it from the "Toolbars" page.
125
+ To open the "Toolbars" page, type `Toolbar` on the Rhino command line...
126
+
127
+ ![Rhino Toolbars](/gitbook/.gitbook/assets/Rhino_toolbars.png)
128
+
129
+ ## Documentation
130
+
131
+ For further "getting started" instructions, a tutorial, examples, and an detailed description of the individual commands and the user interface, please check out the online documentation here: [COMPAS-Masonry Gitbook](https://blockresearchgroup.gitbook.io/COMPAS-Masonry)
132
+
133
+ ## Issues
134
+
135
+ Please report problems using the issue tracker of the github repo: <https://github.com/blockresearchgroup/compas-Masonry/issues>
@@ -0,0 +1,15 @@
1
+ compas_masonry/__init__.py,sha256=6rlt9_Slk91nZSCwRLS2Fq-S-jeMbC69Mo8xhW-JG-A,66
2
+ compas_masonry/__main__.py,sha256=-GqoftNECZBtC8F82KWJrdSD05BcKBrrL0ElHEEUHyk,36
3
+ compas_masonry/session.py,sha256=qTzZTLZA2pnLPvYeLPxefwlCgp8dhn4JAiL9prF3Sbo,240
4
+ compas_masonry/settings.py,sha256=RRVsZ4ZgV6Sinr2tHQ5FeV5IIqK7S1hwW7aDgsFWiFs,2565
5
+ compas_masonry/splash.py,sha256=rQSOXMcHguoAT7eqRN_edO0ctN0i95tzYX7PgLKJWT4,1342
6
+ compas_masonry/forms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ compas_masonry/forms/settings.py,sha256=m6tgvhYnOF-Nv-GvfDlCmThC7Yf0FMsXpBp2c6mRJPM,6288
8
+ compas_masonry/scene/__init__.py,sha256=5mfXQp2zkLD45WN69RzDs0aHunMmTwbBQiz4cF9VvvM,429
9
+ compas_masonry/scene/envelopeobject.py,sha256=bGsuB8NEm4KoMDtqneE5XcmjUX9BqaBNvISVow7qA8A,3146
10
+ compas_masonry/scene/formobject.py,sha256=_Jf58QRI3liGv6T3fmTDEb_sp3cd1E3UrVu-B2BfgjI,18164
11
+ compas_masonry-0.2.1.dist-info/licenses/LICENSE,sha256=wLowriMQkZN59QuARxuHKSyxeWroT2yQwAHRItCs2yM,1071
12
+ compas_masonry-0.2.1.dist-info/METADATA,sha256=SrNN11LtEDWuOZe8ozd5gl7PDIqIqT7hRFzfOrWVHDU,5822
13
+ compas_masonry-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ compas_masonry-0.2.1.dist-info/top_level.txt,sha256=iQC5Th2Uypnt4aDzfH4p37BbV1mL-okJfjoHZKz_3do,15
15
+ compas_masonry-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ ETH Zurich - Block Research Group
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ compas_masonry