Compare commits

...

301 Commits

Author SHA1 Message Date
Sigma Dimensions 8083a635a8 You can now import work schedules from csv 2023-03-28 04:04:48 +00:00
Dion Moult fab76bbcde Fixed bug where enumeration values in psets could be empty lists. 2023-03-27 23:08:47 +11:00
krande 7f78df595f minor clarification in comment 2023-03-27 13:35:24 +02:00
krande 03ff34f89f update ci-restart script with code that works and is testable locally 2023-03-27 13:35:24 +02:00
krande 8820a1dcc3 switch to a python implementation of the same procedure 2023-03-27 13:35:24 +02:00
krande 514975d123 add a workflow to catches and restarts failed conda builds due to 143 errors. And minor cleanup of unused variables in conda meta.yaml 2023-03-27 13:35:24 +02:00
Dion Moult c87911f00e IFC selector now uses "class" as a keyword to get the IFC class to disambiguate between "type" and "class". 2023-03-27 21:47:11 +11:00
Dion Moult 1e77562f0f Fix #2847. You can now join meshes and extrusions together. 2023-03-27 21:46:26 +11:00
Dion Moult b1843ce781 Fix IfcTester bug which did not consider null enumeration values 2023-03-27 17:36:06 +11:00
Dion Moult 02fb09ff7e Implement faux-mirroring. 2023-03-27 14:21:59 +11:00
Dion Moult bdb193f500 Minor fix 2023-03-27 13:38:14 +11:00
Dion Moult e9a3630595 Fix #2898. You can now copy materials from the materials tab. 2023-03-26 19:04:32 +11:00
Dion Moult fb3e4016ec See #2898. Fix bug where UI data was not refreshed when you switched active materials. 2023-03-26 18:07:10 +11:00
Trashman247 15a1aa5355 Move bl_descriptions
Moved bl_descriptions under bl_options when bl_options exists, copying the syntax of already existing operators with descriptions.
2023-03-26 17:46:33 +11:00
Trashman247 f1d6e95e51 Add tooltips to IFC Project Info and Clash Sets Panels 2023-03-26 17:46:33 +11:00
Dion Moult b9c99b35a5 Fix #2881. Support added for referencing nested relationships and material set items. 2023-03-25 22:07:18 +11:00
Dion Moult df0a503b89 Improve implementation of is_moved so that anywhere we check movement, types and projects are not considered 2023-03-24 21:59:09 +11:00
Dion Moult 346071185f Fix bug where appending a product from a library which had relationships to types caused a bunch of types to be pulled in unintentionally 2023-03-24 20:53:55 +11:00
Dion Moult f3f90e0ad5 Fix bug where mesh analysis during updating meshes as solids (i.e. extrusions) did not remove doubles, causing it to fail on sloppy meshes (and meshes are often sloppy) 2023-03-24 20:53:18 +11:00
Dion Moult 8aa2c1eea7 Fix critical bug where types would think they moved and cause long syncs at export time. 2023-03-24 20:52:20 +11:00
Dion Moult 57c32e55c2 Fix #2698. Bug where clash sets couldn't be visualised if you had one of the models open (i.e. not linked) in the active session. 2023-03-24 18:25:33 +11:00
Dion Moult de080a668f Fix #2901. Silly typo. 2023-03-24 17:48:27 +11:00
Dion Moult 00fc71dcbe Fix #2837. Shared subelements mistakenly caused the subelement append loop to break early. The solution is to traverse one level at a time. 2023-03-24 17:36:46 +11:00
Andrej730 808cb6ec14 Fixed outdated code issue 2023-03-24 11:00:08 +05:00
Andrej730 8899776106 Fixed #2900
1) Leaders text decorator wasn't changed after adding support for multiple literals
2) added drawing data update on toggling viewport annotations - so user would see correct values right away; also fixed similar thing on updating product
2023-03-24 11:00:08 +05:00
Andrej730 ae674b00c2 Fixed bugs after adding aggregated text annotations (#2899) 2023-03-24 10:09:23 +05:00
Dion Moult 73b6d23d4f Minor fix 2023-03-24 16:05:27 +11:00
Dion Moult 6d1cabe534 Fix #2887. Fix bug where you could create multiple opening objects. 2023-03-24 15:03:39 +11:00
Dion Moult ab34fda42c See #2887. Save mesh dialog now only shown when mesh is edited. 2023-03-24 14:33:13 +11:00
Dion Moult fab4133df6 Minor fix 2023-03-24 13:22:10 +11:00
Dion Moult 301e368a2c Minor fix 2023-03-23 22:09:31 +11:00
Dion Moult 0a22714fb8 See #2887. Experimental edit mode toggle override to ensure mesh changes stay in sync and so that you can edit meshes without openings applied. 2023-03-23 21:51:06 +11:00
Thomas Krijnen ef2cec7435 #2884 depends python3-pytest 2023-03-23 09:05:39 +01:00
Dion Moult b4f486e9aa Minor fix 2023-03-23 16:54:15 +11:00
Andrej730 297303063e Small changes in default names for saving ifc projects
1) changed first ifc saving handler from save_pre to save_post so that in the situation when you just created with .blend file and started ifc project on save dialog would suggest the same name for the .ifc file and for .blend file (previously it was always suggesting ".ifc" and you had to type it in manually).

2) suggested default ifc project named changed from `<blend_filename>.blend.ifc` to `<blend_filename>.ifc` for simplicity.

PS save_pre didn't really work anyway - by the time "save ifc dialog" file was invoked and appeared .blend file was already saved anyway.
2023-03-23 09:54:16 +05:00
Andrej730 df95bdb48e Fixing tests after 2003bfe4f 2023-03-23 09:53:02 +05:00
Dion Moult 95ac021623 Improve UI to indicate active representation. 2023-03-23 15:37:52 +11:00
Dion Moult 416efa4f14 See #2887. Fix bad UI design that could lead to objects created with the unintended context. 2023-03-23 10:44:57 +11:00
Ryan Schultz 2003bfe4fe Added_MODEL_VIEW_context
added...
context_type="Model", context_identifier="Annotation", target_view="MODEL_VIEW", parent=model
from: https://github.com/IfcOpenShell/IfcOpenShell/issues/2890
2023-03-23 08:00:39 +11:00
Andrej730 fc55a48c11 Automated adjustment of annotations scale if either camera orthographic scale or drawing scale changed 2023-03-22 15:56:55 +05:00
Andrej730 37a6be1e2b Ifc text viewport rotation for text with box alignment 2023-03-22 15:47:07 +05:00
Andrej730 e551bf531c Optimized ifc text viewport rotation algorithm 2023-03-22 14:37:48 +05:00
Andrej730 e173ea7f80 Ifc Text rotation in viewport preview 2023-03-22 14:05:03 +05:00
Andrej730 f8dc421f03 Fixed bug with multiline text in svg
Insertion position at <text> instead of <tspan> creates unnecessary x offsets between tspans
2023-03-22 11:04:43 +05:00
Andrej730 e2afb34e0d Fixed typo causing #2889 2023-03-22 10:09:38 +05:00
Andrej730 f7181aae8a Multiple literals in 1 Ifc Text + aggregated text annotations
Added option to store multiple literals inside 1 Ifc text object.

If multiple literals share the same `BoxAlignment` then only the first one will be displayed in viewport
to prevent visual clutter (although added small asterisk symbol to indicate that there are still multiple literals).

Added option for aggregated text annotations. Example - "door-tag" symbol.
If you create text, specify "door-tag" symbol in EPset_Annotation.Symbol
\and add two literals to it then they'll automatically will fit to that door-tag in the svg drawing
(https://i.imgur.com/OXaIlWe.png).

The way it works - you define symbol in symbols.svg (or in your own symbols svg file)
and then add the `<text>` fields where text from literals will fit right in.
The imporant note: `<text>` tag must have attribute `data-type="text-template"` for it to work.
For example: `<text text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>`
2023-03-21 17:37:28 +05:00
ArturTomczak 2a0fdd214c update the namespaces to current IDS schema 2023-03-21 20:37:37 +11:00
Dion Moult 45209be0ff You can now easily get a list of FMHEM classes and subtypes to list explicitly in IDS requirements 2023-03-21 17:15:59 +11:00
Dion Moult fcb3640bee IfcTester webapp can now render simple PartOf requirements 2023-03-21 17:13:34 +11:00
Dion Moult d503a4b50b Fix #2883. Regression in text in drawings. 2023-03-21 12:31:32 +11:00
Dion Moult 8bae2984e1 Fix #2886. Typo. 2023-03-21 09:11:30 +11:00
Andrej730 78499cc55d Fixed bugs related to "print all" (#2882) 2023-03-20 15:15:54 +05:00
Dion Moult b6aa5c57c0 Fix bug where copying types with parametric materials could lead to invalidly reused material set items. 2023-03-20 20:40:11 +11:00
Dion Moult d71e34f073 Fix bug where 2D types were not loaded as a fallback from the body representation. 2023-03-20 19:42:54 +11:00
Dion Moult a38d06d7e7 Remove dead code that manually synced drawing references. Now we have an automatic sync_references function instead. 2023-03-20 17:05:43 +11:00
Dion Moult 72b5cc8a44 Minor fix 2023-03-20 17:03:39 +11:00
Dion Moult c97e1079e8 See #2866. Fix issue where annotation should be able to use 3D coordinates (i.e. Model context type) for elevations and sections where it makes more sense. 2023-03-20 16:36:50 +11:00
Carlos Villagrasa 65caa7906d Merge remote-tracking branch 'upstream/v0.7.0' into pset_custom_units_test 2023-03-19 22:33:01 +01:00
Carlos Villagrasa d89a8520a9 Added a test for pset custom IfcUnits 2023-03-19 22:23:36 +01:00
Andres d68356c3a7 Rename class causing unregistering error 2023-03-20 08:21:11 +11:00
Andres cb0e8b6bf2 Add own sublogger for IfcSverchok 2023-03-20 08:21:11 +11:00
Andres b20b99669d Chore: add newline at end of file 2023-03-20 08:21:11 +11:00
Andres d41a277b71 Fix runtime error when unregistering add-on 2023-03-20 08:21:11 +11:00
Andres 33e6a0fdae Update logger to match Sverchok latest 2023-03-20 08:21:11 +11:00
Massimo Fabbro d911d8579d Add qto data file 2023-03-20 08:12:24 +11:00
Massimo Fabbro 05bdc962a3 Now it's possible to see the selected object related cost items and related quantities in qto n-panel 2023-03-20 08:12:24 +11:00
Sigma Dimensions 0e03a01f61 fixing previous commit #ff806dc 2023-03-19 14:11:32 +00:00
Sigma Dimensions ff806dc6e4 improve covering qto calculation 2023-03-19 13:30:35 +00:00
Thomas Krijnen 5fcfbac4a3 Update validate.py 2023-03-19 13:24:27 +01:00
Thomas Krijnen 00b18f67cf #2685 indeterminate upper bound formatting 2023-03-19 12:46:15 +01:00
Dion Moult 088354a96b Fix critical bug where copied representations would be inefficiently or incorrectly replicated. 2023-03-19 22:03:45 +11:00
Dion Moult cfd857233c Fix #2864. Fix bug where element containment was invalidly prioritised over voids and fills in relative placement. 2023-03-19 18:46:34 +11:00
Dion Moult 2816d7f522 Fix bug where deleting the drawing object could lead to orphaned relationships 2023-03-19 18:15:09 +11:00
Dion Moult 3da718a6d1 Fix bug where removing objects could leave orphaned annotation relationships 2023-03-19 15:17:26 +11:00
Dion Moult 558aff72a0 Psets and qtos are now only created when properties exist to prevent invalid IFCs. 2023-03-19 14:49:31 +11:00
Dion Moult bcc399b842 Empty psets are purged to discourage invalid psets with no properties 2023-03-19 13:13:15 +11:00
Andrej730 32e47d03d4 IFC text - box alignment in viewport 2023-03-18 11:56:29 +05:00
Carlos Villagrasa a669c194cf Merge pull request #2869 from cvillagrasa/api_material_fix
Api material fix
2023-03-17 17:04:22 +01:00
Carlos Villagrasa 91f4687eac Revert "Add more parameters to material.add_layer"
This reverts commit 9946117de1.
2023-03-17 17:02:35 +01:00
Andrej730 31f6baaf67 small refactor for Ifc.edit and tool.Ifc
1) replaced IfcStore.edited_objs.add with tool.Ifc.edit
2) updated interface in tool.py
3) removed unlink from tool.Ifc - it was declared there twice
2023-03-17 17:50:56 +05:00
Andrej730 a83c4b2896 Stair, roof, railing - auto updating representation when creating one or update it
So basically there should be no need to manually update representation after you created one of the objects from ifc modifiers.
2023-03-17 17:32:36 +05:00
Andrej730 c685425667 Fixed bugs with text annotations after
1) Bug when new text added without specifying it's font size in class appeared empty;
2) Bug when leader text appeared weirdly bold.
2023-03-17 17:02:42 +05:00
ay-ex 553334d50e add imports to snippet
added imports and syntax highlighting so it is easier to run for newcomers
2023-03-17 20:33:15 +11:00
Andrej730 0b7b0a29a9 Solved redundant spaces in multiline svg text annotations (#2833)
The issue occurs because of svg pretty formatting - indent spaces <text> tag treats as actual spaces. Because of that use some css magic to remove those spaces from <text> tag and assign class to it's children <tspan> tags.

Altough trying to solve this issue I've tried to disable svg pretty format and got empty svg as a result (only templates, styles, no annotations) for some reason.
2023-03-17 11:05:27 +05:00
Andrej730 ae7f834a0f draw_text_annotation refactor
Refactored a code a bit and replaced literal parsing with `text_obj.BIMTextProperties.text` since it's already done beforehand in `tool.Drawing.update_text_value()`.
2023-03-17 11:05:26 +05:00
Andrej730 fe0a98a44f black format 2023-03-16 16:08:58 +05:00
Andrej730 35e748089b Prevent pyrna_enum_to_py warnings on using BIM Tool hotkeys
Type of warning fixed:
WARN (bpy.rna): pyrna_enum_to_py: current value '0' matches no enum in 'BIMModelProperties', '', 'ifc_class'
2023-03-16 16:08:58 +05:00
Andrej730 ff2cda7d18 Small refactor + allowing to preview text in viewport while editing 2023-03-16 16:08:58 +05:00
Andrej730 0d473fa9ac Fixing BoxAlignment value in tests (similar to 9f7db386a) 2023-03-16 16:08:58 +05:00
Andrej730 d8eb765906 BIM Tool - hotkey for editing text annotation
Added hotkey in BIM Tool for editing active text annotation (It's binded on Shift + E).
2023-03-16 16:08:58 +05:00
Carlos Villagrasa fab7b590f7 Allow for custom IfcUnits when editing psets 2023-03-16 20:39:32 +11:00
Andrej730 c9f52d2f8e Option to print all svg drawings #2794
It's working either if you shift click the printing button or supply `print_all = True` argument to `bim.create_drawing operator`.
2023-03-16 11:35:31 +05:00
Andrej730 821993d289 Fixed accidentally removed line in ee1e40171 2023-03-16 10:11:26 +05:00
Andrej730 2d32bc609a Figured a new way to update viewport to preview changes in ifc text 2023-03-16 10:11:26 +05:00
Andrej730 296cec02e8 Removed outdated code from BIMTextProperties 2023-03-16 10:11:26 +05:00
Andrej730 b55ac88c9d Fixing tests after 9f7db386a
TestExportTextLiteralAttributes and TestImportTextAttributes was failing because now we're assuming that BoxAlignment will have 1 of values from the list below and not "BoxAlignment"

"top-left", "top-middle", "top-right", "middle-left", "center", "middle-right", "bottom-left", "bottom-middle", "bottom-right"
2023-03-15 19:43:17 +05:00
Andrej730 c6d3598c52 Fixing tests after e9fdc979d 2023-03-15 19:43:17 +05:00
Andrej730 c4d2b30017 black format 2023-03-15 19:43:17 +05:00
Andrej730 ee1e401719 Ifc Text - drop down menu to switch font size
Added drop down menu to Ifc Text section in object properties where you can change text's size and it will automatically change it in `EPset_Annotation.Classes`. You can also preview it in viewport and it will write in IFC only after you finish editing.

Still need to investigate `annotation.Annotator.resize_text(context.active_object)` in `refreshFontSize`.
2023-03-15 19:43:16 +05:00
Andrej730 3c2ef518e8 IfcText - box alignment interface instead of manually typing text position
realigning box widget
2023-03-15 19:43:16 +05:00
Andrej730 f3e327e088 Added classes description for EPset_Annotation 2023-03-15 19:43:16 +05:00
Andrej730 be1d4e06de IfcText - different text sizes in viewport 2023-03-15 19:43:16 +05:00
Dion Moult 32f5a77d07 New shape utilities for getting top / bottom elevations and bbox. 2023-03-15 18:51:31 +11:00
Dion Moult 9121a6b864 Revert "Fix #2418. Object materials are now found in the materials tab."
This reverts commit 7666463751.
2023-03-15 18:30:11 +11:00
Carlos Villagrasa 9946117de1 Add more parameters to material.add_layer 2023-03-15 08:27:46 +01:00
Carlos Villagrasa ee85055d45 Fix bug in which layer.Material was erased 2023-03-15 08:27:27 +01:00
Andrej730 cce0867f63 IfcText - spacing in multiline text now depends on text size #2833 2023-03-14 19:38:15 +05:00
Andrej730 021321eb65 IfcText - support multiline text in viewport preview 2023-03-14 19:38:15 +05:00
Andrej730 6bd1b84b2b IfcText to appear on top of object properties stack
1) Puts IFC Text only on top of ifc panels to make it easier to change it's text
2) I guess it could be a good practive to set `bl_order = 1` by default so it will be possible later to prioritize something since by default blender sets `bl_order = 0` and then even `bl_order = -1` wouldn't help.
3) May not work in previously created files
2023-03-14 19:38:15 +05:00
Massimo Fabbro f5281a0895 Delete unuseful function 2023-03-14 11:24:37 +11:00
Massimo Fabbro 08fdf6014d weight calculator improvement (see #2859) 2023-03-14 11:24:37 +11:00
Andrej730 94f06d9e18 Another fix for dissolve limit in hiped roof generator... 2023-03-13 17:05:08 +05:00
Andrej730 8405ee4926 Fixed bug dissolve limit in generate hipped roof (e69452883)
Now it will work fine in both edit and object mode
2023-03-13 11:28:36 +05:00
Massimo Fabbro 154c4295df Finish mapping mappable quantities with get_weight 2023-03-13 13:18:02 +11:00
Massimo Fabbro 93519c1c63 Now is possible to calculate an object gross weight 2023-03-13 13:18:02 +11:00
Dion Moult 1250a20e35 Fix #2858. Fix #2856. BlenderBIM Add-on drawings and IfcCSV now support the new quoted and escaped selector capabilities. 2023-03-13 11:09:56 +11:00
Thomas Krijnen 9ec54228f8 Update ci-py-only.yml 2023-03-12 12:01:12 +01:00
Dion Moult 3f94184ace Fix bug where IfcURIReference properties wouldn't display 2023-03-12 21:55:07 +11:00
Thomas Krijnen 88c06f5450 Try different way of invoking ccache 2023-03-12 11:46:25 +01:00
Thomas Krijnen e50045bbed Update ci.yml 2023-03-12 10:06:32 +01:00
Thomas Krijnen 41417a77b9 Update ci-py-only.yml 2023-03-12 10:06:29 +01:00
Thomas Krijnen a5774bd02a Update ci-py-only.yml 2023-03-12 09:40:30 +01:00
Thomas Krijnen c38f48ee62 Try to let pip not install outdated ifopsh 2023-03-12 09:04:33 +01:00
Dion Moult 30c850e37d Fix #2140. Voids and fills can now be added, removed, and edited for non-geometric aggregates. 2023-03-12 12:52:30 +11:00
Dion Moult b6fdbd979e Adding existing objects as openings no longer move to cursor which is more intuitive 2023-03-12 12:52:30 +11:00
Thomas Krijnen 7a8bcbee33 #2685 poly curve test cases 2023-03-11 20:44:59 +01:00
Thomas Krijnen 42d7c7d32c #2685 Recompile rule 2023-03-11 20:44:11 +01:00
Thomas Krijnen c71a5a4d65 #2685 Indexing of simple type aggregates; fix index codegen bug; sizeof of indeterminate; repeat over indeterminate 2023-03-11 20:43:00 +01:00
Andrej730 e69452883f Temporarily commented out dissolve_limited in generate_hipped_roof
Currently it causes an error if you run it in object mode and doesn't really work in edit mode either because you may have no vertices selected. Will fix soon.
2023-03-11 20:47:41 +05:00
Sigma Dimensions 8624888bfc You can now specify custom names for types instead of relying on the default "TYPEX" naming 2023-03-11 14:13:56 +01:00
Andrej730 fad5c7c921 Section annotation - custom markers / toggle displaying the markers
Now it's possible to set custom markers in section annotation.

To do so you need to add BBIM_Section pset to the annotation and then you can either toggle displaying some marker with ShowStartArrow/ShowEndArrow or you can write the id of some custom symbol in StartArrowSymbol/EndArrowSymbol (need to make sure the symbol is included in symbols.svg).

Other changes:

- removed vertex sorting in decoration.py so that start and end points will be the same thing both in svg and in viewport representation (previously the sorting was needed of the direction the arrow in shader wasn't inverted));

- fixed bug when in svg section arrow symbols wouldn't rotate if you move individual verts of the annotation, not the entire object (I guess this bug sneaked in after adapting elevation annotation)
2023-03-11 18:02:16 +05:00
Dion Moult 0dad93bf00 See #2140. You can now bulk unassign aggregates, and they are auto directly contained in any previous indirect containers. 2023-03-11 16:31:23 +11:00
Dion Moult c51aa910cb See #2140. You can now select multiple objects and bulk add them to a single aggregate, and customise the aggregate class. 2023-03-11 16:31:23 +11:00
Andrej730 6d486aeb4d Removed debug print 2023-03-11 10:30:24 +05:00
Dion Moult 7041a4a4a9 Fix #2850. Splitting walls now reassigns voids and fills to the appropriate split portion. 2023-03-11 15:08:26 +11:00
Andrej730 381570cd9e Decorators - a bit more consistent geometry parsing between edit/object modes 2023-03-10 19:26:08 +05:00
Andrej730 c9f93b2316 Section annotation option not to draw full line between the markers
Here's the example - https://i.imgur.com/db09OVF.png

To make it work need to add BBIM_Section property set to the annotation and set "HasConnectedSectionLine" to False (by default it's True).
Also added preview for this feature in viewport.
2023-03-10 19:26:08 +05:00
Dion Moult 3706ecc5f2 Fix #2689. Fully disable wall sketches until we come up with a proper UX for it. 2023-03-10 21:21:03 +11:00
Dion Moult 51ad51036e Generated spaces now remember height and origin represents recalculation point. 2023-03-10 19:48:49 +11:00
Dion Moult 3edbcb9ff8 Fix #2849. Brick.ttl is now bundled. 2023-03-10 17:49:11 +11:00
Dion Moult 3e3c1699da Fix #1047. You can now set offsets to the iterator via Python. 2023-03-10 15:49:45 +11:00
Dion Moult 7666463751 Fix #2418. Object materials are now found in the materials tab. 2023-03-10 15:37:21 +11:00
Dion Moult c532b90c92 Minor fix. 2023-03-10 08:50:25 +11:00
Andrej730 7677733850 small fix for tool.Drawing.get_drawing_metadata
Otherwise it always returned a list with 1 empty string like `[ "" ]` which resulted in messy class names in svg like "-87IfcAnnotation0LtaLiEpzAOxEqmIvCV27EFALLSLOPEANGLE".
2023-03-09 18:50:05 +05:00
Andrej730 972d701801 Fall annotations
Added fall annotation - they appear both in viewport and svg.

There are three types of annotations (SLOPE_ANGLE, SLOPE_FRACTION, SLOPE_PERCENT) you can switch them by changing annotation's ObjectType to one of those values.

By default it uses angle representation.
2023-03-09 18:50:05 +05:00
Dion Moult cdee5532b6 Fix #2838. Bug where invalid 3D entity was used for 2D coordinates. 2023-03-09 21:01:54 +11:00
Dion Moult 38019288de Fix #2839. Bug where undo system could store edits of IFC primitives. 2023-03-09 18:18:56 +11:00
Dion Moult 3e2ca328d2 Get side area lets you choose the side axis now in util.shape. 2023-03-09 17:48:42 +11:00
Dion Moult 93b44b43a5 You can now get all materials including materials in sets via the util module. 2023-03-09 17:48:10 +11:00
Dion Moult 4ebcf8133c Fix #2435. The selector now supports regex filtering. 2023-03-09 11:07:28 +11:00
kzfile 13f8b183cb fix IfcSpace default style 2023-03-08 20:58:49 +01:00
Andrej730 8e5154e65e Minor fix for 6d524b632
Added get_viewport_context to interface too
2023-03-08 19:53:22 +05:00
Andrej730 6d524b632c Small fix for warnings " 'bim.cad_tool' not found for space"
Small fix to allow setting cad_tool even if operators were called not from viewport and to avoid warnings like "Warning: Tool 'bim.cad_tool' not found for space 'PROPERTIES'"
2023-03-08 18:42:36 +05:00
Andrej730 4ad3d65628 UI fixes for railing and roof modifiers
Changes:
1) Added editing railing / roof path as hotkey to bim / cad tools.
2) Added profile decorator
3) Some switching modes and tools for convenience
4) Fixed nasty bug in tool.Blender.apply_bmesh - method wasn't updating current mesh if it was in edit mode.
2023-03-08 18:22:28 +05:00
Dion Moult 4282014bfa Fix #2843. You can now get the bounding box centroid of geometry. 2023-03-08 20:58:21 +11:00
Andrej730 daee666735 Ifc Roof modifier
Added Ifc Roof modifier (also added it to shift-a menu and type manager). It works similar way to railing modifier - you define roof profile and then roof is generated.

Currently it has only 1 type of roof - HIP_ROOF (it utilizes bim.generate_hipped_roof operator).
2023-03-07 18:05:49 +05:00
Andrej730 5a6ae57833 Added ifc operators to some model operators 2023-03-07 09:43:05 +05:00
Andrej730 da8e5dced1 Added test to check #2839 2023-03-06 18:36:29 +05:00
Andrej730 fb38c68c7d Black format after adding railing modifier 2023-03-06 16:01:17 +05:00
Andrej730 2dc3c9b027 Railing - added to type manager 2023-03-06 15:47:55 +05:00
Andrej730 b057312ebf Ifc Railing modifier
Added Ifc Railing modifier (similarly to stair, window, door modifiers...).
You can start working with it either creating some edge based path and adding this modifier or by creating Railing for Shift+A menu (it will create some simple path to start with).

In railing modifier you can switch two edit modes - editing main railing properties and editing path. Editing path will show only the railing path, you can edit it and save it. You cannot edit the railing pass if you're not in this mode.
2023-03-06 15:47:55 +05:00
Thomas Krijnen d1217e61c4 #2822 svg: begin group lazily when data is actually being appended 2023-03-06 09:16:27 +01:00
Thomas Krijnen b521b68fd8 svg: compiler warnings 2023-03-06 09:15:27 +01:00
Thomas Krijnen c9302cde7c svg: prevent garbage data on empty path 2023-03-06 09:14:40 +01:00
Dion Moult 823e3c76eb Fix #2699. Bools and numeric filters are now handled in the selection UI. 2023-03-06 18:10:51 +11:00
Dion Moult 30d6289942 New utility to quickly get the grid north or true north angles. 2023-03-06 17:47:22 +11:00
Dion Moult a43a3d1ecd Fix #2745. You can now quote and escape complex pset names in the selector. 2023-03-06 17:47:03 +11:00
Dion Moult 9e0c5e9244 Fix #2835. Bug where in certain scenarios the decorator would crash due to prematurely freed bmesh. 2023-03-06 15:06:12 +11:00
Dion Moult a8f1df8a50 Fix #2836. Bug where very small lines could accidentally be seen as closed loops due to paperspace rounding. 2023-03-06 12:07:28 +11:00
Dion Moult fba84ecd3c Fix bug where invalid svg embedding was done when building sheets. 2023-03-06 11:52:55 +11:00
Dion Moult 8cc4edf60f Titleblocks now contain north symbol with dynamic angles. 2023-03-06 11:52:39 +11:00
Dion Moult e6eb455d71 Bump 2023-03-04 23:04:05 +11:00
Thomas Krijnen b84782e07f #2810 catch bad positioning warning and empty result 2023-03-03 14:32:02 +01:00
Dion Moult 2e3e0bef12 Minor fix 2023-03-03 22:27:47 +11:00
Dion Moult 51ff2f05bf Fix #2823. Only update colours if you actually change them. 2023-03-03 22:22:47 +11:00
Andrej730 42110e7f63 Added IfcPropertySingleValue to EnablePsetEditing to fix bug
Noticed bug trying to edit "Data" in psets from ifc modifiers and fixed it.

Traceback:
Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\pset\operator.py", line 94, in execute
    self.load_from_pset_data(pset)
  File "\blenderbim\bim\module\pset\operator.py", line 206, in load_from_pset_data
    metadata.set_value(value)
UnboundLocalError: local variable 'value' referenced before assignment
2023-03-03 15:36:14 +05:00
Dion Moult f0733cb148 Minor hotfix 2023-03-03 20:53:10 +11:00
Dion Moult 7383f44f8f See #2645. Update tests to use overridden built-ins. 2023-03-03 18:22:47 +11:00
Dion Moult 311a80955d Fix #2828. Bug where existing "TOP" connections were not removed if you tried to extend to a new slab / roof. Allow generic "top" connections to any LAYER3 element. 2023-03-03 17:59:20 +11:00
Dion Moult 08561b11c3 Fix #2077. You can now edit profiles via their axis line. 2023-03-03 17:47:16 +11:00
Andrej730 47192cc834 Fixed typo in generate_pset_templates.py 2023-03-03 11:08:56 +05:00
Dion Moult 860a0bfc8e Fix #2828. Extending walls to roofs / slabs is now more intuitive as you select the target slab last. 2023-03-03 14:42:08 +11:00
Dion Moult 5677768e99 Fix #2818. Alignment functions now considers layer set usage offsets. 2023-03-03 11:55:49 +11:00
Andrej730 4c58ca28e5 Synchronizing batting viewport and svg dimensions
Also switched to even more simple pattern to indicate insulation because of the vertex limit
2023-03-02 19:00:23 +05:00
Dion Moult 588d6ccaaa Attempt 2. 2023-03-02 21:51:09 +11:00
Dion Moult 21f583800a Ensure Python 3.10 is used to fetch BlenderBIM Add-on dependencies 2023-03-02 21:43:22 +11:00
Andrej730 4ea08a087c Minor array refactor 2023-03-02 14:39:36 +05:00
Dion Moult e5885db187 Get batting thickness from decorator data 2023-03-02 19:02:00 +11:00
Andrej730 1fca38625f A last bit of cleaning up temp files after tests 2023-03-02 10:45:00 +05:00
Sigma Dimensions cf2ad43ed7 refactor pset operator 2023-03-01 15:37:19 +01:00
Andrej730 fb3023bb79 Moved blenderbim tests temporary files to separate directory
Moved temporary test files to separate directory and added it to gitignore

Still have test.exp.cache.dat spawning from `test_feature`...
2023-03-01 18:46:13 +05:00
Andrej730 52f18fea85 Viewport preview for batting annotation
Currently it's always set to the same thickness and doesn't update from pset value.

It's currently using zigzag pattern to indicate batting because some techincal limitation of the shader (Error: C6033: Hardware limitation reached, can only emit 256 vertices of this size)

Also added batting to the annotation tool panel.
2023-03-01 17:37:17 +05:00
Dion Moult d5ecb9b8ea See #2820. Fix issue where some hatches weren't merged when an object had multiple SVG paths. 2023-03-01 22:25:12 +11:00
Dion Moult 67d33001d5 See #2820. Ensure only closed polygons are merged in drawings. 2023-03-01 21:57:33 +11:00
Dion Moult 4f571fa2fc Attempt to fix pytest bundling. 2023-03-01 21:39:15 +11:00
Dion Moult c5bd902dc6 See #2820. See #2822. Fix to accommodate empty element groups in drawings. 2023-03-01 21:08:34 +11:00
Dion Moult 68282c7a1e Fix #2821. 2023-03-01 20:54:07 +11:00
Sigma Dimensions d40dcae212 fix 5d csv imports with funky characters and numbers, fix 5d export 2023-03-01 10:51:52 +01:00
Dion Moult 12151c40ff Fix #2799. Drawing polygon merging now is purely material based. 2023-03-01 17:52:57 +11:00
Dion Moult 9c8b15fa1a Fix #2645. No longer override built-in operators for critical features. 2023-03-01 14:30:56 +11:00
Dion Moult 3d49cc9d13 By default, the site is now cut with a nice thick line on drawings 2023-03-01 11:28:45 +11:00
Dion Moult 955c0d19a6 See #2799. Cut walls with the same material in drawings are now merged by default. 2023-02-28 22:59:45 +11:00
Andrej730 3cf7a9e925 Renamed batting pset 2023-02-28 11:24:47 +05:00
Andrej730 8cbf719772 Replaced tool method from 17add48 with existing one 2023-02-28 10:04:18 +05:00
Dion Moult 2f614330c9 Fix #2781. Usability bug where it wasn't clear references were only loaded from the active external classification library. 2023-02-28 12:39:15 +11:00
Dion Moult 5a40519114 Minor fix 2023-02-28 11:43:36 +11:00
Dion Moult 96271e8bd8 Users can no longer create orphaned opening elements, which are invalid. 2023-02-28 10:05:22 +11:00
Dion Moult 9b75a415d4 See #2812. Resolve issue where obsolete link_to_scene argument was used. 2023-02-28 10:03:56 +11:00
Dion Moult 5f71abb5fa Minor fix to insulation scale 2023-02-28 09:23:28 +11:00
Dion Moult affb066b7b Fix #2813. Bug where the user could try to load thumbnails even though there were no types. 2023-02-28 08:44:36 +11:00
Andrej730 17add48254 Minor refactor for modifiers 2023-02-27 19:18:39 +05:00
Andrej730 f5b6fdcb5b Batting Annotation pattern
Added batting pattern to line annotations.
The way to use it is:
1) Add "Line" Annotation;
2) Change object type to "BATTING"
3) Add object property set "Pset_BBIM_Bating" (also added in this commit) to specify the batting thickness.

Currently there is no preview for batting in viewport.

Other changes:
- EPset_Annotation.ifc -> Psets_BBIM_Annotation.ifc (more general psets ifc library for the future use)
2023-02-27 19:18:39 +05:00
Dion Moult 68bdbe4c4f Bundle bpypolyskel so that users can start playing with the hipped roof generator 2023-02-27 21:49:04 +11:00
Dion Moult 363ddc2a9b Minor fix 2023-02-27 20:49:45 +11:00
Dion Moult 20d44378d9 You can now generate type-based spaces and regenerate spaces when boundaries change. 2023-02-27 20:49:38 +11:00
Dion Moult 4fe321275a Fix bug where space types were not loaded. 2023-02-27 20:47:20 +11:00
Dion Moult 60b1192703 Fix critical bug where a user might override and break existing objects if they reused an object name 2023-02-27 20:46:53 +11:00
Dion Moult b25233a748 Fix critical bug where duplicate orphaned collections were created in the background. 2023-02-27 20:12:39 +11:00
Dion Moult d9b54c923c Fix bug where you couldn't manage spatial types in the type manager 2023-02-27 20:10:27 +11:00
Dion Moult 131ee8270b Minor fix 2023-02-27 10:57:28 +11:00
Massimo Fabbro e71bace6a8 update makefile with shapely 2.0.1 2023-02-27 09:10:46 +11:00
Dion Moult 2c4a0669ed Fix bug where copying to container might get the wrong coordinates if containers weren't synced. 2023-02-26 22:31:21 +11:00
Dion Moult ee451b6b29 Experimental operator to generate hipped roofs from a footprint. 2023-02-26 21:23:35 +11:00
Dion Moult 7b858fffb2 All generated drawing text now has classes so you can customise their style. 2023-02-26 21:22:43 +11:00
Dion Moult 6ce0deeb63 Fix #2808. Bug where you couldn't assign element types in IFC4X3. 2023-02-26 11:26:13 +11:00
Dion Moult 96b2eaeae4 Experimental operator to generate spaces bounded by walls. 2023-02-25 21:40:02 +11:00
Andrej730 8fe8175a87 Monolithic stair variant generator 2023-02-25 10:40:27 +05:00
Andrej730 ee2bedfb64 Update openings position based on fillings position at wall regenarete
So basically now when you move windows or doors attached to the wall, their openings positions will also be updated if you hit "regenerate" for the wall.
2023-02-24 17:12:41 +05:00
Andrej730 aa8da74ed8 ifc2x3 pset library name fix + copyrights 2023-02-24 11:07:33 +05:00
Dion Moult f2c03a508f Fix #2600. Link back to exact docs folder in repo. 2023-02-24 16:21:21 +11:00
Dion Moult aa172488b0 Fix #2594. Document potential lag in PyPI releases of C++ core. 2023-02-24 13:35:46 +11:00
Dion Moult ae09adf854 Fix #2424. Bug where indirect assets (e.g. importing a type using a material) with relationships had their relationships duplicated 2023-02-24 13:01:26 +11:00
Andrej730 5b5dd73684 get_inverse_attributes - added more correct recursive unpacking for aggregates and selections
Not affected the result schemas though
2023-02-23 18:56:24 +05:00
Andrej730 3849a87adc Added simple tests for door and window modifier
This thing used to result in errors before we fixed `replace_object_ifc_representation`
2023-02-23 18:38:20 +05:00
Andrej730 708dc6aca2 IFC4X3/IFC2X3 pset templates library
Generated from xmls from the ISO release of IFC4X3.
Based on https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/annex/annex-a/general-usage/IFC4_ADD2.ifc

Also generated pset templates library for IFC2X3 (also based on similar xmls) but IFC2X3 ifc pset library is using... IFC4X3 schema because IfcPropertySetTemplate and IfcRelDeclares are not available in IFC2X3. Which is okay for our purposes for now.

Will need to update IFC4X3 library if one of the issues will be resolved:
https://github.com/buildingSMART/IFC4.3.x-development/issues/587
https://github.com/buildingSMART/IFC4.3.x-development/issues/586
2023-02-23 18:17:14 +05:00
Dion Moult 2fe2c349fd Fix #2743. obj2ifc scripts updated to support multiple OBJ files. 2023-02-23 22:17:34 +11:00
Dion Moult d396d32a8f You can now extend beams to walls. 2023-02-23 18:33:31 +11:00
Dion Moult 1fd02755f8 Fix BCF module bugs where certain BCF entities were assumed to exist. 2023-02-23 18:32:44 +11:00
Dion Moult 79d92b511b Fix #2442. Fix bug where viewboxes were not clipped when compiling drawing sheets. 2023-02-23 13:16:33 +11:00
Andrej730 ff7d93e66c Undo tests - proof of concept
Created some simple undo tests.

This "flip wall and ctrl-z" test actually is crashing blender if I do it manually - so I guess undo in background mode is still kind of different.
2023-02-22 20:24:43 +05:00
Dion Moult 68a8c7a235 Add basic integration tests for modeling slabs and beams 2023-02-22 21:50:12 +11:00
Dion Moult 5e744ff7d1 Add integration tests for basic wall modeling features 2023-02-22 20:38:02 +11:00
Dion Moult 921a185ffa Fix bug where natively loaded faceted breps didn't respect false origins. 2023-02-22 16:32:32 +11:00
Dion Moult 855751f026 Fix #2767. Removing representations now cleans up after orphaned Blender mesh data. 2023-02-22 16:24:24 +11:00
Sigma Dimensions 255c2efe98 Fix sequence data loading for unnamed items, ref #2783 2023-02-21 22:55:44 +01:00
Sigma Dimensions 892b282054 Fix loading task properties when calendar doesn't have a name, ref issue #2783 2023-02-21 20:23:07 +01:00
Dion Moult 26efc6b4e7 Fix bug where BIM tool hotkeys could activate operations without being in a correct state. 2023-02-21 21:54:53 +11:00
Dion Moult a0618c5827 Fix #2769. Bug where parametric UI was hardcoded to only work for walls. Now works on any generic parametric vertical layers. 2023-02-21 21:41:52 +11:00
Andrej730 d4bbb6e2c7 Added Model/Profile/Elevation_VIEW context as default for new projects #2784
Default Model/Profile/Elevation_VIEW context could be helpful to make openings created with BIM tool more correct.
2023-02-21 11:46:05 +05:00
Andrej730 66b7692227 Fixed windows/openings bugs
1) Window bug (#2792) - lining_offset wasn't converted from ifc units during window properties editing
2) Openings bug - OveallWidth/OverallHeight based openings was converted to ifc units even though their dimensions were already in ifc units.
2023-02-21 11:20:36 +05:00
Dion Moult 2429f507ad Fix #2785. Fix bug where invalid models from Revit that reused material usages could create invalid models. 2023-02-21 13:48:24 +11:00
Dion Moult 03a66d2806 Fix #2787. Fix creating invalid orphaned property and group relationships when removing systems / groups / elements. 2023-02-21 10:59:47 +11:00
Dion Moult 9d8551cb48 Demo library now has profile names to follow native IFC best practices 2023-02-21 10:14:06 +11:00
Dion Moult 2ca0b52c6a Fix failing test when editing a material profile with an unnamed profile 2023-02-21 09:51:30 +11:00
Massimo Fabbro 9cc754488b calculate all quantities smoke test 2023-02-21 09:12:30 +11:00
Massimo Fabbro f81ceb53cf add qto feature test 2023-02-21 09:12:30 +11:00
Dion Moult d1d0b5724d Revert "Solving test fail - keeping unnamed profiles when updating profiles in MaterialsData"
This reverts commit 0e1689c2d9.
2023-02-21 08:51:44 +11:00
Sigma Dimensions 40560b71c2 BBIM: fix material errors, and add smoke tests 2023-02-20 17:49:54 +01:00
Andrej730 378a25bbaa Added some .feature convenience debug fixtures 2023-02-20 19:57:05 +05:00
Andrej730 1dfabe2591 Fixed issue with float vectors assertion during tests
`sequence.feature` - Added `[:]` to make errors more readable values in errors (for example, (0.20000000298023224, 0.20000000298023224, 0.20000000298023224, 1.0) instead of `<bpy_float[4], Object.color>`

`test_feature.py` - fixed `FAILED test/bim/test_feature.py::test_animate_the_consumption_of_a_wall - AssertionError: Value is <bpy_float[4], Object.color>`
Test was failing because of float garbage in object.color (the comparison was (0.20000000298023224, 0.20000000298023224, 0.20000000298023224, 1.0) <> [0.2, 0.2, 0.2, 1] )

After that fix all tests now are passed 🥲
2023-02-20 19:53:16 +05:00
Andrej730 0e1689c2d9 Solving test fail - keeping unnamed profiles when updating profiles in MaterialsData
Solves test_enable_editing_material_set_item test error
```
FAILED test/bim/test_feature.py::test_enable_editing_material_set_item - RuntimeError: Error: Python: Traceback (most recent call last).

RuntimeError: Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\material\operator.py", line 587, in execute
    self.mprops.profiles = str(material_set_item.Profile.id())
TypeError: bpy_struct: item.attr = val: enum "107" not found in ('79', '98')
Location: C:\software\Steam\steamapps\common\Blender\3.4\scripts\modules\bpy\ops.py:113
```

The problem was `bim.enable_editing_material_set_item"` was trying to set `MaterialsData` profiles and wasn't able to set unnamed profile because `MaterialsData.profiles()` was discarding unnamed profiles (which is seems contradictory because `profiles()` is also handling unnamed profiles anyway).
2023-02-20 18:00:48 +05:00
Sigma Dimensions bd071cc472 Add smoke test for editing material profiles 2023-02-20 11:08:15 +01:00
Dion Moult dde847cb5a IfcTester can now output reports in BCF 2023-02-20 17:50:36 +11:00
Dion Moult 3c17718f70 Fix bug where adding a BCF viewpoint didn't take into account project units 2023-02-20 17:50:36 +11:00
Andrej730 0445119e77 Shifting dimensions defined openings by min_x and min_z to prevent #2784 2023-02-20 11:22:02 +05:00
Dion Moult e82cb5c149 IfcTester can now create reports in ODS format 2023-02-20 17:05:07 +11:00
Dion Moult 984548f315 IfcTester can now output reports in plain text 2023-02-20 17:04:38 +11:00
Dion Moult a0be4f58e6 Fix #2780. Fix bug where validation couldn't be run. pytest is now bundled. 2023-02-20 11:26:09 +11:00
Dion Moult 147602e107 Fix bug where ifcopenshell express rule validator required a logger with set_state. 2023-02-20 11:26:09 +11:00
Andrea Ghensi ac81169290 build(bcf): remove conda dependency 2023-02-20 07:26:34 +11:00
Dion Moult 1d78e05370 Fix #2774. Bump IfcOpenShell. 2023-02-19 21:53:49 +11:00
Andrej730 4b15b02ac8 Fixed Door modifier initial opening problem #2784
- add check of OverallHeight and OverallWidth for creating openings for doors and windows;
- OverallHeight, OverallWidth update for all the occurences of the type for doors and windows;
- new tool.Ifc method - get_all_element_occurences
2023-02-18 17:11:04 +05:00
Thomas Krijnen 57d18a0644 Update IfcIShapeProfileDef.cpp 2023-02-18 13:00:45 +01:00
Dion Moult 2f73af7e6a Fix bug where BlenderBIM Add-on BCF module was completely broken. Now works with latest BCF library for BCF-XML v2. 2023-02-18 21:12:36 +11:00
Dion Moult e712ef344d New feature in BCF to allow extracting specific files like header files, snippets, or bitmaps. 2023-02-18 21:11:05 +11:00
Dion Moult f91e9a3119 Fix BCF bug where viewpoints were not created with a .bcfv suffix which is not compliant. 2023-02-18 21:10:27 +11:00
Dion Moult 9cf36f1d91 Fix BCF bug where BCFv2 files were unable to be loaded 2023-02-18 21:09:32 +11:00
Andrej730 ab9a4856ab Update entity-types mapping for ifc4
Added IfcWindowStyle and IfcDoorStyle for suitable StandartCases since StandartCases inherit from IfcDoor and IfcWindow.
2023-02-17 17:57:17 +05:00
Andrej730 d52618277e Ifc4x3 - entity-types mapping
- added mapping entity-types mapping based on iso release of ifc4x3

- added some manual corrections for `generate_util_type_json.py` if someone would regenerate the map for ifc4.

- manual correction in ifc4x3 mapping for IfcCivilElement. It could be removed in the future releases of IFC if https://github.com/buildingSMART/IFC4.3.x-development/issues/583 is resolved

If curious to look what were the changes for mapping between ifc4 and ifc4x3:
https://github.com/IfcOpenShell/IfcOpenShell/commit/da51253fb0a9d08aa93f5fe3c074d3592b3aa997#diff-98f525c91bb47b9820e603fba61cc015d3e6ae7261593d727c51114e3ff73aa7
2023-02-17 17:57:16 +05:00
CyrilWaechter 743bd2c48f Add buttons to selected related boundaries
Related by a common:
- BuildingElement
- BuildingElementType
- Space
- SpaceType
2023-02-17 13:21:24 +01:00
CyrilWaechter 11894de455 Add button space to select its boundaries 2023-02-17 10:43:48 +01:00
Dion Moult 0c8122afc4 See #2762. Final final attempt. 2023-02-17 13:28:02 +11:00
Dion Moult f893503d0c See #2762. Final attempt. 2023-02-17 13:12:09 +11:00
Dion Moult 504782f156 See #2762. Attempt 3. 2023-02-17 12:53:55 +11:00
Dion Moult 5d1f32bfc0 See #2762. 2023-02-17 12:28:19 +11:00
Dion Moult 5bc315be92 See #2762. 2023-02-17 12:27:31 +11:00
Dion Moult e8f21c79b1 See #2762. 2023-02-17 12:03:25 +11:00
Sigma Dimensions ba24f9b491 - Improve ODS export
- Fix XLSX export bug
- Improve active cost item UI active cost item
- Fix missing tool cost function
2023-02-16 20:22:45 +01:00
Andrej730 937fa21cce Fix to prevent pyrna_enum_to_py warnings using BIM Tool
Prevents couple `pyrna_enum_to_py` warnings that occured using BIM Tool

```
WARN (bpy.rna): C:\Users\blender\git\blender-v340\blender.git\source\blender\python\intern\bpy_rna.c:1341 pyrna_enum_to_py: current value '0' matches no enum in 'BIMModelProperties', '', 'ifc_class_browser'
WARN (bpy.rna): C:\Users\blender\git\blender-v340\blender.git\source\blender\python\intern\bpy_rna.c:1341 pyrna_enum_to_py: current value '0' matches no enum in 'BIMModelProperties', '', 'relating_type_id'
WARN (bpy.rna): C:\Users\blender\git\blender-v340\blender.git\source\blender\python\intern\bpy_rna.c:1341 pyrna_enum_to_py: current value '0' matches no enum in 'BIMModelProperties', '', 'ifc_class'
```
2023-02-16 17:43:34 +05:00
Andrej730 16cbab2de7 Commented how to regenerate BIM Tool keymap in case of errors 2023-02-16 16:40:07 +05:00
Andrej730 a85f81d8ca Refactor - moved some helpers functions to tool 2023-02-16 15:16:42 +05:00
Andrej730 1617ede010 Simplified addings hotkeys descriptions in BIM Tool
Now it's more universal and doesn't require some single operator to be attached to hotkey description.
Also added description for some hotkeys
2023-02-16 14:36:24 +05:00
Dion Moult d9f1d501aa See #2762. Try again. 2023-02-16 15:01:19 +11:00
Sigma Dimensions 8ee60db0df Re-assigning a product type class should also reassign the class of its typed instances 2023-02-15 23:11:30 +01:00
Sigma Dimensions d2e02c8ab3 complete cost schedule exports commit 9dc69f3 2023-02-15 23:09:15 +01:00
Sigma Dimensions 2d4e4d2445 forgotten commit for duplicating tasks 2023-02-15 23:00:43 +01:00
Sigma Dimensions 9dc69f3aa7 You can now export Cost Schedules as CSV, ODS, XSLX with BlenderBIM or using the cli app 2023-02-15 22:50:39 +01:00
Andrej730 476ab506d0 Added ifc4x3 json schema and it's parser
API for docs works the same way as it worked for ifc2x3 and ifc4.

Besides changing `doc.py` and adding `.json` schemas added `ifc4x3dev_scrape_data_for_docs.py` (it's kind of hacky now and relies on `server.py` code from the further mentioned repo) that's used to parse some entities descriptions from https://github.com/buildingSMART/IFC4.3.x-development for later use in `doc.py`.
2023-02-15 18:38:04 +05:00
CyrilWaechter a6fa6201f7 Fix bugs introduced in cf87852 2023-02-15 10:34:25 +01:00
Dion Moult 26f7db0504 Fix #2698. IfcClash results are now aware of multiple linked IFC models. 2023-02-15 20:32:50 +11:00
Dion Moult 77a0da2a17 See #2762. Another attempt at fixing PyPI build. 2023-02-15 20:06:07 +11:00
283 changed files with 48107 additions and 3541 deletions
+104
View File
@@ -0,0 +1,104 @@
import pathlib
import shutil
import requests
import zipfile
import os
# To test this locally, set these environment variables
REPO_OWNER = os.environ.get("REPO_OWNER", "IfcOpenShell/IfcOpenShell")
MY_WORKFLOW = os.environ.get("MY_WORKFLOW", "ci-ifcopenshell-conda-daily")
WORKFLOW_RUN_ID = os.environ.get("WORKFLOW_RUN_ID", None)
TOKEN = os.environ.get("GITHUB_TOKEN", None)
# To test this locally create a fine-grained personal access token for this repo with permissions "actions:read"
# See https://github.com/settings/tokens?type=beta
# This is a list of strings that indicate that a job has stopped abruptly.
SIGNS_OF_STOPPAGE = [
"Error: The operation was canceled.",
"fatal error C1060: compiler is out of heap space",
]
def start_request_session():
s = requests.Session()
headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
if TOKEN:
headers["Authorization"] = f"Bearer {TOKEN}"
s.headers = headers
return s
def get_ci_run(repo_name, run_id):
url = f"https://api.github.com/repos/{repo_name}/actions/runs/{run_id}"
s = start_request_session()
response = s.get(url)
return response.json()
def evaluate_log_file_for_abrupt_stop(log_file):
with open(log_file) as f:
for i, line in enumerate(f):
for sign in SIGNS_OF_STOPPAGE:
if sign in line:
print(f"Found sign of abrupt stoppage in [line {i}]: '{sign}'")
return True
return False
def get_ci_specific_run_specific_failure_details(repo_name, run_id):
url = f"https://api.github.com/repos/{repo_name}/actions/runs/{run_id}/attempts/1/logs"
s = start_request_session()
response = s.get(url)
# SAVE zip file
with open("logs.zip", "wb") as f:
f.write(response.content)
# unzip file
with zipfile.ZipFile("logs.zip", "r") as zip_ref:
zip_ref.extractall("logs")
failed_logs = []
for file in pathlib.Path("logs").iterdir():
if file.is_dir():
continue
if evaluate_log_file_for_abrupt_stop(file):
failed_logs.append(file)
return failed_logs
def restart_job(repo_name, job_id):
url = f"https://api.github.com/repos/{repo_name}/actions/runs/{job_id}/rerun-failed-jobs"
s = start_request_session()
response = s.post(url)
return response.json()
def eval_jobs():
run = get_ci_run(REPO_OWNER, WORKFLOW_RUN_ID)
if run["run_attempt"] > 1:
print("This is not the first attempt. Exiting")
return
failed_logs = get_ci_specific_run_specific_failure_details(REPO_OWNER, WORKFLOW_RUN_ID)
if len(failed_logs) == 0:
print("No runs exhibit signs of abrupt stoppage")
return
print("restarting job", WORKFLOW_RUN_ID)
r = restart_job(REPO_OWNER, WORKFLOW_RUN_ID)
print(r)
shutil.rmtree("logs")
os.remove("logs.zip")
if __name__ == "__main__":
eval_jobs()
@@ -62,6 +62,7 @@ jobs:
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.10'
- run: echo ${{ env.DATE }}
- name: Get current date
id: date
+5 -1
View File
@@ -8,7 +8,11 @@ on:
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "49 23 13 * *" # 11min before utc midnight
- cron: "0 0 18 * *"
push:
paths:
- '.github/workflows/ci-ifcopenshell-pypi.yml'
env:
major: 0
+8 -7
View File
@@ -7,7 +7,7 @@ on:
- 'test/**'
- 'conda/**'
- 'cmake/**'
- '.github/workflows/ci_py_only.yml'
- '.github/workflows/ci-py-only.yml'
pull_request:
jobs:
@@ -46,13 +46,14 @@ jobs:
- name: ccache
uses: hendrikmuhs/ccache-action@v1
with:
key: ${GITHUB_WORKFLOW}
- name: Build ifcopenshell
run: |
sudo /usr/sbin/update-ccache-symlinks
mkdir build && cd build
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
PATH="/usr/lib/ccache:$PATH" cmake \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
@@ -62,7 +63,7 @@ jobs:
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4" \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add1" \
-DBUILD_CONVERT=Off \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
@@ -79,8 +80,8 @@ jobs:
- name: Install Python dependencies
run: |
sudo /usr/bin/python -m pip install -U pip
sudo /usr/bin/python -m pip install xmlschema numpy lxml
sudo /usr/bin/python -m pip install src/bcf
sudo /usr/bin/python -m pip install xmlschema xsdata numpy lxml
sudo /usr/bin/python -m pip install src/bcf --no-deps
sudo /usr/bin/python -m pip install pytest
sudo /usr/bin/python -m pip install isodate
sudo /usr/bin/python -m pip install lark
@@ -0,0 +1,34 @@
name: Restart failed daily conda builds
on:
workflow_run:
workflows: [ ci-ifcopenshell-conda-daily ]
types:
- completed
env:
REPO_OWNER: IfcOpenShell/IfcOpenShell
MY_WORKFLOW: ci-ifcopenshell-conda-daily
jobs:
restart-failed-runs:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- name: checkout
uses: actions/checkout@v3
- name: Use python 3.11
uses: actions/setup-python@v2
with:
python-version: 3.11
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Check for failed runs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
run: |
python check_repo_ci_jobs.py
working-directory: .github/workflows
+5 -3
View File
@@ -57,6 +57,8 @@ jobs:
- name: ccache
uses: hendrikmuhs/ccache-action@v1
with:
key: ${GITHUB_WORKFLOW}
- name: Build ifcopenshell
run: |
@@ -73,7 +75,7 @@ jobs:
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4;4x3" \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add1" \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
@@ -103,8 +105,8 @@ jobs:
- name: Install Python dependencies
run: |
sudo /usr/bin/python -m pip install -U pip
sudo /usr/bin/python -m pip install xmlschema numpy lxml
sudo /usr/bin/python -m pip install src/bcf
sudo /usr/bin/python -m pip install xmlschema xsdata numpy lxml
sudo /usr/bin/python -m pip install src/bcf --no-deps
sudo /usr/bin/python -m pip install pytest
sudo /usr/bin/python -m pip install isodate
sudo /usr/bin/python -m pip install lark
+3
View File
@@ -84,6 +84,9 @@ src/ifcopenshell-python/test/build
# blenderbim libs
src/blenderbim/blenderbim/libs
# blenderbim test temp files
src/blenderbim/test/files/temp
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
+1 -1
View File
@@ -1017,7 +1017,7 @@ endforeach(COMPONENT)
set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3.8 ${BOOST_DEPS}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3.8, python3-pytest ${BOOST_DEPS}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION_SUMMARY "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
+1 -3
View File
@@ -1,8 +1,6 @@
{% set name = "ifcopenshell" %}
{% set version = "0.7.0" %}
{% set occt_version = "7.6.2" %}
{% set cgal_cpp_version = "5.3" %}
{% set hdf5_version = "1.12.1" %}
package:
name: {{ name }}
+2 -22
View File
@@ -1,31 +1,11 @@
.PHONY: test
test:
.PHONY: ci
ci:
tox
.PHONY: qa
qa:
black .
isort .
pylint ./src --output-format=colorized
.PHONY: typecheck
typecheck:
mypy src/bcf
.PHONY: license
license:
copyright-header --license LGPL3 --copyright-holder "Andrea Ghensi <andrea.ghensi@gmail.com>" --copyright-year "2022" --copyright-software "IfcOpenShell" --copyright-software-description "BCF XML file handling" -a ./ -o ./
.PHONY: coverage
coverage:
coverage run --source bcf -m pytest tests
coverage html
xdg-open htmlcov/index.html
.PHONY: clean
clean:
rm -rf htmlcov
# TODO: make this based on xsd file presence
.PHONY: models
models:
+1 -1
View File
@@ -27,7 +27,7 @@ with load("/path/to/file.bcf") as bcfxml:
# Get a dictionary of topics
topics = bcfxml.topics
for topic_handler in bcfxml.topics:
for topic_guid, topic_handler in bcfxml.topics.items():
topic = topic_handler.topic
print("Topic guid is", topic.guid)
print("Topic title is", topic.title)
-7
View File
@@ -1,7 +0,0 @@
name: bcf-client
channels:
- conda-forge
dependencies:
- ifcopenshell
- xsdata
- numpy
+22 -7
View File
@@ -15,6 +15,7 @@ keywords = ["IFC", "BCF", "BIM", "eingineering"]
dependencies = [
"xsdata",
"numpy",
"ifcopenshell",
]
version = "0.0.1"
classifiers = [
@@ -58,20 +59,34 @@ exclude_lines = [
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py310
isolated_build = true
env_list = lint, type, py3{10,11}
skip_missing_interpreters = true
requires = tox-conda
[testenv]
conda_deps =
deps =
pytest
pytest-cov
coverage
conda_channels =
conda-forge
conda_env = environment.yml
commands = pytest --cov --cov-report=term tests
[testenv:lint]
description = run linters
skip_install = true
deps =
black
isort
pylint
commands =
black {posargs:.}
isort {posargs:.}
pylint {posargs:.} --output-format=colorized
[testenv:type]
description = run type checks
deps =
mypy>=0.991
commands =
- mypy {posargs:src}
"""
[tool.mypy]
-1
View File
@@ -4,4 +4,3 @@ pylint
isort
xsdata
tox==3.27.1
tox-conda
+6 -2
View File
@@ -23,8 +23,9 @@ from pathlib import Path
from typing import Optional, Union
from bcf.v2.bcfxml import BcfXml as BcfXml2
from bcf.v2.model import Version as Version2
from bcf.v3.bcfxml import BcfXml as BcfXml3
from bcf.v3.model import Version
from bcf.v3.model import Version as Version3
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
@@ -65,5 +66,8 @@ def _get_version(filepath: Union[str, Path], xml_handler: Optional[AbstractXmlPa
"""
xml_handler = xml_handler or XmlParserSerializer()
with zipfile.ZipFile(filepath) as bcf_zip:
version = xml_handler.parse(bcf_zip.read("bcf.version"), Version)
try:
version = xml_handler.parse(bcf_zip.read("bcf.version"), Version3)
except:
version = xml_handler.parse(bcf_zip.read("bcf.version"), Version2)
return version.version_id
+55 -7
View File
@@ -1,5 +1,6 @@
"""BCF XML V2 Topic handler."""
import datetime
import tempfile
import uuid
import zipfile
from pathlib import Path
@@ -218,9 +219,56 @@ class TopicHandler:
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
destination_zip.writestr(real_path.at, self.document_references[doc.referenced_document])
def add_viewpoint(self, element: entity_instance) -> None:
def extract_file(self, entity, outfile: Optional[Path] = None) -> Path:
"""Extracts an element with a file into a temporary directory
These include header files, bim snippets, document references, and
viewpoint bitmaps. External reference are not downloaded. Instead, the
URI reference is returned.
:param entity: The entity with a file reference to extract
:type entity: bcf.v2.model.HeaderFile,bcf.v2.model.BimSnippet,bcf.v2.model.TopicDocumentReference
:param outfile: If provided, save the header file to that location.
Otherwise, a temporary directory is created and the filename is
derived from the header's original filename.
:type outfile: pathlib.Path,optional
:return: The filepath of the extracted file. It may be a URL if the
header file is external.
:rtype: Path
"""
Add a viewpoint tergeting an IFC element to the topic.
if hasattr(entity, "reference"):
reference = entity.reference
else:
reference = entity.referenced_document
if not reference:
return
if getattr(entity, "is_external", False):
return entity.reference
resolved_reference = self._topic_dir
for part in Path(reference).parts:
if part == "..":
resolved_reference = resolved_reference.parent
else:
resolved_reference = resolved_reference.joinpath(part)
if not outfile:
if getattr(entity, "filename", None):
filename = entity.filename
else:
filename = resolved_reference.name
outfile = Path(tempfile.mkdtemp()) / filename
with open(outfile, "wb") as f:
f.write(resolved_reference.read_bytes())
return outfile
def add_viewpoint(self, element: entity_instance) -> None:
"""Add a viewpoint pointed at the placement of an IFC element to the topic.
Args:
element: The IFC element.
@@ -229,11 +277,11 @@ class TopicHandler:
self.add_visinfo_handler(new_viewpoint)
def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float_], *guids: str) -> None:
"""
Add a viewpoint tergeting an IFC element to the topic.
"""Add a viewpoint pointing at an XYZ point in space
Args:
element: The IFC element.
position: the XYZ point in space
guids: one or more element GlobalIds.
"""
vi_handler = VisualizationInfoHandler.create_from_point_and_guids(
position, *guids, xml_handler=self._xml_handler
@@ -241,8 +289,8 @@ class TopicHandler:
self.add_visinfo_handler(vi_handler)
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
self.viewpoints[new_viewpoint.guid] = new_viewpoint
self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
self.viewpoints[new_viewpoint.guid + ".bcfv"] = new_viewpoint
self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid + ".bcfv", guid=new_viewpoint.guid))
def __eq__(self, other: object) -> bool | NoReturn:
return (
+8 -2
View File
@@ -5,7 +5,8 @@ from typing import Any, Iterable, Optional
import numpy as np
from ifcopenshell import entity_instance
from ifcopenshell.util import placement
import ifcopenshell.util.unit
import ifcopenshell.util.placement
from numpy.typing import NDArray
import bcf.v2.model as mdl
@@ -209,7 +210,12 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
Returns:
The BCF viewpoint definition.
"""
elem_placement = placement.get_local_placement(element.ObjectPlacement)
ifc_file = element.wrapped_data.file
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
elem_placement[0][3] *= unit_scale
elem_placement[1][3] *= unit_scale
elem_placement[2][3] *= unit_scale
return mdl.VisualizationInfo(
guid=str(uuid.uuid4()),
+4 -2
View File
@@ -187,10 +187,12 @@ class TopicHandler:
self.add_visinfo_handler(vi_handler)
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
self.viewpoints[new_viewpoint.guid] = new_viewpoint
self.viewpoints[new_viewpoint.guid + ".bcfv"] = new_viewpoint
if self.topic.viewpoints is None:
self.topic.viewpoints = mdl.TopicViewpoints()
self.topic.viewpoints.view_point.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
self.topic.viewpoints.view_point.append(
mdl.ViewPoint(viewpoint=new_viewpoint.guid + ".bcfv", guid=new_viewpoint.guid)
)
def __eq__(self, other: object) -> bool | NoReturn:
return (
+1 -1
View File
@@ -15,7 +15,7 @@ def test_create_clash_set_bcf() -> None:
assert len(topic.viewpoints) == 1
guid, vi_handler = next((k, v) for k, v in topic.viewpoints.items())
v_info = vi_handler.visualization_info
assert v_info.guid == guid
assert f"{v_info.guid}.bcfv" == guid
components = v_info.components.selection.component
assert {c.ifc_guid for c in components} == {"firstId", "secondId"}
camera = v_info.perspective_camera
+1 -1
View File
@@ -15,7 +15,7 @@ def test_create_clash_set_bcf() -> None:
assert len(topic.viewpoints) == 1
guid, vi_handler = next((k, v) for k, v in topic.viewpoints.items())
v_info = vi_handler.visualization_info
assert v_info.guid == guid
assert f"{v_info.guid}.bcfv" == guid
components = v_info.components.selection.component
assert {c.ifc_guid for c in components} == {"firstId", "secondId"}
camera = v_info.perspective_camera
+36 -22
View File
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# Copyright (C) 2020-2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
@@ -16,7 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
PYTHON:=python
PYTHON:=python3.10
PIP:=pip3.10
PATCH:=patch
SED:=sed -i
ifeq ($(OS),Windows_NT)
@@ -56,7 +57,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py37h95e2c48_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py37h0379df6_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/30/c0/d0526314971fc661b083ab135747dc68446a3022686da8c16d25fcf6ef07/lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/a2/73/07f507b539527579c026ed5f411c35e287685ce794c4dfb7edf6675cdc24/Shapely-1.8.4-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/1d/a4/931d0780f31f3ea8c4f9ef6464a2825137c5241e6707a5fb03bef760a7eb/shapely-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/ed/d5/c2e84e1e36ab8ebea033921d5886a056c77e18bab5ab1051fcc22de2e8a2/Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl
endif
ifeq ($(PYVERSION), py39)
@@ -64,7 +65,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py39h5aed9d1_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py39h5472131_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/08/12/5af915b2bc0470f4a652ef9978557a0fa8b705b122ccaec6626029e1674c/Shapely-1.8.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/2d/f2/8ec281d357e8bb7d08dc8d727f0e4c8ef3dae7d3fa75c69c8e452bb82d50/shapely-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/01/61/3ff85fb4bb596ce3d223c8fcf93c8df5c12bc8899dfb4fb3cb1c5b20dd5f/Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl
endif
ifeq ($(PYVERSION), py310)
@@ -72,7 +73,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/linux-64/hpp
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/linux-64/eigenpy-2.6.11-py310hf3e5c9c_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py310h7c3ba0c_5.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/25/1e/19b46d8e8881fe0df2e20945d51919eeb1817836d62a90efa8506530e45c/lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/60/20/c6321e3da92a4ad9495727c9aa8c423e47295572ccd35ece82d1ac013e28/Shapely-1.8.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/a8/a5/403728b5614b28083f6424dfdefec5fcf58068495fb03bb08532671c642f/shapely-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/f6/51/320986ebd6d46a0e95c2240468ced73153b691ce07617078bcdf30c609ec/Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2
@@ -86,7 +87,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-f
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/1e/3e/f0abc15d5dac50939bccc589aae336d5ead4c72e7ad1039a2e0f3630ea92/lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/47/a9/7c644e71638bff2bbaa87f05ee62fdf6b4552a55e74bbf731d6210b6d436/Shapely-1.8.4-cp37-cp37m-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/e6/7d/4923f27c340339e1c896c77cafc8ed672c8d381a025bbab6c6ddcba27e8f/shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/88/49/c26fc3b5b0e82bdc9d8751d6b939da29327b0d98f7c3b95a575cbfed2743/Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl
endif
ifeq ($(PYVERSION), py39)
@@ -94,7 +95,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-f
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py39h5405915_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py39ha641261_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/f5/f9/bf6cc34b02b3c7d3777c5e47fa57d2f69387613bbd8decdf3577a9d83a83/Shapely-1.8.4-cp39-cp39-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/36/a4/7e542a209f862f967d7cb8e939eff155f4294a27d17e16441fb8bdd51a2c/shapely-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/88/7a/ddfe28b485b623361457d4783007c1f9ba83a87f93e7fec32f64793efb6c/Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl
endif
ifeq ($(PYVERSION), py310)
@@ -102,7 +103,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-64/hpp-f
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/osx-64/eigenpy-2.6.11-py310hc03097c_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py310h509978a_5.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/a1/44/17b7dac7a18807d30e2fe10c3328c152808f5464565e230bfd0e77f178c6/lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/f5/a8/02347c450b5507b9a7a2c15ad6a029613149468fc55ac2100ab215666159/Shapely-1.8.4-cp310-cp310-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/1f/2a/dc3353c2431cf53e8d04bb8fba27e584410ca3435c9c85f76d71bf0c0e80/shapely-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/d8/60/b13c00d403f34110e96c1b5c0afa73ce461efe3fe960c3a7e3e7fe190d82/Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2
@@ -117,7 +118,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-f
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-64/lxml-4.9.1-py37h994c40b_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/47/a9/7c644e71638bff2bbaa87f05ee62fdf6b4552a55e74bbf731d6210b6d436/Shapely-1.8.4-cp37-cp37m-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/e6/7d/4923f27c340339e1c896c77cafc8ed672c8d381a025bbab6c6ddcba27e8f/shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl
endif
ifeq ($(PYVERSION), py39)
@@ -125,7 +126,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-arm64/hp
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.7/download/osx-arm64/eigenpy-2.6.7-py39ha69f3c1_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h2550fe3_0.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h9eb174b_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/8e/60/8186995a96c5a27c9b79c015bbdd89c6d9a6df9988f86f3f1027ecd4a56a/Shapely-1.8.4-cp39-cp39-macosx_11_0_arm64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/ea/aa/45fbd031edf3149cb767d8b9f9db45d5faf0324d743c6b8fb0298cc022d0/shapely-2.0.1-cp39-cp39-macosx_11_0_arm64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl
endif
ifeq ($(PYVERSION), py310)
@@ -133,7 +134,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-arm64/hp
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.9/download/osx-arm64/eigenpy-2.6.9-py310habfc766_1.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h4566fd1_0.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/ba/d0/75cd36c81a1aa26dd6d37cf7ae1c79b0bc2d379e0f32c359bcb00b14e5c3/Shapely-1.8.4-cp310-cp310-macosx_11_0_arm64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/ec/41/d59208743e737184e1b403e95a937aebb022b8459e99efbcd5208fc8be46/shapely-2.0.1-cp310-cp310-macosx_11_0_arm64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/0c/5f/117b653cad585f3aedfe0de996c292e67d4b020ed77f652e5a6c8c24f908/Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-arm64/assimp-5.0.1-h0f81e16_7.tar.bz2
@@ -147,7 +148,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-f
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py37h2c32e34_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py37h3b38789_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/9e/5e/171ee9d40a600f565fe691ec5bf7596247ec62cfb2edc00c91afe8ea837b/lxml-4.6.3-cp37-cp37m-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/ca/4d/85b912d7947261e6b3218b01946bcd7c2efb2bbafaf517c6fe1dc3bb8399/Shapely-1.8.4-cp37-cp37m-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/e2/87/b8b8d8d57b429b01aa56b7d723075c09f33c988b8091bb6f790c83436909/shapely-2.0.1-cp37-cp37m-win_amd64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/69/f5/9e802159d78b2eaf26bf1f8b94648605993f5ca7247ac8870f065063fc40/Pillow-9.2.0-cp37-cp37m-win_amd64.whl
endif
ifeq ($(PYVERSION), py39)
@@ -155,7 +156,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-f
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py39h3ce40e6_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py39hefe7e4c_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/72/d4/426ecb8849c47c3e370c87aa0ac05d85768df917ffea27fcd6686a5e6495/lxml-4.6.3-cp39-cp39-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/72/c3/0d52812db25ca71203f6203891277e6ee4f0244b21a36c062123ece15f36/Shapely-1.8.4-cp39-cp39-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/a7/ae/eab645c4075093584b7a65ab71cb8ff4563a015bd935c9b55dba14b2c1eb/shapely-2.0.1-cp39-cp39-win_amd64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/19/3f/b4d4bcf05dbcbe07f2e9613a8f4180c297395e73a91d8ad22c32c6624f8c/Pillow-9.2.0-cp39-cp39-win_amd64.whl
endif
ifeq ($(PYVERSION), py310)
@@ -163,7 +164,7 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/win-64/hpp-f
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/win-64/eigenpy-2.6.11-py310hbd43d28_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py310hc781a3c_5.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/f6/71/65c80a4caa1617a4c6e8fe1500cebb179db96232e2f623bfe6a1f4294e39/lxml-4.8.0-cp310-cp310-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/24/b5/46d13799804b5fe9ede2eca8b19023410e29b78baca0bff727671a1df8c1/Shapely-1.8.4-cp310-cp310-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/81/8a/7ac076a86b2632f1872284c5e60ed5f2fc26094875a85b35d9fa17b52504/shapely-2.0.1-cp310-cp310-win_amd64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/02/55/67a3c17b9e7d972ed8c246f104da99ca4f3ea42fba566697e479011b84b6/Pillow-9.2.0-cp310-cp310-win_amd64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/win-64/assimp-5.0.1-hc2aa0de_6.tar.bz2
@@ -189,7 +190,7 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-07ee62e-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-476ab50-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -207,14 +208,16 @@ endif
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/entity_instance.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/file.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/validate.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py dist/blenderbim/libs/site/packages/ifcopenshell/express/
# Provides bcf functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/bcf/src/bcf dist/blenderbim/libs/site/packages/
# Provides IFCClash functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcclash/ifcclash dist/blenderbim/libs/site/packages/
# Provides BIMTester functionality
cd dist/working && $(PYTHON) -m venv env
cd dist/working && . env/bin/activate && pip install pybabel
cd dist/working && . env/bin/activate && pip install babel
cd dist/working && . env/bin/activate && $(PIP) install pybabel
cd dist/working && . env/bin/activate && $(PIP) install babel
cd dist/working && . env/bin/activate && ./env/bin/pybabel compile -d ./IfcOpenShell-0.7.0/src/ifcbimtester/bimtester/locale/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcbimtester/bimtester dist/blenderbim/libs/site/packages/
# Provides IFCTester functionality
@@ -236,6 +239,7 @@ endif
rm -rf dist/working
# Provides Mustache templating in construction documentation
# TODO: remove this dependency, it seems overkill and we seem to get by with str replaces
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/3f/e7/8750ba6c6101d6aa5ceeb20c013adf2c6f3554a12c71d75654b468404bfa/pystache-0.6.0.tar.gz
cd dist/working && tar -xzvf pystache*
@@ -367,10 +371,10 @@ endif
cd dist/working && cp *.whl shapely.zip && unzip shapely.zip
cp -r dist/working/shapely dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), win)
cp -r dist/working/Shapely.libs dist/blenderbim/libs/site/packages/
cp -r dist/working/shapely.libs dist/blenderbim/libs/site/packages/
endif
ifeq ($(PLATFORM), linux)
cp -r dist/working/Shapely.libs dist/blenderbim/libs/site/packages/
cp -r dist/working/shapely.libs dist/blenderbim/libs/site/packages/
endif
rm -rf dist/working
@@ -392,11 +396,13 @@ endif
cp -r dist/working/tqdm-4.50.0/tqdm dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFCCityJSON
mkdir dist/working
cd dist/working && $(PYTHON) -m venv env
cd dist/working && mkdir site-packages
cd dist/working && . env/bin/activate && pip install cjio --target=./site-packages
# Required by IFCCityJSON
cd dist/working && . env/bin/activate && $(PIP) install cjio --target=./site-packages
# Provides express rule validation for ifcopenshell.validate
cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages
cd dist/working/site-packages/ && rm -r *dist-info*
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
@@ -542,6 +548,7 @@ endif
rm -rf dist/working
# Required by IFCCOBie for XLSX support
# TODO: see if we can replace this with openpyxl which does both read/write
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/0c/bc/82d6783f83f65f56d8b77d052773c4a2f952fa86385f0cd54e1e006658d7/XlsxWriter-1.2.9.tar.gz
cd dist/working && tar -xzvf XlsxWriter*
@@ -613,7 +620,7 @@ endif
cd dist/working/brickschema-0.5.1/ && cp -r brickschema ../../blenderbim/libs/site/packages/
# For now lets bundle the latest nightly schema
cd dist/working && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
cd dist/working && cp Brick.ttl ../../blenderbim/bim/schema/Brick.ttl
cd dist/working && cp Brick.ttl ../blenderbim/bim/schema/Brick.ttl
rm -rf dist/working
# Required by brickschema
@@ -645,6 +652,13 @@ endif
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
rm -rf dist/working
# Required for hipped roof generation
mkdir dist/working
cd dist/working && wget https://github.com/prochitecture/bpypolyskel/archive/refs/heads/master.zip
cd dist/working && unzip master.zip
cp -r dist/working/bpypolyskel-master/bpypolyskel dist/blenderbim/libs/site/packages/
rm -rf dist/working
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
rm -rf dist/blenderbim
+2 -2
View File
@@ -152,7 +152,7 @@ def register():
bpy.app.handlers.redo_post.append(handler.redo_post)
bpy.app.handlers.load_post.append(handler.setDefaultProperties)
bpy.app.handlers.load_post.append(handler.loadIfcStore)
bpy.app.handlers.save_pre.append(handler.ensureIfcExported)
bpy.app.handlers.save_post.append(handler.ensureIfcExported)
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
bpy.types.Object.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties)
bpy.types.Material.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties)
@@ -174,7 +174,7 @@ def unregister():
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(handler.setDefaultProperties)
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
bpy.app.handlers.save_pre.remove(handler.ensureIfcExported)
bpy.app.handlers.save_post.remove(handler.ensureIfcExported)
del bpy.types.Scene.BIMProperties
del bpy.types.Object.BIMObjectProperties
del bpy.types.Material.BIMObjectProperties
@@ -74,17 +74,17 @@ DATA;
#67=IFCMATERIALLAYERSET((#69),$,$);
#68=IFCRELASSOCIATESMATERIAL('03XoY4bYD4ZwZgvg3JqIIK',$,$,$,(#66),#67);
#69=IFCMATERIALLAYER(#19,0.3,$,$,$,$,$);
#70=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,0.5,0.6);
#70=IFCRECTANGLEPROFILEDEF(.AREA.,'500x600',$,0.5,0.6);
#71=IFCCOLUMNTYPE('187MdL9O13_OzlwsOaT0zY',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
#72=IFCMATERIALPROFILESET($,$,(#74),$);
#73=IFCRELASSOCIATESMATERIAL('1CMVTn08P2Tu8afsZHQiUk',$,$,$,(#71),#72);
#74=IFCMATERIALPROFILE($,$,#19,#70,$,$);
#75=IFCCIRCLEHOLLOWPROFILEDEF(.AREA.,$,$,0.25,0.005);
#75=IFCCIRCLEHOLLOWPROFILEDEF(.AREA.,'500.0x5.0 CHS',$,0.25,0.005);
#76=IFCCOLUMNTYPE('3e6tIcskjDOuj1_UAWcT87',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
#77=IFCMATERIALPROFILESET($,$,(#79),$);
#78=IFCRELASSOCIATESMATERIAL('0ahT30pYb7zQ968gWY9q1A',$,$,$,(#76),#77);
#79=IFCMATERIALPROFILE($,$,#19,#75,$,$);
#80=IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,$,$,0.075,0.15,0.005,0.005,0.005);
#80=IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,'150x75x2.0 RHS',$,0.075,0.15,0.002,0.005,0.005);
#81=IFCCOLUMNTYPE('0ZxXYg0BTFg8sbqXr50qPW',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
#82=IFCMATERIALPROFILESET($,$,(#84),$);
#83=IFCRELASSOCIATESMATERIAL('0ul_xSpIH2MxsZcv$ZFXG7',$,$,$,(#81),#82);
@@ -1,13 +0,0 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((),'2;1');
FILE_NAME('EPset_Annotation.ifc','2020-01-01T00:00:00',(),(),'EPset_Annotation','EPset_Annotation',$);
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,22 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((),'2;1');
FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_Annotation','Psets_BBIM_Annotation',$);
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#6,#7));
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#9,#10,#11,#12,#13));
#9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('2$6U0mLI9AiPRWeBabdY3u',$,'EndArrowSymbol','Custom symbol for the end of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('1naFqntIL7igCY7hCaE7kq',$,'HasConnectedSectionLine','Connect or disconnect section markers with line (by default = True).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -19,11 +19,12 @@
*/
* { stroke-linecap: round; stroke-linejoin: round; }
text { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT'; font-size: 4.13px; }
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; font-size: 4.13px; }
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }
.PredefinedType-LINEWORK { stroke: black; stroke-width: 0.25; }
.PredefinedType-BACKGROUND { stroke: black; stroke-width: 0.18; }
.PredefinedType-GRID { marker-start: url(#grid-marker); marker-end: url(#grid-marker); }
@@ -32,23 +33,27 @@ text { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT';
.PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); }
.PredefinedType-DIMENSION { marker-start: url(#dimension-marker-start); marker-end: url(#dimension-marker-end); }
.PredefinedType-ANGLE { marker-start: url(#angle-marker-start); marker-end: url(#angle-marker-end); }
.PredefinedType-RADIUS { marker-end: url(#radius-marker-end); }
.PredefinedType-RADIUS { marker-end: url(#fall-marker-end); }
.PredefinedType-FALL { marker-end: url(#radius-marker-end); }
.PredefinedType-SLOPEANGLE { marker-end: url(#radius-marker-end); }
.PredefinedType-SLOPEPERCENT { marker-end: url(#radius-marker-end); }
.PredefinedType-SLOPEFRACTION { marker-end: url(#radius-marker-end); }
.PredefinedType-DIAMETER { marker-start: url(#diameter-marker-start); marker-end: url(#diameter-marker-end); }
.PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; }
.PredefinedType-STAIRARROW { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
.PredefinedType-BOUNDARY { fill: none; stroke: red; stroke-width: 1; stroke-dasharray: 12, 4, 3, 4, 3, 4; }
.PredefinedType-BATTING { marker-start: url(#batting); stroke: none; }
.PredefinedType-SEALANT { fill: url(#crosshatch1); stroke-width: 0.25; }
.PredefinedType-FILLAREA { fill: white; stroke: black; }
.PredefinedType-BREAKLINE { fill: none; stroke: black; stroke-width: 0.25; marker-mid: url(#breakline-marker); }
.PredefinedType-TEXT { fill: black; stroke: none; }
path.PredefinedType-TEXTLEADER { marker-end: url(#leader-marker); }
text.PredefinedType-TEXTLEADER { fill: black; stroke: none; }
text.title { /* 7mm */ font-size: 11.55px; }
text.header { /* 5mm */ font-size: 8.25px; }
text.large { /* 3.5mm */ font-size: 5.78px; }
text.regular { /* 2.5mm */ font-size: 4.13px; }
text.small { /* 1.8mm */ font-size: 2.97px; }
text.PredefinedType-TEXTLEADER, tspan.PredefinedType-TEXTLEADER { fill: black; stroke: none; }
text.title, tspan.title { /* 7mm */ font-size: 11.55px; }
text.header, tspan.header { /* 5mm */ font-size: 8.25px; }
text.large, tspan.large { /* 3.5mm */ font-size: 5.78px; }
text.regular, tspan.regular { /* 2.5mm */ font-size: 4.13px; }
text.small, tspan.small { /* 1.8mm */ font-size: 2.97px; }
text.GRID, tspan.GRID { /* 5mm */ font-size: 8.25px; }
.material-blank { fill: white; }
.material-diagonal1 { fill: url(#diagonal1); }
.material-diagonal2 { fill: url(#diagonal2); }
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<marker id="batting" markerHeight="10" markerWidth="4" orient="auto" refX="0" refY="5" markerUnits="userSpaceOnUse">
<!-- Tip: https://svg-path-visualizer.netlify.app/ -->
<path style="fill: none; stroke:black; stroke-width:0.18" d="M 0 2 A 2 2 0 0 1 4 2 L 2 8 M 0 2 L 2 8 A 2 2 0 0 0 4 10 M 2 8 A 2 2 0 0 1 0 10" />
</marker>
<!-- Tip: https://www.svgviewer.dev/ -->
<marker id="eng-dimension-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
<g>
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />
@@ -21,6 +19,12 @@
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
</g>
</marker>
<!-- same as for the radius -->
<marker id="fall-marker-end" markerHeight="14" markerWidth="11" orient="auto" refX="10" refY="7">
<g>
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
</g>
</marker>
<marker id="angle-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
<g>
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -5,7 +5,9 @@
</g>
<g id="door-tag">
<circle cy="1.75" r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<text text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<line x1="-5" y1="1.75" x2="5" y2="1.75" style="stroke: black; stroke-width: 0.25;" />
<text y="5" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
</g>
<g id="elevation-arrow">
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 841 594"
id="root"
version="1.1"
height="594mm"
width="841mm">
width="841mm"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs901" />
<metadata
@@ -20,7 +20,6 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
@@ -44,146 +43,10 @@
width="3027.1016"
id="rect116616"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.88976377;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
id="text116526"
y="909.15491"
x="113.70713"
style="font-style:normal;font-weight:normal;font-size:15.1536px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.1536px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.94488188;stroke-miterlimit:4;stroke-dasharray:none"
y="909.15491"
x="113.70713"
id="tspan116524">REV. NO.</tspan></text>
<path
id="path116530"
d="M 75.590564,895.24802 H 3102.9921"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path116532"
d="M 151.6811,895.24802 V 1046.9291"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 454.04331,895.24802 V 1046.9291"
id="path116534" />
<path
id="path116536"
d="M 529.63386,895.24802 V 1046.4302"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 605.22441,895.24802 V 1046.9291"
id="path116538" />
<path
id="path116540"
d="M 75.590564,914.14565 H 605.22441"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 75.590564,933.04329 H 605.22441"
id="path116542" />
<path
id="path116544"
d="M 75.590564,951.94093 H 605.22441"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 75.590564,970.83857 H 605.22441"
id="path116546" />
<path
id="path116548"
d="M 75.590564,989.73621 H 605.22441"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 75.590564,1008.6338 H 605.22441"
id="path116550" />
<path
id="path116552"
d="M 75.590564,1027.5315 H 605.22441"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path116576"
d="M 2800.5,895.24802 V 1046.9291"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path116578"
d="m 2800.5,1008.6338 h 302.4921"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path116598"
d="m 3027.4027,1008.6338 v 38.2953"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:15.1536px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
x="302.86591"
y="909.15491"
id="text1395"><tspan
id="tspan1393"
x="302.86591"
y="909.15491"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.1536px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.94488188;stroke-miterlimit:4;stroke-dasharray:none">DESCRIPTION</tspan></text>
<text
id="text1399"
y="909.15491"
x="491.97513"
style="font-style:normal;font-weight:normal;font-size:15.1536px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.1536px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.94488188;stroke-miterlimit:4;stroke-dasharray:none"
y="909.15491"
x="491.97513"
id="tspan1397">AUTHOR</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:16.0591px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
x="568.05585"
y="909.15485"
id="text1403"><tspan
id="tspan1401"
x="568.05585"
y="909.15485"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16.0591px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.94488188;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:11.2271px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
x="2839.5081"
y="1020.4988"
id="text1421"><tspan
id="tspan1419"
x="2839.5081"
y="1020.4988"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.2271px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.94488188;stroke-miterlimit:4;stroke-dasharray:none">DRAWING NUMBER</tspan></text>
<text
id="text1429"
y="1020.4988"
x="3038.5586"
style="font-style:normal;font-weight:normal;font-size:11.2271px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.94488188;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.2271px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.94488188;stroke-miterlimit:4;stroke-dasharray:none"
y="1020.4988"
x="3038.5586"
id="tspan1427">REV</tspan></text>
<text
id="text2529"
y="1036.1166"
x="3030.9653"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3918px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.999999px"
y="1036.1166"
x="3030.9653"
id="tspan2527">{{Revision}}</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3918px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="2804.7021"
y="1036.1166"
id="text2533"><tspan
id="tspan2531"
x="2804.7021"
y="1036.1166"
style="stroke-width:0.999999px">{{Identification}} - {{Name}}</tspan></text>
</g>
<g
transform="matrix(0.26458333,0,0,0.26458334,-3.180917e-5,297.00003)"
@@ -205,4 +68,260 @@
transform="matrix(0.26458333,0,0,0.26458334,-3.180917e-5,297.00003)"
id="g90677"
style="stroke-width:3.77953" />
<g
id="g38171"
transform="translate(411.014,286.96036)">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,246.86771 V 286.75"
id="path116576" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,276.8677 H 410"
id="path116578" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 390.00031,276.8677 V 287"
id="path116598" />
<text
id="text1421"
y="280.00699"
x="340.28653"
style="font-style:normal;font-weight:normal;font-size:2.9705px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.9705px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="280.00699"
x="340.28653"
id="tspan1419">DRAWING NUMBER</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:3.05925px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25747;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="393.00568"
y="280.0592"
id="text1429"><tspan
id="tspan1427"
x="393.00568"
y="280.0592"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.05925px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25747;stroke-miterlimit:4;stroke-dasharray:none">REV</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="390.94287"
y="284.13919"
id="text2529"><tspan
id="tspan2527"
x="390.94287"
y="284.13919"
style="stroke-width:0.264583px">{{Revision}}</tspan></text>
<text
id="text2533"
y="284.13919"
x="331.07742"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.265;stroke-dasharray:none"
y="284.13919"
x="331.07742"
id="tspan2531">{{Identification}}</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,266.868 H 410"
id="path36736" />
<text
id="text36740"
y="270.05899"
x="339.00919"
style="font-style:normal;font-weight:normal;font-size:3.05925px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25747;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.05925px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25747;stroke-miterlimit:4;stroke-dasharray:none"
y="270.05899"
x="339.00919"
id="tspan36738">DRAWING TITLE</tspan></text>
<text
id="text36744"
y="275.05634"
x="330.99438"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.96055px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.371;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.371;stroke-dasharray:none"
y="275.05634"
x="330.99438"
id="tspan36742">{{Name}}</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 390.00031,246.86771 V 266.993"
id="path36746" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.88677px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.169851;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="397.04108"
y="249.72177"
id="text36677"><tspan
id="tspan36675"
x="397.04108"
y="249.72177"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.88677px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.169851;stroke-miterlimit:4;stroke-dasharray:none">GRID NORTH</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.88677px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.169851;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="338.77008"
y="249.72177"
id="text36752"><tspan
id="tspan36750"
x="335.76562"
y="249.72177"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.88677px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.169851;stroke-miterlimit:4;stroke-dasharray:none">COMPANY</tspan></text>
<g
id="g37148"
transform="translate(399.87519,258.25776)">
<g
id="g37140"
data-type="grid-north">
<path
id="path11652"
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="M 0,0 V 7.12566 Z" />
<path
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="M 7.3079,3e-5 H -7.30798"
id="path36547" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
id="path36601"
cx="-3.8305665e-05"
cy="4.15625e-05"
r="5" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="m -5.00004,4e-5 5,-5 5,5 H 3.94162 L -4e-5,-3.94162 -3.9417,4e-5 Z"
id="path36671" />
<path
style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-dasharray:none"
d="M -4e-5,-7.12545 V 3e-5"
id="path36673" />
</g>
</g>
</g>
<g
id="g38492"
transform="translate(9.7896584,287.21036)">
<text
id="text116526"
y="250.54721"
x="20.085007"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="250.54721"
x="20.085007"
id="tspan116524">REV. NO.</tspan></text>
<path
id="path116532"
d="M 30.132288,246.86771 V 287"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 110.13229,246.86771 V 287"
id="path116534" />
<path
id="path116536"
d="M 130.13229,246.86771 V 286.868"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 150.13229,246.86771 V 287"
id="path116538" />
<path
id="path116540"
d="M 10,251.86771 H 170.132"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,256.86771 H 170.132"
id="path116542" />
<path
id="path116544"
d="M 10,261.86771 H 170.132"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,266.86771 H 170.132"
id="path116546" />
<path
id="path116548"
d="M 10,271.86771 H 170.132"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,276.8677 H 170.132"
id="path116550" />
<path
id="path116552"
d="M 10,281.86772 H 170.132"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="70.13327"
y="250.54721"
id="text1395"><tspan
id="tspan1393"
x="70.13327"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DESCRIPTION</tspan></text>
<text
id="text1399"
y="250.54721"
x="120.16841"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="250.54721"
x="120.16841"
id="tspan1397">AUTHOR</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="140.29811"
y="250.54721"
id="text1403"><tspan
id="tspan1401"
x="140.29811"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">ISSUED</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="180.14232"
y="250.54721"
id="text36130"><tspan
id="tspan36128"
x="175.72011"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">NOTES</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="194.37886"
y="255.5477"
id="text36768"><tspan
id="tspan36766"
x="189.95665"
y="255.5477"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DO NOT SCALE DRAWINGS</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 170.132,246.86771 V 287"
id="path36836" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="160.29811"
y="250.54721"
id="text36842"><tspan
id="tspan36840"
x="160.29811"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 20 KiB

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="594mm"
height="420mm"
version="1.1"
id="root"
viewBox="0 0 594 420">
viewBox="0 0 594 420"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs901" />
<metadata
@@ -20,7 +20,6 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
@@ -40,146 +39,10 @@
width="553.5"
id="rect116616"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.499999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
id="text116526"
y="363.54721"
x="30.085009"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="363.54721"
x="30.085009"
id="tspan116524">REV. NO.</tspan></text>
<path
id="path116530"
d="m 20.000001,359.86771 553.749999,0"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path116532"
d="M 40.132289,359.86771 V 400"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 120.13229,359.86771 V 400"
id="path116534" />
<path
id="path116536"
d="M 140.13229,359.86771 V 399.868"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 160.13229,359.86771 V 400"
id="path116538" />
<path
id="path116540"
d="M 20.000001,364.86771 H 160.13229"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 20.000001,369.86771 H 160.13229"
id="path116542" />
<path
id="path116544"
d="M 20.000001,374.86771 H 160.13229"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 20.000001,379.86771 H 160.13229"
id="path116546" />
<path
id="path116548"
d="M 20.000001,384.86771 H 160.13229"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 20.000001,389.8677 H 160.13229"
id="path116550" />
<path
id="path116552"
d="M 20.000001,394.86772 H 160.13229"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="80.13327"
y="363.54721"
id="text1395"><tspan
id="tspan1393"
x="80.13327"
y="363.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DESCRIPTION</tspan></text>
<text
id="text1399"
y="363.54721"
x="130.16841"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="363.54721"
x="130.16841"
id="tspan1397">AUTHOR</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="150.29811"
y="363.54721"
id="text1403"><tspan
id="tspan1401"
x="150.29811"
y="363.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
<path
id="path116576"
d="M 493.96563,359.86771 V 400"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path116578"
d="M 493.96563,389.8677 H 574"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path116598"
d="M 554.00031,389.8677 V 400"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.9705px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="504.28653"
y="393.00699"
id="text1421"><tspan
id="tspan1419"
x="504.28653"
y="393.00699"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.9705px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DRAWING NUMBER</tspan></text>
<text
id="text1429"
y="393.00699"
x="556.9519"
style="font-style:normal;font-weight:normal;font-size:2.9705px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.9705px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="393.00699"
x="556.9519"
id="tspan1427">REV</tspan></text>
<text
id="text2529"
y="397.13919"
x="554.94287"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.264583px"
y="397.13919"
x="554.94287"
id="tspan2527">{{Revision}}</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="495.07742"
y="397.13919"
id="text2533"><tspan
id="tspan2531"
x="495.07742"
y="397.13919"
style="stroke-width:0.264583px">{{Identification}} - {{Name}}</tspan></text>
<g
style="stroke-width:3.77953"
id="g90669"
@@ -200,4 +63,260 @@
style="stroke-width:3.77953"
id="g90677"
transform="matrix(0.26458333,0,0,0.26458334,-3.180917e-5,297.00003)" />
<g
id="g38171"
transform="translate(163.75769,113)">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,246.86771 V 286.75"
id="path116576" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,276.8677 H 410"
id="path116578" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 390.00031,276.8677 V 287"
id="path116598" />
<text
id="text1421"
y="280.00699"
x="340.28653"
style="font-style:normal;font-weight:normal;font-size:2.9705px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.9705px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="280.00699"
x="340.28653"
id="tspan1419">DRAWING NUMBER</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:3.05925px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25747;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="393.00568"
y="280.0592"
id="text1429"><tspan
id="tspan1427"
x="393.00568"
y="280.0592"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.05925px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25747;stroke-miterlimit:4;stroke-dasharray:none">REV</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="390.94287"
y="284.13919"
id="text2529"><tspan
id="tspan2527"
x="390.94287"
y="284.13919"
style="stroke-width:0.264583px">{{Revision}}</tspan></text>
<text
id="text2533"
y="284.13919"
x="331.07742"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.265;stroke-dasharray:none"
y="284.13919"
x="331.07742"
id="tspan2531">{{Identification}}</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,266.868 H 410"
id="path36736" />
<text
id="text36740"
y="270.05899"
x="339.00919"
style="font-style:normal;font-weight:normal;font-size:3.05925px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25747;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.05925px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25747;stroke-miterlimit:4;stroke-dasharray:none"
y="270.05899"
x="339.00919"
id="tspan36738">DRAWING TITLE</tspan></text>
<text
id="text36744"
y="275.05634"
x="330.99438"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.96055px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.371;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.371;stroke-dasharray:none"
y="275.05634"
x="330.99438"
id="tspan36742">{{Name}}</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 390.00031,246.86771 V 266.993"
id="path36746" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.88677px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.169851;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="397.04108"
y="249.72177"
id="text36677"><tspan
id="tspan36675"
x="397.04108"
y="249.72177"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.88677px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.169851;stroke-miterlimit:4;stroke-dasharray:none">GRID NORTH</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.88677px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.169851;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="338.77008"
y="249.72177"
id="text36752"><tspan
id="tspan36750"
x="335.76562"
y="249.72177"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.88677px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.169851;stroke-miterlimit:4;stroke-dasharray:none">COMPANY</tspan></text>
<g
id="g37148"
transform="translate(399.87519,258.25776)">
<g
id="g37140"
data-type="grid-north">
<path
id="path11652"
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="M 0,0 V 7.12566 Z" />
<path
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="M 7.3079,3e-5 H -7.30798"
id="path36547" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
id="path36601"
cx="-3.8305665e-05"
cy="4.15625e-05"
r="5" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="m -5.00004,4e-5 5,-5 5,5 H 3.94162 L -4e-5,-3.94162 -3.9417,4e-5 Z"
id="path36671" />
<path
style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-dasharray:none"
d="M -4e-5,-7.12545 V 3e-5"
id="path36673" />
</g>
</g>
</g>
<g
id="g38492"
transform="translate(10.000001,113)">
<text
id="text116526"
y="250.54721"
x="20.085007"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="250.54721"
x="20.085007"
id="tspan116524">REV. NO.</tspan></text>
<path
id="path116532"
d="M 30.132288,246.86771 V 287"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 110.13229,246.86771 V 287"
id="path116534" />
<path
id="path116536"
d="M 130.13229,246.86771 V 286.868"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 150.13229,246.86771 V 287"
id="path116538" />
<path
id="path116540"
d="M 10,251.86771 H 170.132"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,256.86771 H 170.132"
id="path116542" />
<path
id="path116544"
d="M 10,261.86771 H 170.132"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,266.86771 H 170.132"
id="path116546" />
<path
id="path116548"
d="M 10,271.86771 H 170.132"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,276.8677 H 170.132"
id="path116550" />
<path
id="path116552"
d="M 10,281.86772 H 170.132"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="70.13327"
y="250.54721"
id="text1395"><tspan
id="tspan1393"
x="70.13327"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DESCRIPTION</tspan></text>
<text
id="text1399"
y="250.54721"
x="120.16841"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="250.54721"
x="120.16841"
id="tspan1397">AUTHOR</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="140.29811"
y="250.54721"
id="text1403"><tspan
id="tspan1401"
x="140.29811"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">ISSUED</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="180.14232"
y="250.54721"
id="text36130"><tspan
id="tspan36128"
x="175.72011"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">NOTES</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="194.37886"
y="255.5477"
id="text36768"><tspan
id="tspan36766"
x="189.95665"
y="255.5477"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DO NOT SCALE DRAWINGS</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 170.132,246.86771 V 287"
id="path36836" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="160.29811"
y="250.54721"
id="text36842"><tspan
id="tspan36840"
x="160.29811"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 20 KiB

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="420mm"
height="297mm"
version="1.1"
id="root"
viewBox="0 0 420 297">
viewBox="0 0 420 297"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs901" />
<metadata
@@ -20,7 +20,6 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
@@ -40,150 +39,10 @@
width="399.5"
id="rect116616"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.49999897;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
id="text116526"
y="250.54721"
x="20.085007"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="250.54721"
x="20.085007"
id="tspan116524">REV. NO.</tspan></text>
<path
id="path116530"
d="m 10,246.86771 399.75,0"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path116532"
d="M 30.132288,246.86771 V 287"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 110.13229,246.86771 V 287"
id="path116534" />
<path
id="path116536"
d="M 130.13229,246.86771 V 286.868"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 150.13229,246.86771 V 287"
id="path116538" />
<path
id="path116540"
d="M 10,251.86771 H 150.13229"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,256.86771 H 150.13229"
id="path116542" />
<path
id="path116544"
d="M 10,261.86771 H 150.13229"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,266.86771 H 150.13229"
id="path116546" />
<path
id="path116548"
d="M 10,271.86771 H 150.13229"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,276.8677 H 150.13229"
id="path116550" />
<path
id="path116552"
d="M 10,281.86772 H 150.13229"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="70.13327"
y="250.54721"
id="text1395"><tspan
id="tspan1393"
x="70.13327"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DESCRIPTION</tspan></text>
<text
id="text1399"
y="250.54721"
x="120.16841"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="250.54721"
x="120.16841"
id="tspan1397">AUTHOR</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="140.29811"
y="250.54721"
id="text1403"><tspan
id="tspan1401"
x="140.29811"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
<g
transform="translate(-154)"
id="g3735">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 483.96563,246.86771 V 287"
id="path116576" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 483.96563,276.8677 H 564"
id="path116578" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 544.00031,276.8677 V 287"
id="path116598" />
<text
id="text1421"
y="280.00699"
x="494.28653"
style="font-style:normal;font-weight:normal;font-size:2.9705px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.9705px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="280.00699"
x="494.28653"
id="tspan1419">DRAWING NUMBER</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.9705px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="546.9519"
y="280.00699"
id="text1429"><tspan
id="tspan1427"
x="546.9519"
y="280.00699"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.9705px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">REV</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="544.94287"
y="284.13919"
id="text2529"><tspan
id="tspan2527"
x="544.94287"
y="284.13919"
style="stroke-width:0.264583px">{{Revision}}</tspan></text>
<text
id="text2533"
y="284.13919"
x="485.07742"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.264583px"
y="284.13919"
x="485.07742"
id="tspan2531">{{Identification}} - {{Name}}</tspan></text>
</g>
<g
style="stroke-width:3.77953"
id="g90669"
@@ -204,4 +63,271 @@
style="stroke-width:3.77953"
id="g90677"
transform="matrix(0.26458333,0,0,0.26458334,-3.180917e-5,297.00003)" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:100%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="162.3047"
y="256.66803"
id="text36756" />
<text
xml:space="preserve"
style="font-size:20.4013px;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT, Normal';text-align:center;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-dasharray:none"
x="161.2028"
y="266.93387"
id="text36760" />
<g
id="g38492">
<text
id="text116526"
y="250.54721"
x="20.085007"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="250.54721"
x="20.085007"
id="tspan116524">REV. NO.</tspan></text>
<path
id="path116532"
d="M 30.132288,246.86771 V 287"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 110.13229,246.86771 V 287"
id="path116534" />
<path
id="path116536"
d="M 130.13229,246.86771 V 286.868"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 150.13229,246.86771 V 287"
id="path116538" />
<path
id="path116540"
d="m 10,251.86771 160.132,0"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 10,256.86771 160.132,0"
id="path116542" />
<path
id="path116544"
d="m 10,261.86771 160.132,0"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 10,266.86771 160.132,0"
id="path116546" />
<path
id="path116548"
d="m 10,271.86771 160.132,0"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 10,276.8677 160.132,0"
id="path116550" />
<path
id="path116552"
d="m 10,281.86772 160.132,0"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="70.13327"
y="250.54721"
id="text1395"><tspan
id="tspan1393"
x="70.13327"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DESCRIPTION</tspan></text>
<text
id="text1399"
y="250.54721"
x="120.16841"
style="font-style:normal;font-weight:normal;font-size:4.00939px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.00939px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="250.54721"
x="120.16841"
id="tspan1397">AUTHOR</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="140.29811"
y="250.54721"
id="text1403"><tspan
id="tspan1401"
x="140.29811"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">ISSUED</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="180.14232"
y="250.54721"
id="text36130"><tspan
id="tspan36128"
x="175.72011"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">NOTES</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="194.37886"
y="255.5477"
id="text36768"><tspan
id="tspan36766"
x="189.95665"
y="255.5477"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DO NOT SCALE DRAWINGS</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 170.132,246.86771 V 287"
id="path36836" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.24897px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="160.29811"
y="250.54721"
id="text36842"><tspan
id="tspan36840"
x="160.29811"
y="250.54721"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
</g>
<g
id="g38171"
transform="translate(-0.25000095)">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,246.86771 V 286.75"
id="path116576" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,276.8677 H 410"
id="path116578" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 390.00031,276.8677 V 287"
id="path116598" />
<text
id="text1421"
y="280.00699"
x="340.28653"
style="font-style:normal;font-weight:normal;font-size:2.9705px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.9705px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none"
y="280.00699"
x="340.28653"
id="tspan1419">DRAWING NUMBER</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:3.05925px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25747;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="393.00568"
y="280.0592"
id="text1429"><tspan
id="tspan1427"
x="393.00568"
y="280.0592"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.05925px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25747;stroke-miterlimit:4;stroke-dasharray:none">REV</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="390.94287"
y="284.13919"
id="text2529"><tspan
id="tspan2527"
x="390.94287"
y="284.13919"
style="stroke-width:0.264583px">{{Revision}}</tspan></text>
<text
id="text2533"
y="284.13919"
x="331.07742"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.265;stroke-dasharray:none"
y="284.13919"
x="331.07742"
id="tspan2531">{{Identification}}</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 329.96563,266.868 H 410"
id="path36736" />
<text
id="text36740"
y="270.05899"
x="339.00919"
style="font-style:normal;font-weight:normal;font-size:3.05925px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25747;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.05925px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25747;stroke-miterlimit:4;stroke-dasharray:none"
y="270.05899"
x="339.00919"
id="tspan36738">DRAWING TITLE</tspan></text>
<text
id="text36744"
y="275.05634"
x="330.99438"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.96055px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.371;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
style="stroke-width:0.371;stroke-dasharray:none"
y="275.05634"
x="330.99438"
id="tspan36742">{{Name}}</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 390.00031,246.86771 V 266.993"
id="path36746" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.88677px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.169851;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="397.04108"
y="249.72177"
id="text36677"><tspan
id="tspan36675"
x="397.04108"
y="249.72177"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.88677px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.169851;stroke-miterlimit:4;stroke-dasharray:none">GRID NORTH</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.88677px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.169851;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="338.77008"
y="249.72177"
id="text36752"><tspan
id="tspan36750"
x="335.76562"
y="249.72177"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.88677px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.169851;stroke-miterlimit:4;stroke-dasharray:none">COMPANY</tspan></text>
<g
id="g37148"
transform="translate(399.87519,258.25776)">
<g
id="g37140"
data-type="grid-north">
<path
id="path11652"
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="M 0,0 V 7.12566 Z" />
<path
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="M 7.3079,3e-5 H -7.30798"
id="path36547" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
id="path36601"
cx="-3.8305665e-05"
cy="4.15625e-05"
r="5" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:round;stroke-dasharray:none"
d="m -5.00004,4e-5 5,-5 5,5 H 3.94162 L -4e-5,-3.94162 -3.9417,4e-5 Z"
id="path36671" />
<path
style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-dasharray:none"
d="M -4e-5,-7.12545 V 3e-5"
id="path36673" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 20 KiB

+7 -2
View File
@@ -126,7 +126,8 @@ class IfcExporter:
continue
try:
if isinstance(obj, bpy.types.Material):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj)
if self.has_changed_shading(obj):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj)
else:
element = tool.Ifc.get_entity(obj)
if element:
@@ -148,11 +149,15 @@ class IfcExporter:
checksum = obj.data.BIMMeshProperties.material_checksum
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
def has_changed_shading(self, obj):
checksum = obj.BIMMaterialProperties.shading_checksum
return checksum != repr(np.array(obj.diffuse_color).tobytes())
def sync_object_placement(self, obj):
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.is_moved(obj):
return
blender_matrix = np.array(obj.matrix_world)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4:
bpy.ops.bim.update_representation(obj=obj.name)
return element
+7 -3
View File
@@ -47,9 +47,9 @@ def mode_callback(obj, data):
):
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
IfcStore.edited_objs.add(obj)
tool.Ifc.edit(obj)
elif IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id).is_a("IfcGridAxis"):
IfcStore.edited_objs.add(obj)
tool.Ifc.edit(obj)
def name_callback(obj, data):
@@ -94,13 +94,17 @@ def name_callback(obj, data):
def color_callback(obj, data):
if obj.BIMMaterialProperties.ifc_style_id:
IfcStore.edited_objs.add(obj)
tool.Ifc.edit(obj)
def active_object_callback():
refresh_ui_data()
def active_material_index_callback(obj, data):
refresh_ui_data()
def subscribe_to(object, data_path, callback):
try:
subscribe_to = object.path_resolve(data_path, False)
+10 -33
View File
@@ -31,9 +31,15 @@ import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
def draw_attributes(props, layout, copy_operator=None):
def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None):
"""you can set attribute active in popup with `active_attribute`
meaning you will be able to type into attribute's field without having to click
on it first
"""
for attribute in props:
row = layout.row(align=True)
if attribute == popup_active_attribute:
row.activate_init = True
draw_attribute(attribute, row, copy_operator)
@@ -64,14 +70,14 @@ def draw_attribute(attribute, layout, copy_operator=None):
text=attribute.name,
)
if attribute.is_uri:
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.data_path = attribute.path_from_id("string_value")
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if copy_operator:
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
op.name = attribute.name
if attribute.is_uri:
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.data_path = attribute.path_from_id("string_value")
def import_attributes(ifc_class, props, data, callback=None):
@@ -267,35 +273,6 @@ def convert_property_group_from_si(property_group, skip_props=()):
setattr(property_group, prop_name, prop_value)
def draw_image_for_ifc_profile(draw, profile, size):
"""generates image based on `profile` using `PIL.ImageDraw`"""
settings = ifcopenshell.geom.settings()
settings.set(settings.INCLUDE_CURVES, True)
shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1]] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
max_x = max([v[0] for v in grouped_verts])
min_x = min([v[0] for v in grouped_verts])
max_y = max([v[1] for v in grouped_verts])
min_y = min([v[1] for v in grouped_verts])
dim_x = max_x - min_x
dim_y = max_y - min_y
max_dim = max([dim_x, dim_y])
scale = 100 / max_dim
for vert in grouped_verts:
vert[0] = round(scale * (vert[0] - min_x)) + ((size / 2) - scale * (dim_x / 2))
vert[1] = round(scale * (vert[1] - min_y)) + ((size / 2) - scale * (dim_y / 2))
for e in grouped_edges:
draw.line((tuple(grouped_verts[e[0]]), tuple(grouped_verts[e[1]])), fill="white", width=2)
class IfcHeaderExtractor:
def __init__(self, filepath: str):
self.filepath = filepath
+3
View File
@@ -51,6 +51,7 @@ class IfcStore:
history = []
future = []
schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
session_files = {}
@staticmethod
def purge():
@@ -71,6 +72,7 @@ class IfcStore:
IfcStore.history = []
IfcStore.future = []
IfcStore.schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
IfcStore.session_files = {}
@staticmethod
def get_file():
@@ -318,6 +320,7 @@ class IfcStore:
blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback)
elif isinstance(obj, bpy.types.Object):
blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback)
blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback)
for listener in IfcStore.element_listeners:
listener(element, obj)
+59 -24
View File
@@ -302,12 +302,12 @@ class IfcImporter:
)
if self.body_contexts:
self.settings.set_context_ids(self.body_contexts)
# Annotation is to accommodate broken Revit files
# Annotation ContextType is to accommodate broken Revit files
# See https://github.com/Autodesk/revit-ifc/issues/187
self.plan_contexts = [
c.id()
for c in self.file.by_type("IfcGeometricRepresentationContext")
if c.ContextType in ["Plan", "Annotation"]
if c.ContextType in ["Plan", "Annotation"] or c.ContextIdentifier == "Annotation"
]
if self.plan_contexts:
self.settings_2d.set_context_ids(self.plan_contexts)
@@ -335,14 +335,23 @@ class IfcImporter:
if self.ifc_import_settings.has_filter or offset or offset_limit < len(self.elements):
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
if self.file.schema in ("IFC2X3", "IFC4"):
if self.file.schema == "IFC2X3":
self.element_types = set(
self.file.by_type("IfcElementType")
+ self.file.by_type("IfcDoorStyle")
+ self.file.by_type("IfcWindowStyle")
)
elif self.file.schema == "IFC4":
self.element_types = set(
self.file.by_type("IfcElementType")
+ self.file.by_type("IfcDoorStyle")
+ self.file.by_type("IfcWindowStyle")
+ self.file.by_type("IfcSpatialElementType")
)
else:
self.element_types = set(self.file.by_type("IfcElementType"))
self.element_types = set(
self.file.by_type("IfcElementType") + self.file.by_type("IfcSpatialElementType")
)
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
@@ -616,37 +625,33 @@ class IfcImporter:
def create_element_type(self, element):
self.ifc_import_settings.logger.info("Creating object %s", element)
representation_map = self.get_type_product_body_representation_map(element)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
mesh = None
if representation_map:
representation = representation_map.MappedRepresentation
if representation:
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
mesh = self.meshes.get(mesh_name)
if mesh is None:
shape = None
try:
shape = ifcopenshell.geom.create_shape(self.settings, representation_map.MappedRepresentation)
shape = ifcopenshell.geom.create_shape(self.settings, representation)
except:
try:
shape = ifcopenshell.geom.create_shape(self.settings_2d, representation)
except:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
if shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
except:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
self.link_element(element, obj)
self.material_creator.create(element, obj, mesh)
self.type_products[element.GlobalId] = obj
def get_type_product_body_representation_map(self, element):
if not element.RepresentationMaps:
return
for representation_map in element.RepresentationMaps:
context = representation_map.MappedRepresentation.ContextOfItems
if (
context.ContextType == "Model"
and context.ContextIdentifier == "Body"
and context.TargetView == "MODEL_VIEW"
):
return representation_map
def create_native_elements(self):
progress = 0
checkpoint = time.time()
@@ -985,9 +990,37 @@ class IfcImporter:
self.convert_representation(representation)
mesh = bpy.data.meshes.new("Native")
props = bpy.context.scene.BIMGeoreferenceProperties
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
if props.has_blender_offset and self.is_point_far_away(self.mesh_data["co"][0:3], is_meters=False):
offset_point = np.linalg.inv(mat) @ np.array(
(
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
0.0,
)
)
verts = [None] * len(self.mesh_data["co"])
for i in range(0, len(self.mesh_data["co"]), 3):
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
self.mesh_data["co"][i] * self.unit_scale,
self.mesh_data["co"][i + 1] * self.unit_scale,
self.mesh_data["co"][i + 2] * self.unit_scale,
offset_point[0] * self.unit_scale,
offset_point[1] * self.unit_scale,
offset_point[2] * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
)
mesh["has_cartesian_point_offset"] = True
else:
verts = [c * self.unit_scale for c in self.mesh_data["co"]]
mesh["has_cartesian_point_offset"] = False
mesh.vertices.add(self.mesh_data["total_verts"])
mesh.vertices.foreach_set("co", [c * self.unit_scale for c in self.mesh_data["co"]])
# mesh.vertices.foreach_set("co", self.mesh_data["co"])
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(len(self.mesh_data["vertex_index"]))
mesh.loops.foreach_set("vertex_index", self.mesh_data["vertex_index"])
mesh.polygons.add(self.mesh_data["total_polygons"])
@@ -1497,6 +1530,8 @@ class IfcImporter:
if rendering_style and texture_style:
self.create_surface_style_with_textures(blender_material, rendering_style, texture_style)
tool.Style.record_shading(blender_material)
def create_surface_style_shading(self, blender_material, surface_style):
alpha = 1.0
# Transparency was added in IFC4
@@ -34,7 +34,6 @@ class AggregateData:
cls.data = {
"has_relating_object": cls.has_relating_object(),
"relating_object_label": cls.get_relating_object_label(),
"relating_object_id": cls.get_relating_object_id(),
"has_related_objects": cls.has_related_objects(),
"related_objects_amount": cls.get_related_objects_amount(),
"ifc_class": cls.ifc_class(),
@@ -55,12 +54,6 @@ class AggregateData:
if aggregate:
return f"{aggregate.is_a()}/{aggregate.Name or ''}"
@classmethod
def get_relating_object_id(cls) -> int:
aggregate = cls.get_relating_object()
if aggregate:
return aggregate.id()
@classmethod
def get_related_objects(cls):
return ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(bpy.context.active_object))
@@ -58,16 +58,22 @@ class BIM_OT_unassign_object(bpy.types.Operator, Operator):
bl_idname = "bim.unassign_object"
bl_label = "Unassign Object"
bl_options = {"REGISTER", "UNDO"}
relating_object: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
def _execute(self, context):
core.unassign_object(
tool.Ifc,
tool.Collector,
relating_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object)),
related_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.related_object)),
)
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
continue
core.unassign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(aggregate),
related_obj=tool.Ifc.get_object(element),
)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
@@ -92,52 +98,61 @@ class BIM_OT_disable_editing_aggregate(bpy.types.Operator, Operator):
core.disable_editing_aggregate(tool.Aggregate, obj=context.active_object)
class BIM_OT_add_aggregate(bpy.types.Operator):
class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
"""Add aggregate to IFC element"""
bl_idname = "bim.add_aggregate"
bl_label = "Add Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
row = self.layout
row.prop(self, "ifc_class")
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
element = tool.Ifc.get_entity(obj)
if not element:
return {"FINISHED"}
try:
ifc_class = tool.Ifc.schema().declaration_by_name(self.ifc_class).name()
except:
return
aggregate = self.create_aggregate(context, ifc_class)
aggregate = self.create_aggregate(context)
tool.Collector.sync(obj)
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
current_container = ifcopenshell.util.element.get_container(element)
if current_aggregate:
core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(current_aggregate),
related_obj=aggregate,
)
elif current_container:
blenderbim.core.spatial.assign_container(
tool.Ifc,
tool.Collector,
tool.Spatial,
structure_obj=tool.Ifc.get_object(current_container),
element_obj=aggregate,
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
return {"FINISHED"}
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
def create_aggregate(self, context):
aggregate_collection = bpy.data.collections.new("IfcElementAssembly/Assembly")
tool.Collector.sync(obj)
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
current_container = ifcopenshell.util.element.get_container(element)
if current_aggregate:
core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(current_aggregate),
related_obj=aggregate,
)
elif current_container:
blenderbim.core.spatial.assign_container(
tool.Ifc,
tool.Collector,
tool.Spatial,
structure_obj=tool.Ifc.get_object(current_container),
element_obj=aggregate,
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context, ifc_class):
aggregate_collection = bpy.data.collections.new(f"{ifc_class}/Assembly")
context.scene.collection.children.link(aggregate_collection)
aggregate = bpy.data.objects.new("Assembly", None)
aggregate_collection.objects.link(aggregate)
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class="IfcElementAssembly")
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
return aggregate
@@ -67,8 +67,6 @@ class BIM_PT_aggregate(Panel):
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
op = row.operator("bim.unassign_object", icon="X", text="")
op.relating_object = AggregateData.data["relating_object_id"]
op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id
else:
row.label(text="No Aggregate", icon="TRIA_UP")
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
@@ -20,43 +20,44 @@ import bpy
from . import ui, prop, operator
classes = (
operator.NewBcfProject,
operator.LoadBcfProject,
operator.LoadBcfTopics,
operator.LoadBcfTopic,
operator.LoadBcfComments,
operator.EditBcfProjectName,
operator.EditBcfAuthor,
operator.EditBcfTopicName,
operator.EditBcfTopic,
operator.SaveBcfProject,
operator.AddBcfTopic,
operator.AddBcfHeaderFile,
operator.AddBcfBimSnippet,
operator.AddBcfReferenceLink,
operator.AddBcfDocumentReference,
operator.AddBcfLabel,
operator.AddBcfRelatedTopic,
operator.ViewBcfTopic,
operator.RemoveBcfTopic,
operator.RemoveBcfComment,
operator.RemoveBcfBimSnippet,
operator.RemoveBcfReferenceLink,
operator.RemoveBcfDocumentReference,
operator.RemoveBcfRelatedTopic,
operator.EditBcfReferenceLinks,
operator.EditBcfLabels,
operator.RemoveBcfLabel,
operator.EditBcfComment,
operator.AddBcfComment,
operator.ActivateBcfViewpoint,
operator.AddBcfBimSnippet,
operator.AddBcfComment,
operator.AddBcfDocumentReference,
operator.AddBcfHeaderFile,
operator.AddBcfLabel,
operator.AddBcfReferenceLink,
operator.AddBcfRelatedTopic,
operator.AddBcfTopic,
operator.AddBcfViewpoint,
operator.RemoveBcfViewpoint,
operator.RemoveBcfFile,
operator.EditBcfAuthor,
operator.EditBcfComment,
operator.EditBcfLabels,
operator.EditBcfProjectName,
operator.EditBcfReferenceLinks,
operator.EditBcfTopic,
operator.EditBcfTopicName,
operator.ExtractBcfFile,
operator.LoadBcfComments,
operator.LoadBcfProject,
operator.LoadBcfTopic,
operator.LoadBcfTopics,
operator.NewBcfProject,
operator.OpenBcfReferenceLink,
operator.SelectBcfHeaderFile,
operator.RemoveBcfBimSnippet,
operator.RemoveBcfComment,
operator.RemoveBcfDocumentReference,
operator.RemoveBcfFile,
operator.RemoveBcfLabel,
operator.RemoveBcfReferenceLink,
operator.RemoveBcfRelatedTopic,
operator.RemoveBcfTopic,
operator.RemoveBcfViewpoint,
operator.SaveBcfProject,
operator.SelectBcfBimSnippetReference,
operator.SelectBcfDocumentReference,
operator.SelectBcfHeaderFile,
operator.ViewBcfTopic,
prop.BcfReferenceLink,
prop.BcfLabel,
prop.BcfBimSnippet,
@@ -20,14 +20,20 @@ import os
import bpy
import bcf
import bcf.bcfxml
import bcf.v2.model
import uuid
import numpy as np
import tempfile
import webbrowser
import ifcopenshell
import ifcopenshell.util.unit
from . import bcfstore
import blenderbim.bim.module.bcf.prop as bcf_prop
from . import bcfstore
from pathlib import Path
from blenderbim.bim.ifc import IfcStore
from math import radians, degrees, atan, tan, cos, sin
from mathutils import Vector, Matrix, Euler, geometry
from xsdata.models.datatype import XmlDateTime
class NewBcfProject(bpy.types.Operator):
@@ -56,7 +62,6 @@ class LoadBcfProject(bpy.types.Operator):
if self.filepath:
bcfstore.BcfStore.bcfxml = bcf.bcfxml.load(self.filepath)
bcfxml = bcfstore.BcfStore.get_bcfxml()
bcfxml.get_project()
context.scene.BCFProperties.name = bcfxml.project.name
bpy.ops.bim.load_bcf_topics()
context.scene.BCFProperties.is_loaded = True
@@ -74,7 +79,6 @@ class LoadBcfTopics(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
bcfxml.get_topics()
context.scene.BCFProperties.topics.clear()
for index, topic_guid in enumerate(bcfxml.topics.keys()):
new = context.scene.BCFProperties.topics.add()
@@ -91,7 +95,7 @@ class LoadBcfTopic(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = bcfxml.get_topic(self.topic_guid)
topic = bcfxml.topics[self.topic_guid]
bcfxml.get_header(self.topic_guid)
new = context.scene.BCFProperties.topics[self.topic_index]
data_map = {
@@ -125,10 +129,10 @@ class LoadBcfTopic(bpy.types.Operator):
if topic.topic.bim_snippet:
data_map = {
"type": topic.bim_snippet.snippet_type,
"is_external": topic.bim_snippet.is_external,
"reference": topic.bim_snippet.reference,
"schema": topic.bim_snippet.reference_schema,
"type": topic.topic.bim_snippet.snippet_type,
"is_external": topic.topic.bim_snippet.is_external,
"reference": topic.topic.bim_snippet.reference,
"schema": topic.topic.bim_snippet.reference_schema,
}
for key, value in data_map.items():
if value is not None:
@@ -164,10 +168,9 @@ class LoadBcfComments(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
bcfxml.get_comments(self.topic_guid)
blender_topic = context.scene.BCFProperties.topics.get(self.topic_guid)
blender_topic.comments.clear()
for comment in bcfxml.topics[self.topic_guid].comments.values():
for comment in bcfxml.topics[self.topic_guid].comments:
new = blender_topic.comments.add()
data_map = {
"name": comment.guid,
@@ -192,7 +195,6 @@ class EditBcfProjectName(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
bcfxml.project.name = context.scene.BCFProperties.name
bcfxml.edit_project()
return {"FINISHED"}
@@ -218,7 +220,6 @@ class EditBcfTopicName(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = bcfxml.topics[blender_topic.name]
topic.title = blender_topic.title
bcfxml.edit_topic(topic)
return {"FINISHED"}
@@ -242,7 +243,6 @@ class EditBcfTopic(bpy.types.Operator):
topic.topic_status = blender_topic.status or None
topic.topic_type = blender_topic.type or None
bcfxml.edit_topic(topic)
props.refresh_topic(context)
return {"FINISHED"}
@@ -256,7 +256,7 @@ class SaveBcfProject(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
bcfxml.save_project(self.filepath)
bcfxml.save(self.filepath)
return {"FINISHED"}
def invoke(self, context, event):
@@ -299,15 +299,17 @@ class AddBcfBimSnippet(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
bim_snippet = bcf.v2.data.BimSnippet()
bim_snippet.reference = props.bim_snippet_reference
bim_snippet.reference_schema = props.bim_snippet_schema
bim_snippet.snippet_type = props.bim_snippet_type
bcfxml.add_bim_snippet(topic, bim_snippet)
is_external = "://" in props.bim_snippet_reference
bim_snippet = bcf.v2.model.BimSnippet(
reference=props.bim_snippet_reference if is_external else Path(props.bim_snippet_reference).name,
reference_schema=props.bim_snippet_schema,
snippet_type=props.bim_snippet_type,
is_external=is_external,
)
topic.topic.bim_snippet = bim_snippet
with open(props.bim_snippet_reference, "r") as f:
topic.bim_snippet = f.read()
bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index)
bim_snippet.reference = ""
bim_snippet.reference_schema = ""
bim_snippet.snippet_type = ""
return {"FINISHED"}
@@ -326,15 +328,14 @@ class AddBcfRelatedTopic(bpy.types.Operator):
if props.related_topic == blender_topic.title:
# Prevent adding self as related topic
return False
related_topic = None
related_topic_guid = None
for topic in bcfxml.topics.values():
if topic.title == props.related_topic:
related_topic = bcf.v2.data.RelatedTopic()
related_topic.guid = topic.guid
if topic.topic.title == props.related_topic:
related_topic_guid = topic.guid
break
if not related_topic:
if not related_topic_guid:
return False
if str(related_topic.guid) in [t.name for t in blender_topic.related_topics]:
if str(related_topic_guid) in [t.name for t in blender_topic.related_topics]:
# Prevent adding the same related topic more than once
return False
return True
@@ -343,11 +344,12 @@ class AddBcfRelatedTopic(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
props = context.scene.BCFProperties
blender_topic = props.active_topic
related_topic = bcf.v2.data.RelatedTopic()
related_topic.guid = next((t for t in bcfxml.topics.values() if t.title == props.related_topic)).guid
topic = bcfxml.topics[blender_topic.name]
topic.related_topics.append(related_topic)
bcfxml.edit_topic(topic)
topic.topic.related_topic.append(
bcf.v2.model.TopicRelatedTopic(
guid=next((t for t in bcfxml.topics.values() if t.topic.title == props.related_topic)).guid
)
)
bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index)
props.related_topic = ""
return {"FINISHED"}
@@ -367,15 +369,24 @@ class AddBcfHeaderFile(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
header_file = bcf.v2.data.HeaderFile()
header_file.reference = props.file_reference
if not os.path.exists(header_file.reference):
header_file.filename = header_file.reference
if len(props.file_ifc_project) == 22:
header_file.ifc_project = props.file_ifc_project
if len(props.file_ifc_spatial_structure_element) == 22:
header_file.ifc_spatial_structure_element = props.file_ifc_spatial_structure_element
bcfxml.add_file(topic, header_file)
is_external = "://" in props.file_reference
filename = Path(props.file_reference).name if os.path.exists(props.file_reference) else None
if filename:
with open(props.file_reference, "r") as f:
topic.reference_files[filename] = f.read()
header_file = bcf.v2.model.HeaderFile(
filename=filename,
date=XmlDateTime.now(),
reference=props.file_reference if is_external else Path(props.file_reference).name,
ifc_project=props.file_ifc_project,
ifc_spatial_structure_element=props.file_ifc_spatial_structure_element,
is_external=is_external,
)
topic.header.file.append(header_file)
props.refresh_topic(context)
return {"FINISHED"}
@@ -409,27 +420,34 @@ class AddBcfViewpoint(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
viewpoint = bcf.v2.data.Viewpoint()
if blender_camera.data.type == "ORTHO":
camera = bcf.v2.data.OrthogonalCamera()
camera.view_to_world_scale = blender_camera.data.ortho_scale
viewpoint.orthogonal_camera = camera
elif blender_camera.data.type == "PERSP":
camera = bcf.v2.data.PerspectiveCamera()
camera.field_of_view = degrees(blender_camera.data.angle)
viewpoint.perspective_camera = camera
camera.camera_view_point.x = blender_camera.location.x
camera.camera_view_point.y = blender_camera.location.y
camera.camera_view_point.z = blender_camera.location.z
direction = blender_camera.matrix_world.to_quaternion() @ Vector((0.0, 0.0, -1.0))
camera.camera_direction.x = direction.x
camera.camera_direction.y = direction.y
camera.camera_direction.z = direction.z
up = blender_camera.matrix_world.to_quaternion() @ Vector((0.0, 1.0, 0.0))
camera.camera_up_vector.x = up.x
camera.camera_up_vector.y = up.y
camera.camera_up_vector.z = up.z
camera_view_point = bcf.v2.model.Point(
x=blender_camera.location.x, y=blender_camera.location.y, z=blender_camera.location.z
)
camera_direction = bcf.v2.model.Direction(x=direction.x, y=direction.y, z=direction.z)
camera_up_vector = bcf.v2.model.Direction(x=up.x, y=up.y, z=up.z)
if blender_camera.data.type == "ORTHO":
camera = bcf.v2.model.OrthogonalCamera(
view_to_world_scale=blender_camera.data.ortho_scale,
camera_view_point=camera_view_point,
camera_direction=camera_direction,
camera_up_vector=camera_up_vector,
)
visualization_info = bcf.v2.model.VisualizationInfo(orthogonal_camera=camera)
elif blender_camera.data.type == "PERSP":
camera = bcf.v2.model.PerspectiveCamera(
field_of_view=degrees(blender_camera.data.angle),
camera_view_point=camera_view_point,
camera_direction=camera_direction,
camera_up_vector=camera_up_vector,
)
visualization_info = bcf.v2.model.VisualizationInfo(guid=str(uuid.uuid4()), perspective_camera=camera)
# TODO allow the user to enable or disable snapshotting
snapshot = None
blender_render = context.scene.render
old_file_format = blender_render.image_settings.file_format
@@ -437,9 +455,17 @@ class AddBcfViewpoint(bpy.types.Operator):
old_filepath = blender_render.filepath
blender_render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
bpy.ops.render.opengl(write_still=True)
viewpoint.snapshot = blender_render.filepath
with open(blender_render.filepath, "rb") as f:
snapshot = f.read()
# viewpoint.snapshot = blender_render.filepath
# TODO we should handle selection and visibility state
vizinfo = bcf.v2.visinfo.VisualizationInfoHandler(visualization_info=visualization_info, snapshot=snapshot)
topic.viewpoints[vizinfo.guid + ".bcfv"] = vizinfo
topic.markup.viewpoints.append(
bcf.v2.model.ViewPoint(viewpoint=vizinfo.guid + ".bcfv", guid=vizinfo.guid, snapshot=vizinfo.guid + ".png")
)
bcfxml.add_viewpoint(topic, viewpoint)
blender_render.filepath = old_filepath
blender_render.image_settings.file_format = old_file_format
props.refresh_topic(context)
@@ -459,9 +485,15 @@ class RemoveBcfViewpoint(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
props = context.scene.BCFProperties
blender_topic = props.active_topic
viewpoint_guid = blender_topic.viewpoints
topic = bcfxml.topics[blender_topic.name]
bcfxml.delete_viewpoint(viewpoint_guid, topic)
for key, viewpoint in topic.viewpoints.items():
if viewpoint.guid == blender_topic.viewpoints:
break
del topic.viewpoints[key]
for i, viewpoint in enumerate(topic.markup.viewpoints):
if viewpoint.guid == blender_topic.viewpoints:
break
del topic.markup.viewpoints[i]
props.refresh_topic(context)
return {"FINISHED"}
@@ -477,7 +509,7 @@ class RemoveBcfFile(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
bcfxml.delete_file(topic, self.index)
del topic.header.file[self.index]
props.refresh_topic(context)
return {"FINISHED"}
@@ -495,8 +527,7 @@ class RemoveBcfTopic(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
props = context.scene.BCFProperties
topic_to_delete = props.active_topic
bcfxml.delete_topic(topic_to_delete.name)
del bcfxml.topics[props.active_topic.name]
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -515,8 +546,7 @@ class AddBcfReferenceLink(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
topic.reference_links.append(props.reference_link)
bcfxml.edit_topic(topic)
topic.topic.reference_link.append(props.reference_link)
bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index)
props.reference_link = ""
return {"FINISHED"}
@@ -536,10 +566,23 @@ class AddBcfDocumentReference(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
document_reference = bcf.v2.data.DocumentReference()
document_reference.referenced_document = props.document_reference
document_reference.description = props.document_reference_description or None
bcfxml.add_document_reference(topic, document_reference)
is_external = "://" in props.document_reference
filename = Path(props.document_reference).name if os.path.exists(props.document_reference) else None
if filename:
with open(props.document_reference, "rb") as f:
topic.document_references[filename] = f.read()
document_reference = bcf.v2.model.TopicDocumentReference(
referenced_document=props.document_reference if is_external else filename,
description=props.document_reference_description or None,
guid=str(uuid.uuid4()),
is_external=is_external,
)
topic.topic.document_reference.append(document_reference)
bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index)
props.document_reference = ""
props.document_reference_description = ""
@@ -562,8 +605,7 @@ class AddBcfLabel(bpy.types.Operator):
topic = bcfxml.topics[blender_topic.name]
new = blender_topic.labels.add()
new.name = props.label
topic.labels.append(props.label)
bcfxml.edit_topic(topic)
topic.topic.labels.append(props.label)
props.label = ""
return {"FINISHED"}
@@ -578,11 +620,10 @@ class EditBcfReferenceLinks(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
for index, reference_link in enumerate(topic.reference_links):
for index, reference_link in enumerate(topic.topic.reference_link):
if reference_link == blender_topic.reference_links[index].name:
continue
topic.reference_links[index] = blender_topic.reference_links[index].name
bcfxml.edit_topic(topic)
topic.topic.reference_link[index] = blender_topic.reference_links[index].name
return {"FINISHED"}
@@ -597,12 +638,11 @@ class EditBcfLabels(bpy.types.Operator):
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
for index, label in enumerate(blender_topic.labels):
if index >= len(topic.labels):
if index >= len(topic.topic.labels):
break
if label.name == topic.labels[index]:
if label.name == topic.topic.labels[index]:
continue
topic.labels[index] = blender_topic.labels[index].name
bcfxml.edit_topic(topic)
topic.topic.labels[index] = blender_topic.labels[index].name
return {"FINISHED"}
@@ -617,8 +657,7 @@ class RemoveBcfReferenceLink(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
del topic.reference_links[self.index]
bcfxml.edit_topic(topic)
del topic.topic.reference_link[self.index]
blender_topic.reference_links.remove(self.index)
return {"FINISHED"}
@@ -634,8 +673,7 @@ class RemoveBcfLabel(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
del topic.labels[self.index]
bcfxml.edit_topic(topic)
del topic.topic.labels[self.index]
blender_topic.labels.remove(self.index)
return {"FINISHED"}
@@ -650,7 +688,7 @@ class RemoveBcfBimSnippet(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
bcfxml.delete_bim_snippet(topic)
topic.topic.bim_snippet = None
blender_topic.bim_snippet.schema = ""
blender_topic.bim_snippet.reference = ""
blender_topic.bim_snippet.type = ""
@@ -668,7 +706,7 @@ class RemoveBcfDocumentReference(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
bcfxml.delete_document_reference(topic, self.index)
del topic.topic.document_reference[self.index]
bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index)
return {"FINISHED"}
@@ -684,8 +722,7 @@ class RemoveBcfRelatedTopic(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
del topic.related_topics[self.index]
bcfxml.edit_topic(topic)
del topic.topic.related_topic[self.index]
bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index)
return {"FINISHED"}
@@ -701,7 +738,10 @@ class RemoveBcfComment(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
bcfxml.delete_comment(self.comment_guid, topic)
for i, comment in enumerate(topic.comments):
if comment.guid == self.comment_guid:
break
del topic.comments[i]
bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid)
return {"FINISHED"}
@@ -718,9 +758,11 @@ class EditBcfComment(bpy.types.Operator):
blender_topic = props.active_topic
blender_comment = blender_topic.comments.get(self.comment_guid)
topic = bcfxml.topics[blender_topic.name]
comment = topic.comments[self.comment_guid]
comment.comment = blender_comment.comment
bcfxml.edit_comment(comment, topic)
for i, comment in enumerate(topic.comments):
if comment.guid == self.comment_guid:
comment.comment = blender_comment.comment
comment.modified_date = XmlDateTime.now()
comment.modified_author = context.scene.BCFProperties.author
bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid)
return {"FINISHED"}
@@ -745,12 +787,15 @@ class AddBcfComment(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
comment = bcf.v2.data.Comment()
comment.comment = props.comment
comment = bcf.v2.model.Comment(
date=XmlDateTime.now(),
author=context.scene.BCFProperties.author,
comment=props.comment,
guid=str(uuid.uuid4()),
)
if props.has_related_viewpoint:
comment.viewpoint = bcf.v2.data.Viewpoint()
comment.viewpoint.guid = blender_topic.viewpoints
bcfxml.add_comment(topic, comment)
comment.viewpoint = bcf.v2.model.CommentViewpoint(guid=blender_topic.viewpoints)
topic.comments.append(comment)
bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid)
props.comment = ""
props.has_related_viewpoint = False
@@ -791,11 +836,13 @@ class ActivateBcfViewpoint(bpy.types.Operator):
cam_height = context.scene.render.resolution_y
cam_aspect = cam_width / cam_height
obj.data.background_images.clear()
if viewpoint.snapshot:
obj.data.show_background_images = True
obj.data.background_images.clear()
background = obj.data.background_images.new()
background.image = bpy.data.images.load(os.path.join(bcfxml.filepath, topic.guid, viewpoint.snapshot))
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(viewpoint.snapshot)
background.image = bpy.data.images.load(f.name)
src_width = background.image.size[0]
src_height = background.image.size[1]
src_aspect = src_width / src_height
@@ -805,6 +852,9 @@ class ActivateBcfViewpoint(bpy.types.Operator):
else:
background.frame_method = "CROP"
background.display_depth = "FRONT"
else:
obj.data.show_background_images = False
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].region_3d.view_perspective = "CAMERA"
@@ -814,27 +864,27 @@ class ActivateBcfViewpoint(bpy.types.Operator):
gp = bpy.data.grease_pencils.get("BCF")
if gp:
bpy.data.grease_pencils.remove(gp)
if viewpoint.lines:
if viewpoint.visualization_info.lines:
self.draw_lines(viewpoint, context)
self.delete_clipping_planes(context)
if viewpoint.clipping_planes:
if viewpoint.visualization_info.clipping_planes:
self.create_clipping_planes(viewpoint)
self.delete_bitmaps(context)
if viewpoint.bitmaps:
if viewpoint.visualization_info.bitmap:
self.create_bitmaps(bcfxml, viewpoint, topic)
self.setup_camera(viewpoint, obj, cam_aspect, context)
return {"FINISHED"}
def setup_camera(self, viewpoint, obj, cam_aspect, context):
if viewpoint.orthogonal_camera:
camera = viewpoint.orthogonal_camera
if viewpoint.visualization_info.orthogonal_camera:
camera = viewpoint.visualization_info.orthogonal_camera
obj.data.type = "ORTHO"
obj.data.ortho_scale = viewpoint.orthogonal_camera.view_to_world_scale
elif viewpoint.perspective_camera:
camera = viewpoint.perspective_camera
obj.data.ortho_scale = viewpoint.visualization_info.orthogonal_camera.view_to_world_scale
elif viewpoint.visualization_info.perspective_camera:
camera = viewpoint.visualization_info.perspective_camera
obj.data.type = "PERSP"
if cam_aspect >= 1:
obj.data.angle = radians(camera.field_of_view)
@@ -875,15 +925,15 @@ class ActivateBcfViewpoint(bpy.types.Operator):
obj.matrix_world = Matrix(matrix.tolist())
def set_viewpoint_components(self, viewpoint, context):
if not viewpoint.components:
if not viewpoint.visualization_info.components:
return
# Operators with context overrides are used because they are
# significantly faster than looping through all objects
exception_global_ids = {v.ifc_guid for v in viewpoint.components.visibility.exceptions}
exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions or []}
if viewpoint.components.visibility.default_visibility:
if viewpoint.visualization_info.components.visibility.default_visibility:
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.hide_view_clear()
@@ -907,11 +957,13 @@ class ActivateBcfViewpoint(bpy.types.Operator):
bpy.ops.object.hide_view_set(context_override, unselected=True)
context.area.type = old
if viewpoint.components.view_setup_hints:
if not viewpoint.components.view_setup_hints.spaces_visible:
if viewpoint.visualization_info.components.view_setup_hints:
if not viewpoint.visualization_info.components.view_setup_hints.spaces_visible:
self.hide_spaces(context)
if viewpoint.components.view_setup_hints.openings_visible is not None:
self.set_openings_visibility(viewpoint.components.view_setup_hints.openings_visible, context)
if viewpoint.visualization_info.components.view_setup_hints.openings_visible is not None:
self.set_openings_visibility(
viewpoint.visualization_info.components.view_setup_hints.openings_visible, context
)
else:
self.hide_spaces(context)
self.set_openings_visibility(False, context)
@@ -927,11 +979,12 @@ class ActivateBcfViewpoint(bpy.types.Operator):
context.area.type = old
def set_openings_visibility(self, is_visible, context):
for collection in self.get_opening_collections(context):
collection.hide_viewport = not is_visible
pass # We no longer have an openings collection
def set_selection(self, viewpoint):
selected_global_ids = [s.ifc_guid for s in viewpoint.components.selection]
if not viewpoint.visualization_info.components or not viewpoint.visualization_info.components.selection:
return
selected_global_ids = [s.ifc_guid for s in viewpoint.visualization_info.components.selection.component or []]
bpy.ops.object.select_all(action="DESELECT")
for global_id in selected_global_ids:
obj = IfcStore.get_element(global_id)
@@ -940,7 +993,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
def set_colours(self, viewpoint):
global_id_colours = {}
for coloring in viewpoint.components.coloring:
for coloring in viewpoint.visualization_info.components.coloring or []:
for component in coloring.components:
global_id_colours.setdefault(component.ifc_guid, coloring.color)
for global_id, color in global_id_colours.items():
@@ -948,14 +1001,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
if obj:
obj.color = self.hex_to_rgb(color)
def get_opening_collections(self, context):
collections = []
for collection in context.view_layer.layer_collection.children:
opening_collection = collection.children.get("IfcOpeningElements")
if opening_collection:
collections.append(opening_collection)
return collections
def draw_lines(self, viewpoint, context):
gp = bpy.data.grease_pencils.new("BCF")
scene = context.scene
@@ -967,9 +1012,9 @@ class ActivateBcfViewpoint(bpy.types.Operator):
frame = layer.frames.new(1)
stroke = frame.strokes.new()
stroke.display_mode = "3DSPACE"
stroke.points.add(len(viewpoint.lines) * 2)
stroke.points.add(len(viewpoint.visualization_info.lines.line) * 2)
coords = []
for l in viewpoint.lines:
for l in viewpoint.visualization_info.lines.line:
coords.extend(
[l.start_point.x, l.start_point.y, l.start_point.z, l.end_point.x, l.end_point.y, l.end_point.z]
)
@@ -977,7 +1022,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
def create_clipping_planes(self, viewpoint):
n = 0
for plane in viewpoint.clipping_planes:
for plane in viewpoint.visualization_info.clipping_planes.clipping_plane:
bpy.ops.bim.add_section_plane()
if n == 0:
obj = bpy.data.objects["Section"]
@@ -1010,10 +1055,14 @@ class ActivateBcfViewpoint(bpy.types.Operator):
collection = bpy.data.collections.get("Bitmaps")
if not collection:
collection = bpy.data.collections.new("Bitmaps")
for bitmap in viewpoint.bitmaps:
for bitmap in viewpoint.visualization_info.bitmap:
obj = bpy.data.objects.new("Bitmap", None)
obj.empty_display_type = "IMAGE"
image = bpy.data.images.load(os.path.join(bcfxml.filepath, topic.guid, bitmap.reference))
# image = bpy.data.images.load(os.path.join(bcfxml.filepath, topic.guid, bitmap.reference))
with tempfile.NamedTemporaryFile(delete=False) as f:
topic.extract_file(bitmap, f.name)
# f.write(bitmap.what)
image = bpy.data.images.load(f.name)
src_width = image.size[0]
src_height = image.size[1]
if src_height > src_width:
@@ -1052,6 +1101,7 @@ class SelectBcfHeaderFile(bpy.types.Operator):
bl_label = "Select BCF Header File"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
def execute(self, context):
if self.filepath:
@@ -1093,3 +1143,25 @@ class SelectBcfDocumentReference(bpy.types.Operator):
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExtractBcfFile(bpy.types.Operator):
bl_idname = "bim.extract_bcf_file"
bl_label = "Extract BCF Header File"
bl_options = {"REGISTER", "UNDO"}
entity_type: bpy.props.StringProperty()
index: bpy.props.IntProperty()
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
if self.entity_type == "HEADER_FILE":
entity = topic.header.file[self.index]
elif self.entity_type == "BIM_SNIPPET":
entity = topic.markup.topic.bim_snippet
elif self.entity_type == "DOCUMENT_REFERENCE":
entity = topic.markup.topic.document_reference[self.index]
webbrowser.open(str(topic.extract_file(entity).parent))
return {"FINISHED"}
@@ -95,8 +95,8 @@ def getBcfViewpoints(self, context, force_update=False):
props = context.scene.BCFProperties
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = props.active_topic
viewpoints = bcfxml.get_viewpoints(topic.name) if topic else {}
bcfviewpoints_enum.extend([(v, f"Viewpoint {i+1}", "") for i, v in enumerate(viewpoints.keys())])
viewpoints = bcfxml.topics[topic.name].viewpoints.keys() if topic else []
bcfviewpoints_enum.extend([(v, f"Viewpoint {i+1}", "") for i, v in enumerate(viewpoints)])
return bcfviewpoints_enum
+12 -9
View File
@@ -127,20 +127,21 @@ class BIM_PT_bcf_metadata(Panel):
layout.label(text="Header Files:")
if bcf_topic.header:
for index, f in enumerate(bcf_topic.header.files):
for index, f in enumerate(bcf_topic.header.file):
box = self.layout.box()
row = box.row(align=True)
row.label(text=f.filename, icon="FILE_BLANK")
if f.is_external:
row.operator("bim.open_uri", icon="URL", text="").uri = f.reference
else:
op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="")
op.uri = os.path.join(bcfxml.filepath, topic.name, f.reference)
op = row.operator("bim.extract_bcf_file", icon="FILE_FOLDER", text="")
op.index = index
op.entity_type = "HEADER_FILE"
row.operator("bim.remove_bcf_file", icon="X", text="").index = index
row = box.row()
row.label(text="Date")
row.label(text=f.date)
row.label(text=str(f.date))
if f.ifc_project:
row = box.row()
@@ -196,8 +197,8 @@ class BIM_PT_bcf_metadata(Panel):
if topic.bim_snippet.is_external:
row.operator("bim.open_uri", icon="URL", text="").uri = topic.bim_snippet.reference
else:
op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="")
op.uri = os.path.join(bcfxml.filepath, topic.name, topic.bim_snippet.reference)
op = row.operator("bim.extract_bcf_file", icon="FILE_FOLDER", text="")
op.entity_type = "BIM_SNIPPET"
row.operator("bim.remove_bcf_bim_snippet", icon="X", text="")
else:
row = layout.row(align=True)
@@ -218,8 +219,10 @@ class BIM_PT_bcf_metadata(Panel):
if doc.is_external:
row.operator("bim.open_uri", icon="URL", text="").uri = doc.reference
else:
op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="")
op.uri = os.path.join(bcfxml.filepath, topic.name, doc.reference)
op = row.operator("bim.extract_bcf_file", icon="FILE_FOLDER", text="")
op.entity_type = "DOCUMENT_REFERENCE"
op.index = index
row.operator("bim.remove_bcf_document_reference", icon="X", text="").index = index
row = box.row(align=True)
row.prop(doc, "description")
@@ -236,7 +239,7 @@ class BIM_PT_bcf_metadata(Panel):
try:
row = layout.row(align=True)
op = row.operator(
"bim.view_bcf_topic", text=f"Select {bcfxml.topics[related_topic.name.lower()].title}"
"bim.view_bcf_topic", text=f"Select {bcfxml.topics[related_topic.name.lower()].topic.title}"
)
op.topic_guid = related_topic.name
row.operator("bim.remove_bcf_related_topic", icon="X", text="").index = index
@@ -20,18 +20,21 @@ import bpy
from . import ui, operator, prop
classes = (
operator.ColourByRelatedBuildingElement,
operator.DisableEditingBoundary,
operator.EditBoundaryAttributes,
operator.EnableEditingBoundary,
operator.LoadProjectSpaceBoundaries,
operator.LoadSpaceBoundaries,
operator.LoadBoundary,
operator.SelectRelatedElementBoundaries,
operator.SelectProjectBoundaries,
operator.ColourByRelatedBuildingElement,
operator.EnableEditingBoundary,
operator.DisableEditingBoundary,
operator.EditBoundaryAttributes,
operator.SelectRelatedElementTypeBoundaries,
operator.SelectSpaceBoundaries,
operator.UpdateBoundaryGeometry,
ui.BIM_PT_Boundary,
ui.BIM_PT_SpaceBoundaries,
ui.BIM_PT_SceneBoundaries,
ui.BIM_PT_SpaceBoundaries,
prop.BIMBoundaryProperties,
)
@@ -158,6 +158,70 @@ class LoadSpaceBoundaries(bpy.types.Operator):
return {"FINISHED"}
def get_element_boundaries(element):
if not element:
return ()
if element.is_a("IfcSpace"):
return (b for b in element.BoundedBy or ())
return (b for b in element.ProvidesBoundaries)
class SelectRelatedElementBoundaries(bpy.types.Operator):
bl_idname = "bim.select_related_element_boundaries"
bl_label = "Select related element space boundaries"
bl_options = {"REGISTER", "UNDO"}
related_element: bpy.props.IntProperty()
def execute(self, context):
for obj in context.visible_objects:
obj.select_set(False)
element = tool.Ifc.get().by_id(self.related_element)
for rel in get_element_boundaries(element):
obj = tool.Ifc.get_object(rel)
if obj:
obj.select_set(True)
return {"FINISHED"}
class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
bl_idname = "bim.select_related_element_type_boundaries"
bl_label = "Select related element type space boundaries"
bl_options = {"REGISTER", "UNDO"}
related_element: bpy.props.IntProperty()
def execute(self, context):
for obj in context.visible_objects:
obj.select_set(False)
element = tool.Ifc.get().by_id(self.related_element)
if not element:
return {"FINISHED"}
element_type = tool.Root.get_element_type(element)
if not element_type:
return {"FINISHED"}
for child in tool.Type.get_type_occurrences(element_type):
for rel in get_element_boundaries(child):
obj = tool.Ifc.get_object(rel)
if obj:
obj.select_set(True)
return {"FINISHED"}
class SelectSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.select_space_boundaries"
bl_label = "Select all space boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.visible_objects:
obj.select_set(False)
element = tool.Ifc.get_entity(context.active_object)
for rel in element.BoundedBy or []:
obj = tool.Ifc.get_object(rel)
if obj:
obj.select_set(True)
return {"FINISHED"}
class SelectProjectBoundaries(bpy.types.Operator):
bl_idname = "bim.select_project_space_boundaries"
bl_label = "Select all project space boundaries"
@@ -208,11 +272,10 @@ class ColourByRelatedBuildingElement(bpy.types.Operator):
return result
def _execute(self, context):
self.file = tool.Ifc.get()
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRelSpaceBoundary"):
continue
obj.color = get_colour(element)
@@ -102,6 +102,11 @@ class BIM_PT_Boundary(Panel):
row.label(text=f"{entity.is_a()}/{entity.Name}")
op = row.operator("bim.select_global_id", text="", icon="OBJECT_DATA")
op.global_id = entity.GlobalId
if ifc_attribute in ("RelatingSpace", "RelatedBuildingElement"):
op = row.operator("bim.select_related_element_boundaries", text="", icon="RESTRICT_SELECT_OFF")
op.related_element = entity.id()
op = row.operator("bim.select_related_element_type_boundaries", text="", icon="ASSET_MANAGER")
op.related_element = entity.id()
else:
row.label(text="")
@@ -118,6 +123,7 @@ class BIM_PT_SpaceBoundaries(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "BIM_PT_geometry_object"
@classmethod
@@ -143,6 +149,7 @@ class BIM_PT_SpaceBoundaries(Panel):
self.ifc_file = tool.Ifc.get()
row = self.layout.row()
row.operator("bim.load_space_boundaries")
row.operator("bim.select_space_boundaries", text="", icon="RESTRICT_SELECT_OFF")
for boundary in SpaceBoundariesData.data["boundaries"]:
row = self.layout.row()
row.label(text=boundary["description"], icon="GHOST_ENABLED")
@@ -21,7 +21,7 @@ import bpy
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
class CadTool(WorkSpaceTool):
@@ -48,7 +48,9 @@ class CadTool(WorkSpaceTool):
def draw_settings(context, layout, tool):
obj = context.active_object
if obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile:
if not obj or not obj.data:
return
if hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
@@ -100,7 +102,57 @@ class CadTool(WorkSpaceTool):
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_X")
row.operator("bim.reset_vertex", text="Reset Vertex")
elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS":
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
row.operator("bim.edit_extrusion_axis", text="Save Axis")
row.operator("bim.disable_editing_extrusion_axis", text="", icon="CANCEL")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.cad_hotkey", text="Extend").hotkey = "S_E"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.cad_hotkey", text="Mitre").hotkey = "S_T"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
row.operator("bim.cad_hotkey", text="Fillet").hotkey = "S_F"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
row.operator("bim.cad_hotkey", text="Offset").hotkey = "S_O"
else:
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and context.active_object.BIMRailingProperties.is_editing_path
):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
row.operator("bim.cad_hotkey", text="Apply Railing Path").hotkey = "S_Q"
row.operator("bim.cancel_editing_railing_path", icon="CANCEL", text="")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and context.active_object.BIMRoofProperties.is_editing_path
):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
row.operator("bim.cad_hotkey", text="Apply Roof Path").hotkey = "S_Q"
row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
@@ -187,7 +239,25 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_Q(self):
if tool.Ifc.get_entity(bpy.context.active_object):
bpy.ops.bim.edit_extrusion_profile()
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
bpy.ops.bim.edit_extrusion_profile()
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and bpy.context.active_object.BIMRailingProperties.is_editing_path
):
bpy.ops.bim.finish_editing_railing_path()
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.finish_editing_roof_path()
else:
bpy.ops.bim.edit_arbitrary_profile()
@@ -210,4 +280,9 @@ class CadHotkey(bpy.types.Operator):
def is_profile(self):
obj = bpy.context.active_object
return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile
return (
obj
and obj.data
and hasattr(obj.data, "BIMMeshProperties")
and obj.data.BIMMeshProperties.subshape_type == "PROFILE"
)
@@ -22,6 +22,7 @@ import json
import bmesh
import logging
import numpy as np
import ifcopenshell
from mathutils import Matrix
from math import radians
from blenderbim.bim.ifc import IfcStore
@@ -30,8 +31,10 @@ from blenderbim.bim.ifc import IfcStore
class ExportClashSets(bpy.types.Operator):
bl_idname = "bim.export_clash_sets"
bl_label = "Export Clash Sets"
bl_description = "Export clash sets to a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
@@ -62,8 +65,10 @@ class ImportClashSets(bpy.types.Operator):
bl_idname = "bim.import_clash_sets"
bl_label = "Import Clash Sets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import clash sets from a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
@@ -98,6 +103,7 @@ class AddClashSet(bpy.types.Operator):
bl_idname = "bim.add_clash_set"
bl_label = "Add Clash Set"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add a clash set"
def execute(self, context):
new = context.scene.BIMClashProperties.clash_sets.add()
@@ -110,6 +116,7 @@ class RemoveClashSet(bpy.types.Operator):
bl_idname = "bim.remove_clash_set"
bl_label = "Remove Clash Set"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Remove the selected clash set"
index: bpy.props.IntProperty()
def execute(self, context):
@@ -121,6 +128,7 @@ class AddClashSource(bpy.types.Operator):
bl_idname = "bim.add_clash_source"
bl_label = "Add Clash Source"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add a clash source to this group"
group: bpy.props.StringProperty()
def execute(self, context):
@@ -133,6 +141,7 @@ class RemoveClashSource(bpy.types.Operator):
bl_idname = "bim.remove_clash_source"
bl_label = "Remove Clash Source"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Remove this clash source"
index: bpy.props.IntProperty()
group: bpy.props.StringProperty()
@@ -146,6 +155,7 @@ class SelectClashSource(bpy.types.Operator):
bl_idname = "bim.select_clash_source"
bl_label = "Select Clash Source"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC file to add as a clash source"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
index: bpy.props.IntProperty()
@@ -194,7 +204,8 @@ class SelectSmartGroupedClashesPath(bpy.types.Operator):
class ExecuteIfcClash(bpy.types.Operator):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
filter_glob: bpy.props.StringProperty( default='*.bcf;*.json', options={'HIDDEN'} )
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
@@ -263,6 +274,7 @@ class SelectIfcClashResults(bpy.types.Operator):
bl_idname = "bim.select_ifc_clash_results"
bl_label = "Select IFC Clash Results"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@@ -287,10 +299,26 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"CANCELLED"}
for clash in clash_set["clashes"].values():
global_ids.extend([clash["a_global_id"], clash["b_global_id"]])
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
ifc_file = ""
for scene in obj.users_scene:
if scene.BIMProperties.ifc_file:
ifc_file = scene.BIMProperties.ifc_file
if scene.library:
break
if ifc_file:
if ifc_file not in IfcStore.session_files:
IfcStore.session_files[ifc_file] = ifcopenshell.open(ifc_file)
element_file = IfcStore.session_files[ifc_file]
else:
element_file = self.file
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if element.GlobalId in global_ids:
obj.select_set(True)
return {"FINISHED"}
@@ -308,7 +336,7 @@ class SmartClashGroup(bpy.types.Operator):
def execute(self, context):
from ifcclash import ifcclash
settings = ifcclash.ClashSettings()
self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json")
settings.output = self.filepath
@@ -65,12 +65,13 @@ class ClassificationsData:
class ReferencesData:
@classmethod
def is_available_classification_added(cls):
def active_classification_library(cls):
if not IfcStore.classification_file or not IfcStore.classification_file.by_type("IfcClassification"):
return False
props = bpy.context.scene.BIMClassificationProperties
name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
return name in [e.Name for e in tool.Ifc.get().by_type("IfcClassification")]
if name in [e.Name for e in tool.Ifc.get().by_type("IfcClassification")]:
return name
class ClassificationReferencesData(ReferencesData):
@@ -81,7 +82,7 @@ class ClassificationReferencesData(ReferencesData):
def load(cls):
cls.is_loaded = True
cls.data["references"] = cls.references()
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
cls.data["active_classification_library"] = cls.active_classification_library()
@classmethod
def references(cls):
@@ -103,7 +104,7 @@ class MaterialClassificationsData(ReferencesData):
def load(cls):
cls.is_loaded = True
cls.data["references"] = cls.references()
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
cls.data["active_classification_library"] = cls.active_classification_library()
@classmethod
def references(cls):
@@ -125,7 +126,7 @@ class CostClassificationsData(ReferencesData):
def load(cls):
cls.is_loaded = True
cls.data["references"] = cls.references()
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
cls.data["active_classification_library"] = cls.active_classification_library()
@classmethod
def references(cls):
@@ -100,12 +100,16 @@ class ReferenceUI:
self.draw_reference_ui(reference)
def draw_add_ui(self, context):
if not self.data.data["is_available_classification_added"]:
if not self.data.data["active_classification_library"]:
row = self.layout.row(align=True)
row.label(text="No Active Classification Library")
row.operator("bim.load_classification_library", text="", icon="IMPORT")
return
row = self.layout.row(align=True)
row.prop(self.sprops, "available_classifications", text="")
row.label(text=f"Active Classification Library: {self.data.data['active_classification_library']}")
#row.prop(self.sprops, "available_classifications", text="")
if not self.sprops.available_library_references:
op = row.operator("bim.change_classification_level", text="", icon="IMPORT")
op = row.operator("bim.change_classification_level", text="", icon="GREASEPENCIL")
op.parent_id = int(self.sprops.available_classifications)
return
if self.sprops.active_library_referenced_source:
@@ -74,6 +74,7 @@ class BIM_PT_object_constraints(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "BIM_PT_misc_object"
@classmethod
@@ -30,6 +30,7 @@ classes = (
operator.EnableEditingCostSchedule,
operator.EnableEditingCostItems,
operator.EnableEditingCostItem,
operator.ExportCostSchedules,
operator.ExpandCostItems,
operator.EnableEditingCostItemQuantities,
operator.EnableEditingCostItemQuantity,
@@ -21,6 +21,8 @@ import ifcopenshell
import ifcopenshell.util.cost
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.bim.schema
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
def refresh():
@@ -36,6 +38,8 @@ class CostSchedulesData:
@classmethod
def load(cls):
cls.data = {
"predefined_types": cls.get_cost_schedule_types(),
"total_cost_schedules": cls.total_cost_schedules(),
"schedules": cls.schedules(),
"is_editing_rates": cls.is_editing_rates(),
"cost_items": cls.cost_items(),
@@ -45,6 +49,10 @@ class CostSchedulesData:
}
cls.is_loaded = True
@classmethod
def total_cost_schedules(cls):
return len(tool.Ifc.get().by_type("IfcCostSchedule"))
@classmethod
def schedules(cls):
results = []
@@ -212,6 +220,21 @@ class CostSchedulesData:
for t in tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
]
@classmethod
def get_cost_schedule_types(cls):
results = []
declaration = tool.Ifc().schema().declaration_by_name("IfcCostSchedule")
version = tool.Ifc.get_schema()
for attribute in declaration.attributes():
if attribute.name() == "PredefinedType":
results.extend(
[
(e, e, get_predefined_type_doc(version, "IfcCostSchedule", e))
for e in attribute.type_of_attribute().declared_type().enumeration_items()
]
)
break
return results
class CostItemRatesData:
data = {}
@@ -36,7 +36,7 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Add a new cost schedule"
def _execute(self, context):
core.add_cost_schedule(tool.Ifc)
core.add_cost_schedule(tool.Ifc, predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types)
class EditCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
@@ -328,7 +328,7 @@ class DisableEditingCostItemQuantity(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Disable editing cost item quantity"
def _execute(self, context):
def execute(self, context):
core.disable_editing_cost_item_quantity(tool.Cost)
return {"FINISHED"}
@@ -586,3 +586,22 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator):
core.calculate_cost_item_resource_value(tool.Ifc, cost_item=tool.Ifc.get().by_id(self.cost_item))
return {"FINISHED"}
class ExportCostSchedules(bpy.types.Operator):
bl_idname = "bim.export_cost_schedules"
bl_label = "Export Cost Schedule"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
format: bpy.props.EnumProperty("Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
def execute(self, context):
core.export_cost_schedules(tool.Cost, format=self.format)
return {"FINISHED"}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
self.layout.label(text="Choose a format")
self.layout.prop(self, "format")
@@ -109,6 +109,12 @@ def update_cost_item_name(self, context):
attribute.string_value = self.name
def get_schedule_predefined_types(self, context):
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
return CostSchedulesData.data["predefined_types"]
class CostItem(PropertyGroup):
name: StringProperty(name="Name", update=update_cost_item_name)
identification: StringProperty(name="Identification", update=update_cost_item_identification)
@@ -130,6 +136,9 @@ class CostItemType(PropertyGroup):
class BIMCostProperties(PropertyGroup):
cost_schedule_predefined_types: EnumProperty(
items=get_schedule_predefined_types, name="Predefined Type", default=None
)
is_cost_update_enabled: BoolProperty(name="Is Cost Update Enabled", default=True)
cost_schedule_attributes: CollectionProperty(name="Cost Schedule Attributes", type=Attribute)
is_editing: StringProperty(name="Is Editing")
@@ -42,9 +42,16 @@ class BIM_PT_cost_schedules(Panel):
CostSchedulesData.load()
self.props = context.scene.BIMCostProperties
row = self.layout.row()
if CostSchedulesData.data["total_cost_schedules"]:
row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT")
row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT")
else:
row.label(text="No Cost Schedules Found found.", icon="COMMUNITY")
row = self.layout.row()
row.operator("bim.add_cost_schedule", icon="ADD")
row.prop(self.props, "cost_schedule_predefined_types")
row.operator("bim.add_cost_schedule", icon="ADD", text="Add new")
for schedule in CostSchedulesData.data["schedules"]:
self.draw_cost_schedule_ui(schedule)
@@ -57,7 +64,6 @@ class BIM_PT_cost_schedules(Panel):
op = row.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="")
op.cost_schedule = cost_schedule["id"]
row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY")
row.operator("bim.export_cost_schedule", text="", icon="EXPORT").cost_schedule = cost_schedule["id"]
if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES":
row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK")
elif self.props.is_editing == "COST_ITEMS":
@@ -169,7 +175,7 @@ class BIM_PT_cost_schedules(Panel):
row.prop(self.props, "cost_types", text="")
if self.props.cost_types == "CATEGORY":
row.prop(self.props, "cost_category", text="")
op = row.operator("bim.add_cost_value", text="", icon="ADD")
op = row.operator("bim.add_cost_value", text="Add Value", icon="ADD")
op.parent = self.props.active_cost_item_id
op.cost_type = self.props.cost_types
if self.props.cost_types == "CATEGORY":
@@ -505,6 +511,8 @@ class BIM_UL_cost_items_trait:
self.draw_quantity_column(split2, cost_item)
self.draw_value_column(split2, cost_item)
if self.props.active_cost_item_id == item.ifc_definition_id:
row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL")
for column in self.props.columns:
split2.label(text=str(cost_item["CategoryValues"].get(column.name, "-")))
@@ -82,6 +82,7 @@ class BIM_PT_object_documents(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "BIM_PT_misc_object"
@classmethod
@@ -29,12 +29,10 @@ classes = (
operator.AddDrawingToSheet,
operator.AddSchedule,
operator.AddScheduleToSheet,
operator.AddSectionsAnnotations,
operator.AddSheet,
operator.BuildSchedule,
operator.CleanWireframes,
operator.ContractSheet,
operator.CopyGrid,
operator.CreateDrawing,
operator.CreateSheets,
operator.DisableEditingAssignedProduct,
@@ -42,9 +40,12 @@ classes = (
operator.DisableEditingSchedules,
operator.DisableEditingSheets,
operator.DisableEditingText,
operator.AddTextLiteral,
operator.RemoveTextLiteral,
operator.DuplicateDrawing,
operator.EditAssignedProduct,
operator.EditText,
operator.EditTextPopup,
operator.EditVectorStyle,
operator.EnableEditingAssignedProduct,
operator.EnableEditingText,
@@ -72,6 +73,7 @@ classes = (
prop.Sheet,
prop.DocProperties,
prop.BIMCameraProperties,
prop.Literal,
prop.BIMTextProperties,
prop.BIMAssignedProductProperties,
ui.BIM_PT_camera,
@@ -32,7 +32,7 @@ class Annotator:
if related_element is None:
location, _, _, _ = Annotator.get_placeholder_coords(bpy.context)
else:
obj.data.BIMTextProperties.related_element = related_element
obj.data.BIMAssignedProductProperties.related_element = related_element
location = related_element.location
obj.location = location
obj.hide_render = True
@@ -17,16 +17,17 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell.util.element
import ifcopenshell.util.representation
import blenderbim.tool as tool
def refresh():
ProductAssignmentsData.is_loaded = False
TextData.is_loaded = False
SheetsData.is_loaded = False
SchedulesData.is_loaded = False
DrawingsData.is_loaded = False
DecoratorData.data = {}
class ProductAssignmentsData:
@@ -49,28 +50,6 @@ class ProductAssignmentsData:
return f"{rel.RelatingProduct.is_a()}/{name}"
class TextData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def attributes(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
return []
rep = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
text_literal = [i for i in rep.Items if i.is_a("IfcTextLiteral")][0]
return [
{"name": "Literal", "value": text_literal.Literal},
{"name": "BoxAlignment", "value": text_literal.BoxAlignment},
]
class SheetsData:
data = {}
is_loaded = False
@@ -121,3 +100,117 @@ class SchedulesData:
@classmethod
def total_schedules(cls):
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SCHEDULE"])
FONT_SIZES = {
"small": 1.8,
"regular": 2.5,
"large": 3.5,
"header": 5.0,
"title": 7.0,
}
class DecoratorData:
# stores 1 type of data per object
data = {}
@classmethod
def get_batting_thickness(cls, obj):
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
if element:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
thickness = ifcopenshell.util.element.get_pset(element, "BBIM_Batting", "Thickness")
thickness = thickness * unit_scale if thickness else 1.5
cls.data[obj.name] = thickness
return thickness
@classmethod
def get_section_markers_display_data(cls, obj):
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
if not element:
return
# default values
pset_data = {
"HasConnectedSectionLine": True,
"ShowStartArrow": True,
"StartArrowSymbol": "",
"ShowEndArrow": True,
"EndArrowSymbol": "",
}
obj_pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Section") or {}
pset_data.update(obj_pset_data)
# create more usable display data
start_symbol = pset_data["StartArrowSymbol"]
end_symbol = pset_data["EndArrowSymbol"]
display_data = {
"start": {
"add_circle": pset_data["ShowStartArrow"] and not start_symbol,
"add_symbol": pset_data["ShowStartArrow"] or bool(start_symbol),
"symbol": start_symbol or "section-arrow",
},
"end": {
"add_circle": pset_data["ShowEndArrow"] and not end_symbol,
"add_symbol": pset_data["ShowEndArrow"] or bool(end_symbol),
"symbol": end_symbol or "section-arrow",
},
"connect_markers": pset_data["HasConnectedSectionLine"],
}
cls.data[obj.name] = display_data
return display_data
@classmethod
def get_ifc_text_data(cls, obj):
"""returns font size in mm for current ifc text object"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
return None
props = obj.BIMTextProperties
# getting font size
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
# use `regular` as default
if classes:
classes_split = classes.split()
# prioritize smaller font sizes just like in svg
font_size_type = next(
(font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
)
else:
font_size_type = "regular"
font_size = FONT_SIZES[font_size_type]
# other attributes
props_literals = props.literals
props_literals_n = len(props.literals)
literals = tool.Drawing.get_text_literal(obj, return_list=True)
literals_data = []
for i, literal in enumerate(literals):
literal_data = {
"Literal": literal.Literal,
"BoxAlignment": literal.BoxAlignment,
}
if i < props_literals_n:
literal_data["CurrentValue"] = props_literals[i].value
else:
literal_data["CurrentValue"] = literal.Literal
literals_data.append(literal_data)
text_data = {"Literals": literals_data, "FontSize": font_size}
cls.data[obj.name] = text_data
return text_data
@@ -26,14 +26,15 @@ import bmesh
import ifcopenshell
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.helper as helper
from math import acos, pi, atan, degrees
from functools import reduce
from itertools import chain
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from mathutils import Vector, Matrix, geometry
from bpy_extras.view3d_utils import location_3d_to_region_2d
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
from math import acos, pi
from blenderbim.bim.module.drawing.data import DecoratorData
def ccw(A, B, C):
@@ -180,6 +181,11 @@ class BaseDecorator:
defines=self.DEF_GLSL,
)
self.font_id = blf.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
)
def get_camera_width_mm(self):
# Horrific prototype code to ensure bgl draws at drawing scales
# https://blender.stackexchange.com/questions/16493/is-there-a-way-to-fit-the-viewport-to-the-current-field-of-view
def is_landscape(render):
@@ -199,11 +205,9 @@ class BaseDecorator:
camera_width_model = camera.data.ortho_scale
else:
camera_width_model = camera.data.ortho_scale / render.resolution_y * render.resolution_x
self.camera_width_mm = get_scale(camera) * camera_width_model
self.font_id = blf.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
)
camera_width_mm = get_scale(camera) * camera_width_model
return camera_width_mm
def camera_zoom_to_factor(self, zoom):
return math.pow(((zoom / 50) + math.sqrt(2)) / 2, 2)
@@ -235,12 +239,20 @@ class BaseDecorator:
if element.is_a("IfcAnnotation"):
if element.ObjectType == self.objecttype:
results.append(obj)
elif (
self.objecttype == "MISC"
and element.ObjectType not in decoration_presets
and isinstance(obj.data, bpy.types.Mesh)
):
results.append(obj)
elif self.objecttype == "FALL" and element.ObjectType in (
"SLOPE_ANGLE",
"SLOPE_FRACTION",
"SLOPE_PERCENT",
):
results.append(obj)
return results
def get_path_geom(self, obj, topo=True):
@@ -295,17 +307,10 @@ class BaseDecorator:
def get_editmesh_geom(self, obj):
"""Parses editmode mesh geometry into line segments"""
mesh = bmesh.from_edit_mesh(obj.data)
vertices = []
indices = []
idx = 0
for edge in mesh.edges:
vertices.extend(edge.verts)
indices.append((idx, idx + 1))
idx += 2
vertices = [obj.matrix_world @ v.co for v in vertices]
# TODO: avoid bmesh here?
bm = bmesh.from_edit_mesh(obj.data)
indices = [[v.index for v in edge.verts] for edge in bm.edges]
vertices = [obj.matrix_world @ v.co for v in bm.verts]
return vertices, indices
def decorate(self, context, object):
@@ -313,7 +318,15 @@ class BaseDecorator:
raise NotImplementedError()
def draw_lines(
self, context, obj, vertices, indices, topology=None, is_scale_dependant=True, fill_next_vertices=False
self,
context,
obj,
vertices,
indices,
topology=None,
is_scale_dependant=True,
fill_next_vertices=False,
extra_float_kwargs={},
):
# use is_scale_dependant = False if shader is not using uniform viewportDrawingScale
# otherwise uniform will be discarded during the optimization process
@@ -358,21 +371,39 @@ class BaseDecorator:
# Horrific prototype code
factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom)
camera_width_px = factor * context.region.width
mm_to_px = camera_width_px / self.camera_width_mm
mm_to_px = camera_width_px / self.get_camera_width_mm()
# 0.00025 is a magic constant number I visually discovered to get the right number.
# It probably should be dynamically calculated using system.dpi or something.
viewport_drawing_scale = 0.00025 * mm_to_px
self.shader.uniform_float("viewportDrawingScale", viewport_drawing_scale)
for kwarg, value in extra_float_kwargs.items():
self.shader.uniform_float(kwarg, value)
batch.draw(self.shader)
def draw_label(self, context, text, pos, dir, gap=4, center=True, vcenter=False):
def draw_label(
self,
context,
text,
pos,
text_dir,
gap=4,
center=True,
vcenter=False,
font_size_mm=2.5,
line_no=0,
box_alignment=None,
):
"""Draw text label
Args:
pos: bottom-center
aligned and centered at segment middle
NOTE: `blf.draw` seems to ignore the \n character, so we have to split the text ourselves
and use the `line_no` argument of `draw_label`
"""
# 0 is the default font, but we're fancier than that
font_id = self.font_id
@@ -381,37 +412,59 @@ class BaseDecorator:
color = context.scene.DocProperties.decorations_colour
ang = -Vector((1, 0)).angle_signed(dir)
ang = -Vector((1, 0)).angle_signed(text_dir)
cos = math.cos(ang)
sin = math.sin(ang)
rotation_matrix = Matrix.Rotation(-ang, 2)
# Horrific prototype code
factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom)
camera_width_px = factor * context.region.width
mm_to_px = camera_width_px / self.camera_width_mm
mm_to_px = camera_width_px / self.get_camera_width_mm()
# 0.004118616 is a magic constant number I visually discovered to get the right number.
# In particular it works only for the OpenGOST font and produces a 2.5mm font size.
# It probably should be dynamically calculated using system.dpi or something.
# font_size = 16 <-- this is a good default
font_size = int(0.004118616 * mm_to_px)
# TODO: need to synchronize it better with svg
font_size_px = int(0.004118616 * mm_to_px) * font_size_mm / 2.5
pos = pos - line_no * font_size_px * rotation_matrix[1]
blf.size(font_id, font_size, dpi)
blf.size(font_id, font_size_px, dpi)
w, h = 0, 0
if center or vcenter:
if box_alignment or center or vcenter:
w, h = blf.dimensions(font_id, text)
if center:
# horizontal centering
pos -= Vector((cos, sin)) * w * 0.5
if box_alignment:
box_alignment_offset = Vector((0, 0))
if "bottom" in box_alignment:
pass
elif "top" in box_alignment:
box_alignment_offset += Vector((0, h))
else:
# middle / center
box_alignment_offset += Vector((0, h / 2))
if vcenter:
# vertical centering
pos -= Vector((-sin, cos)) * h * 0.5
if "left" in box_alignment:
pass
elif "right" in box_alignment:
box_alignment_offset += Vector((w, 0))
else:
# middle / center
box_alignment_offset += Vector((w / 2, 0))
pos -= rotation_matrix.transposed() @ box_alignment_offset
else:
if center:
# horizontal centering
pos -= Vector((cos, sin)) * w * 0.5
if gap:
# side-shifting
pos += Vector((-sin, cos)) * gap
if vcenter:
# vertical centering
pos -= Vector((-sin, cos)) * h * 0.5
if gap:
# side-shifting
pos += Vector((-sin, cos)) * gap
blf.enable(font_id, blf.ROTATION)
blf.position(font_id, pos.x, pos.y, 0) if hasattr(pos, "x") else blf.position(font_id, 0, 0, 0)
@@ -750,7 +803,12 @@ class LeaderDecorator(BaseDecorator):
region3d = context.region_data
dir = Vector((1, 0))
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
self.draw_label(context, obj.BIMTextProperties.value, pos, dir, gap=0, center=False, vcenter=False)
props = obj.BIMTextProperties
text_data = props.get_text_edited_data() if props.is_editing else DecoratorData.get_ifc_text_data(obj)
text = text_data["Literals"][0]['CurrentValue']
self.draw_label(context, text, pos, dir, gap=0, center=False, vcenter=False)
class RadiusDecorator(BaseDecorator):
@@ -845,6 +903,125 @@ class RadiusDecorator(BaseDecorator):
self.draw_label(context, text, pos, dir, gap=0, center=False, vcenter=False)
class FallDecorator(BaseDecorator):
"""Decorating text with arrows
- head point with arrow
"""
objecttype = "FALL"
DEF_GLSL = (
BaseDecorator.DEF_GLSL
+ """
#define ARROW_ANGLE PI / 12.0
#define ARROW_SIZE 16.0
"""
)
GEOM_GLSL = """
uniform vec2 winsize;
uniform float viewportDrawingScale;
layout(lines) in;
layout(line_strip, max_vertices=MAX_POINTS) out;
in uint type[];
void main() {
vec4 clip2win = matCLIP2WIN();
vec4 win2clip = matWIN2CLIP();
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
uint t0 = type[0], t1 = type[1];
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
vec4 edge = p1w - p0w, dir = normalize(edge);
vec4 gap1 = vec4(0);
vec4 p;
// end edge arrow for last segment
if (t1 == 2u) {
vec4 head[3];
arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head);
gl_Position = p1;
EmitVertex();
p = p1w - head[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w - head[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
gap1 = dir * viewportDrawingScale * ARROW_SIZE;
}
// stem, adjusted for and arrow
gl_Position = p0;
EmitVertex();
p = p1w - gap1;
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
"""
def decorate(self, context, obj):
verts, idxs, topo = self.get_path_geom(obj)
self.draw_lines(context, obj, verts, idxs, topo)
self.draw_labels(context, obj)
def draw_labels(self, context, obj):
region = context.region
region3d = context.region_data
dir = Vector((1, 0))
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
# generate label text
# same function as in svgwriter.py
def get_label_text():
element = tool.Ifc.get_entity(obj)
B, A = [v.co.xyz for v in spline_points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
O.z = B.z
run = (B - O).length
if run != 0:
angle_tg = rise / run
angle = round(degrees(atan(angle_tg)))
else:
angle = 90
# ues SLOPE_ANGLE as default
if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif element.ObjectType == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{self.format_value(context, rise)} / {self.format_value(context, run)}"
elif element.ObjectType == "SLOPE_PERCENT":
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
if spline_points:
text = get_label_text()
self.draw_label(context, text, pos, dir, gap=0, center=False, vcenter=False)
def get_spline_end(self, obj):
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
if not spline_points:
return Vector((0, 0, 0))
return obj.matrix_world @ spline_points[0].co
class StairDecorator(BaseDecorator):
"""Decorating stairs
- head point with arrow
@@ -1332,6 +1509,128 @@ class BreakDecorator(BaseDecorator):
self.draw_lines(context, obj, verts, idxs)
class BattingDecorator(BaseDecorator):
"""Decorator for batting objects
Uses first two vertices in verts list.
"""
objecttype = "BATTING"
DEF_GLSL = (
BaseDecorator.DEF_GLSL
+ """
#define PATTERN_SEGMENT_LENGTH 2
"""
)
GEOM_GLSL = """
uniform vec2 winsize;
uniform float viewportDrawingScale;
uniform float batting_thickness_winspace;
layout(lines) in;
layout(line_strip, max_vertices=256) out;
void place_vert(vec4 ref_point, vec2 win_space_vert) {
vec4 win2clip = matWIN2CLIP();
vec4 p = ref_point + vec4(win_space_vert, 0, 0);
gl_Position = WIN2CLIP(p);
EmitVertex();
}
void main() {
vec4 clip2win = matCLIP2WIN();
vec4 win2clip = matWIN2CLIP();
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1), pmw = (p0w + p1w) * .5;
vec4 edge = p1w - p0w, dir = normalize(edge);
vec4 gap = dir * 16.0; // TODO: never used?
float segment_width = batting_thickness_winspace / 2.5;
vec2 batting_dimensions = vec2(segment_width, batting_thickness_winspace);
// need to multiply by viewportDrawingScale
// so the drawing will stay consistent on zoom in / zoom out
// but since we use winspace batting dimensions we skip it
mat2 m_edge_space = mat2( dir.xy, dir.yx*vec2(1,-1) );
// simplied rectangle + cross version to indicate batting
place_vert(p0w, m_edge_space * ( vec2(0, 0.5) * batting_dimensions ) );
place_vert(p0w, m_edge_space * ( -vec2(0, 0.5) * batting_dimensions ) );
place_vert(p1w, m_edge_space * ( -vec2(0, 0.5) * batting_dimensions ) );
place_vert(p1w, m_edge_space * ( vec2(0, 0.5) * batting_dimensions ) );
place_vert(p0w, m_edge_space * ( vec2(0, 0.5) * batting_dimensions ) );
EndPrimitive();
place_vert(p0w, m_edge_space * ( -vec2(0, 0.5) * batting_dimensions ) );
place_vert(p1w, m_edge_space * ( vec2(0, 0.5) * batting_dimensions ) );
EndPrimitive();
place_vert(p0w, m_edge_space * ( vec2(0, 0.5) * batting_dimensions ) );
place_vert(p1w, m_edge_space * ( -vec2(0, 0.5) * batting_dimensions ) );
EndPrimitive();
// TODO: more fancy pattern? possibly use of frag shader?
// we're not using complicated patterns because of the
// hardware shader limit:
// Error: C6033: Hardware limitation reached, can only emit 256 vertices of this size
// vec2 pattern_segment_data[PATTERN_SEGMENT_LENGTH];
// simplified insulation pattern
// pattern_segment_data[0] = vec2(0, 1);
// pattern_segment_data[1] = vec2(0.5, 0.8);
// pattern_segment_data[2] = vec2(0, 0.2);
// pattern_segment_data[3] = vec2(0.5, 0);
// pattern_segment_data[4] = vec2(1.0, 0.2);
// pattern_segment_data[5] = vec2(0.5, 0.8);
// pattern_segment_data[6] = vec2(1.0, 1.0);
// zigzag pattern
// pattern_segment_data[0] = vec2(0, 1);
// pattern_segment_data[1] = vec2(0, 0);
// int segs = int( ceil( length(edge) / (batting_dimensions.x * viewportDrawingScale) ) ); // amount of segments
// vec4 p;
// vec2 p_base, p_cur_ver;
// for (int i = 0; i < segs; i++) {
// p_base = m_edge_space * (vec2(i, -0.5) * batting_dimensions);
// for(int j=0; j<PATTERN_SEGMENT_LENGTH; j++) {
// p_cur_ver = p_base + m_edge_space * (pattern_segment_data[j] * batting_dimensions);
// place_vert(p0w, p_cur_ver);
// }
// }
// gl_Position = p1;
// EmitVertex();
// EndPrimitive();
}
"""
def decorate(self, context, obj):
if obj.data.is_editmode:
verts, idxs = self.get_editmesh_geom(obj)
else:
verts, idxs = self.get_mesh_geom(obj)
# TODO: find the less ugly way to figure thickness
thickness = DecoratorData.get_batting_thickness(obj)
region = context.region
region3d = context.region_data
original_edge_length = (verts[1] - verts[0]).length
clipspace_verts = [region3d.perspective_matrix @ v for v in verts[:2]]
winspace_verts = [v * Vector([region.width / 2, region.height / 2, 1]) for v in clipspace_verts]
win_space_edge_length = (winspace_verts[1] - winspace_verts[0]).xy.length
k = win_space_edge_length / original_edge_length
winspace_thickness = k * thickness
self.draw_lines(
context,
obj,
verts[:2],
idxs,
extra_float_kwargs={"batting_thickness_winspace": winspace_thickness},
is_scale_dependant=False,
)
class GridDecorator(BaseDecorator):
objecttype = "GRID"
@@ -1557,6 +1856,11 @@ class SectionDecorator(LevelDecorator):
GEOM_GLSL = """
uniform vec2 winsize;
uniform float viewportDrawingScale;
uniform float connect_markers;
uniform float display_start_symbol;
uniform float display_end_symbol;
uniform float display_start_circle;
uniform float display_end_circle;
layout(lines) in;
layout(line_strip, max_vertices=MAX_POINTS) out;
@@ -1580,7 +1884,7 @@ class SectionDecorator(LevelDecorator):
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
vec4 edge = p1w - p0w, side = normalize(edge);
vec4 gap = side * viewportDrawingScale * CIRCLE_SIZE;
vec4 dir = vec4(cross(vec3(side.xy, 0), vec3(0, 0, 1)).xy, 0, 0);
vec4 edge_ortho_dir = -vec4(cross(vec3(side.xy, 0), vec3(0, 0, 1)).xy, 0, 0);
vec4 p;
vec4 head[CIRCLE_SEGS];
@@ -1589,91 +1893,117 @@ class SectionDecorator(LevelDecorator):
vec4 head5[5];
// start edge circle
for(int i=0; i<CIRCLE_SEGS; i++) {
p = p0w + head[i];
if (display_start_circle > 0) {
for(int i=0; i<CIRCLE_SEGS; i++) {
p = p0w + head[i];
gl_Position = WIN2CLIP(p);
EmitVertex();
}
p = p0w + head[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
p = p0w + head[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
// start edge triangle
triangle_head(side, dir, viewportDrawingScale * TRIANGLE_L, viewportDrawingScale * TRIANGLE_W, viewportDrawingScale * CIRCLE_SIZE, head5);
p = p0w + head5[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head5[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head5[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head5[3];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head5[4];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
// start reference divider
p = p0w + normalize(vec4(-1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + normalize(vec4(1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
if (display_start_symbol > 0) {
// start edge triangle
triangle_head(side, edge_ortho_dir, viewportDrawingScale * TRIANGLE_L, viewportDrawingScale * TRIANGLE_W, viewportDrawingScale * CIRCLE_SIZE, head5);
p = p0w + head5[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head5[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head5[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head5[3];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head5[4];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
// end edge circle
for(int i=0; i<CIRCLE_SEGS; i++) {
p = p1w - head[i];
if (display_end_circle > 0) {
for(int i=0; i<CIRCLE_SEGS; i++) {
p = p1w - head[i];
gl_Position = WIN2CLIP(p);
EmitVertex();
}
p = p1w - head[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
p = p1w - head[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
// end edge triangle
triangle_head(side, dir, viewportDrawingScale * TRIANGLE_L, viewportDrawingScale * TRIANGLE_W, viewportDrawingScale * CIRCLE_SIZE, head5);
p = p1w + head5[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + head5[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + head5[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + head5[3];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + head5[4];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
if (display_end_symbol > 0) {
triangle_head(side, edge_ortho_dir, viewportDrawingScale * TRIANGLE_L, viewportDrawingScale * TRIANGLE_W, viewportDrawingScale * CIRCLE_SIZE, head5);
p = p1w + head5[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + head5[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + head5[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + head5[3];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + head5[4];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
float divider_line_size;
if (connect_markers > 0) {
divider_line_size = CIRCLE_SIZE;
} else {
divider_line_size = CIRCLE_SIZE * 3;
}
// NOTE: basically we consider first vertex to be
// start and the second to be the end marker
// which is a bit inconsistent with svg
// but not that anyone will use multiple edge section annotation
// start reference divider
if (display_start_symbol > 0) {
p = p0w + side * viewportDrawingScale * divider_line_size;
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w - side * viewportDrawingScale * CIRCLE_SIZE;
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
// end reference divider
p = p1w + normalize(vec4(-1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w + normalize(vec4(1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
if (display_end_symbol > 0) {
p = p1w + side * viewportDrawingScale * CIRCLE_SIZE;
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w - side * viewportDrawingScale * divider_line_size;
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
// stem
p = p0w + gap;
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w - gap;
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
if (connect_markers > 0) {
p = p0w + (display_start_symbol > 0 ? gap : vec4(0));
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w - (display_end_symbol > 0 ? gap : vec4(0));
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
}
"""
@@ -1682,9 +2012,23 @@ class SectionDecorator(LevelDecorator):
verts, idxs = self.get_editmesh_geom(obj)
else:
verts, idxs = self.get_mesh_geom(obj)
vert_map = {v.freeze(): (obj.matrix_world.inverted() @ v).x for v in verts}
verts = sorted(verts, key=lambda v: vert_map[v], reverse=True)
self.draw_lines(context, obj, verts, idxs)
# TODO: add dashed line to shader
# mind the vertices limit
display_data = DecoratorData.get_section_markers_display_data(obj)
self.draw_lines(
context,
obj,
verts,
idxs,
extra_float_kwargs={
"connect_markers": float(display_data["connect_markers"]),
"display_start_symbol": float(display_data["start"]["add_symbol"]),
"display_end_symbol": float(display_data["end"]["add_symbol"]),
"display_start_circle": float(display_data["start"]["add_circle"]),
"display_end_circle": float(display_data["end"]["add_circle"]),
},
)
class TextDecorator(BaseDecorator):
@@ -1697,8 +2041,8 @@ class TextDecorator(BaseDecorator):
DEF_GLSL = (
BaseDecorator.DEF_GLSL
+ """
#define ARROW_ANGLE PI / 12.0
#define ARROW_SIZE 16.0
#define CIRCLE_SIZE 4.0
#define CIRCLE_SEGS_ASTERISK 6
"""
)
@@ -1709,54 +2053,34 @@ class TextDecorator(BaseDecorator):
layout(lines) in;
layout(line_strip, max_vertices=MAX_POINTS) out;
void circle_head_asterisk(in float size, out vec4 head[CIRCLE_SEGS_ASTERISK]) {
float angle_d = PI * 2 / CIRCLE_SEGS_ASTERISK;
for(int i = 0; i<CIRCLE_SEGS_ASTERISK; i++) {
float angle = angle_d * i;
head[i] = vec4(cos(angle), sin(angle), 0, 0) * size;
}
}
void main() {
vec4 clip2win = matCLIP2WIN();
vec4 win2clip = matWIN2CLIP();
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
vec4 edge = p1w - p0w, dir = normalize(edge);
vec4 p0 = gl_in[0].gl_Position;
vec4 p0w = CLIP2WIN(p0);
vec4 p;
vec4 head[CIRCLE_SEGS_ASTERISK];
circle_head_asterisk(viewportDrawingScale * CIRCLE_SIZE, head);
vec4 head[3];
arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head);
// start edge arrow
gl_Position = p0;
EmitVertex();
p = p0w + head[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
gl_Position = p0;
EmitVertex();
EndPrimitive();
// end edge arrow
gl_Position = p1;
EmitVertex();
p = p1w - head[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w - head[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
// stem, with gaps for arrows
p = p0w + head[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w - head[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
for (int i=0; i < CIRCLE_SEGS_ASTERISK/2; i++) {
p = p0w + head[i];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head[i+CIRCLE_SEGS_ASTERISK/2];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
}
"""
@@ -1766,9 +2090,48 @@ class TextDecorator(BaseDecorator):
def draw_labels(self, context, obj):
region = context.region
region3d = context.region_data
dir = Vector((1, 0))
def matrix_only_rotation(m):
return m.to_3x3().normalized().to_4x4()
text_dir = Vector((1,0))
text_dir_world = matrix_only_rotation(region3d.perspective_matrix.inverted()) @ text_dir.to_3d()
text_dir_world_rotated = matrix_only_rotation(obj.matrix_world) @ text_dir_world
text_dir = (matrix_only_rotation(region3d.perspective_matrix) @ text_dir_world_rotated).to_2d().normalized()
pos = location_3d_to_region_2d(region, region3d, obj.matrix_world.translation)
self.draw_label(context, obj.BIMTextProperties.value, pos, dir, gap=0, center=False, vcenter=False)
props = obj.BIMTextProperties
text_data = props.get_text_edited_data() if props.is_editing else DecoratorData.get_ifc_text_data(obj)
literals_data = text_data["Literals"]
# draw asterisk symbol to visually indicate that there are multiple literals
if len(literals_data) > 1:
verts = [obj.location]
idxs = [(0, 0)]
self.draw_lines(context, obj, verts, idxs)
box_alignment_used = []
for literal_data in literals_data:
box_alignment = literal_data["BoxAlignment"]
# Skip literals with the same box alignment to prevent visual clutter in viewport
# User is still indicated by the asterisk symbol
if box_alignment in box_alignment_used:
continue
box_alignment_used.append(box_alignment)
for line_i, line in enumerate(literal_data["CurrentValue"].split("\\n")):
self.draw_label(
context,
line,
pos,
text_dir,
gap=0,
center=False,
vcenter=False,
font_size_mm=text_data["FontSize"],
line_no=line_i,
box_alignment=box_alignment,
)
class DecorationsHandler:
@@ -1788,6 +2151,8 @@ class DecorationsHandler:
SectionDecorator,
ElevationDecorator,
TextDecorator,
BattingDecorator,
FallDecorator,
]
installed = None
@@ -23,6 +23,7 @@ import json
import time
import bmesh
import shutil
import shapely
import subprocess
import webbrowser
import multiprocessing
@@ -38,11 +39,12 @@ import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.sheeter as sheeter
import blenderbim.bim.module.drawing.scheduler as scheduler
import blenderbim.bim.module.drawing.helper as helper
from blenderbim.bim.module.drawing.data import DecoratorData
import blenderbim.bim.export_ifc
from lxml import etree
from mathutils import Vector
from timeit import default_timer as timer
from blenderbim.bim.module.drawing.prop import RasterStyleProperty
from blenderbim.bim.module.drawing.prop import RasterStyleProperty, Literal
from blenderbim.bim.ifc import IfcStore
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -137,53 +139,89 @@ class CreateDrawing(bpy.types.Operator):
Only available if :
- IFC file is created
- Camera is in Orthographic mode
"""
- Camera is in Orthographic mode"""
bl_idname = "bim.create_drawing"
bl_label = "Create Drawing"
bl_description = (
"Creates/refreshes a .svg drawing based on currently active camera.\n\n"
+ "SHIFT+CLICK to print all annotations"
)
print_all: bpy.props.BoolProperty(name="Print all", default=False)
@classmethod
def poll(cls, context):
return bool(tool.Ifc.get() and tool.Drawing.is_camera_orthographic() and tool.Drawing.is_drawing_active())
def invoke(self, context, event):
# printing all annotations on shift+click
if event.type == "LEFTMOUSE" and event.shift:
self.print_all = True
else:
# can't rely on default value since `self.print_all = True`
# will set the value to `True` for all future calls
self.print_all = False
return self.execute(context)
def execute(self, context):
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
self.file = IfcStore.get_file()
self.props = context.scene.DocProperties
with profile("Drawing generation process"):
if self.print_all:
original_drawing_id = self.props.active_drawing_id
drawings_to_print = [drawing.ifc_definition_id for drawing in self.props.drawings]
else:
drawings_to_print = [self.props.active_drawing_id]
with profile("Initialize drawing generation process"):
self.props = context.scene.DocProperties
self.cprops = self.camera.data.BIMCameraProperties
self.drawing_name = self.file.by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name
self.get_scale()
if self.cprops.update_representation(self.camera):
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
for drawing_id in drawings_to_print:
if self.print_all:
bpy.ops.bim.activate_view(drawing=drawing_id)
self.svg_writer = svgwriter.SvgWriter()
self.svg_writer.human_scale = self.human_scale
self.svg_writer.scale = self.scale
self.svg_writer.data_dir = context.scene.BIMProperties.data_dir
self.svg_writer.camera = self.camera
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
self.svg_writer.camera_projection = tuple(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
self.file = IfcStore.get_file()
with profile("Generate underlay"):
underlay_svg = self.generate_underlay(context)
with profile("Drawing generation process"):
with profile("Initialize drawing generation process"):
self.cprops = self.camera.data.BIMCameraProperties
self.drawing_name = self.file.by_id(drawing_id).Name
self.get_scale()
if self.cprops.update_representation(self.camera):
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
with profile("Generate linework"):
linework_svg = self.generate_linework(context)
self.svg_writer = svgwriter.SvgWriter()
self.svg_writer.human_scale = self.human_scale
self.svg_writer.scale = self.scale
self.svg_writer.data_dir = context.scene.BIMProperties.data_dir
self.svg_writer.camera = self.camera
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
self.svg_writer.camera_projection = tuple(
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
)
pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]
related_paths = {
"Stylesheet": pset.get("Stylesheet"),
"Markers": pset.get("Markers"),
"Symbols": pset.get("Symbols"),
"Patterns": pset.get("Patterns"),
}
self.svg_writer.define_related_paths(**related_paths)
with profile("Generate annotation"):
annotation_svg = self.generate_annotation(context)
with profile("Generate underlay"):
underlay_svg = self.generate_underlay(context)
with profile("Combine SVG layers"):
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
with profile("Generate linework"):
linework_svg = self.generate_linework(context)
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
with profile("Generate annotation"):
annotation_svg = self.generate_annotation(context)
with profile("Combine SVG layers"):
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
if self.print_all:
bpy.ops.bim.activate_view(drawing=original_drawing_id)
return {"FINISHED"}
def get_camera_dimensions(self):
@@ -200,10 +238,7 @@ class CreateDrawing(bpy.types.Operator):
# Hacky :)
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
with open(svg_path, "w") as outfile:
pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]
self.svg_writer.create_blank_svg(svg_path).define_boilerplate(
pset.get("Stylesheet"), pset.get("Markers"), pset.get("Symbols"), pset.get("Patterns")
)
self.svg_writer.create_blank_svg(svg_path).define_boilerplate()
boilerplate = self.svg_writer.svg.tostring()
outfile.write(boilerplate.replace("</svg>", ""))
if underlay:
@@ -456,8 +491,8 @@ class CreateDrawing(bpy.types.Operator):
with profile("Post processing"):
root = etree.fromstring(results)
self.enrich_linework_with_metadata(root)
self.move_projection_to_bottom(root)
self.merge_linework_and_add_metadata(root)
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
@@ -498,25 +533,64 @@ class CreateDrawing(bpy.types.Operator):
if not it.next():
break
def enrich_linework_with_metadata(self, root):
def merge_linework_and_add_metadata(self, root):
joined_paths = {}
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
classes = ["cut"]
try:
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
except:
# See #1861 - freshly created guids cannot be extracted unfortunately
continue
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
material_name = ""
if material:
if material.is_a("IfcMaterialLayerSet"):
name = material.LayerSetName or "null"
material_name = material.LayerSetName or "null"
else:
name = getattr(material, "Name", "null") or "null"
name = self.canonicalise_class_name(name)
classes.append(f"material-{name}")
material_name = getattr(material, "Name", "null") or "null"
material_name = self.canonicalise_class_name(material_name)
classes.append(f"material-{material_name}")
el.set("class", (el.get("class", "") + " " + " ".join(classes)).strip())
# Drawing convention states that objects with the same material are merged when cut.
if material_name != "null" and el.findall("{http://www.w3.org/2000/svg}path"):
joined_paths.setdefault(material_name, []).append(el)
for key, els in joined_paths.items():
polygons = []
classes = set()
for el in els:
classes.update(el.attrib["class"].split())
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
is_closed_polygon = False
for path in el.findall("{http://www.w3.org/2000/svg}path"):
for subpath in path.attrib["d"].split("M")[1:]:
subpath = "M" + subpath.strip()
coords = [[round(float(o), 1) for o in co[1:].split(",")] for co in subpath.split()]
if len(coords) > 2 and coords[0] == coords[-1]:
is_closed_polygon = True
polygons.append(shapely.Polygon(coords))
if is_closed_polygon:
el.getparent().remove(el)
merged_polygons = shapely.ops.unary_union(polygons)
if type(merged_polygons) == shapely.MultiPolygon:
merged_polygons = merged_polygons.geoms
elif type(merged_polygons) == shapely.Polygon:
merged_polygons = [merged_polygons]
root_g = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
for polygon in merged_polygons:
g = etree.SubElement(root, "g")
path = etree.SubElement(g, "path")
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
for interior in polygon.interiors:
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
path.attrib["d"] = d
g.set("class", " ".join(list(classes)))
root_g.append(g)
def move_projection_to_bottom(self, root):
# https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value
group = root.find("{http://www.w3.org/2000/svg}g")
@@ -573,7 +647,7 @@ class CreateDrawing(bpy.types.Operator):
classes.append("material-{}".format(re.sub("[^0-9a-zA-Z]+", "", self.get_material_name(material))))
classes.append("globalid-{}".format(element.GlobalId))
for attribute in svg_writer.annotations["attributes"]:
result = self.selector.get_element_value(element, attribute)
result = ifcopenshell.util.selector.get_element_value(element, attribute)
if result:
classes.append(
"{}-{}".format(re.sub("[^0-9a-zA-Z]+", "", attribute), re.sub("[^0-9a-zA-Z]+", "", result))
@@ -921,7 +995,6 @@ class ActivateDrawingStyle(bpy.types.Operator):
exec(f"{path} = {value}")
def set_query(self, context):
self.selector = ifcopenshell.util.selector.Selector()
self.include_global_ids = []
self.exclude_global_ids = []
for ifc_file in context.scene.DocProperties.ifc_files:
@@ -930,10 +1003,10 @@ class ActivateDrawingStyle(bpy.types.Operator):
except:
continue
if self.drawing_style.include_query:
results = self.selector.parse(ifc, self.drawing_style.include_query)
results = ifcopenshell.util.selector.Selector.parse(ifc, self.drawing_style.include_query)
self.include_global_ids.extend([e.GlobalId for e in results])
if self.drawing_style.exclude_query:
results = self.selector.parse(ifc, self.drawing_style.exclude_query)
results = ifcopenshell.util.selector.Selector.parse(ifc, self.drawing_style.exclude_query)
self.exclude_global_ids.extend([e.GlobalId for e in results])
if self.drawing_style.include_query:
self.parse_filter_query("INCLUDE", context)
@@ -1134,169 +1207,65 @@ class CleanWireframes(bpy.types.Operator):
return {"FINISHED"}
class CopyGrid(bpy.types.Operator):
bl_idname = "bim.add_grid"
bl_label = "Add Grid"
bl_options = {"REGISTER", "UNDO"}
class EditTextPopup(bpy.types.Operator):
bl_idname = "bim.edit_text_popup"
bl_label = "Edit Text"
first_run: bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return helper.get_active_drawing(context.scene)[0] is not None
def draw(self, context):
# shares most of the code with BIM_PT_text.draw()
# need to keep them in sync or move to some common function
props = context.active_object.BIMTextProperties
row = self.layout.row(align=True)
row.operator("bim.add_text_literal", icon="ADD", text="Add literal")
row = self.layout.row(align=True)
row.prop(props, "font_size")
for i, literal_props in enumerate(props.literals):
box = self.layout.box()
row = self.layout.row(align=True)
row = box.row(align=True)
row.label(text=f"Literal[{i}]:")
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
# skip BoxAlignment since we're going to format it ourselves
attributes = [a for a in literal_props.attributes if a.name != "BoxAlignment"]
blenderbim.bim.helper.draw_attributes(attributes, box)
# a bit hacky way to align box alignment widget
rows = [box.row(align=True) for i in range(3)]
for i in range(9):
if i % 3 == 0:
split = rows[i // 3].split(factor=0.1, align=True)
split.column()
split.prop(literal_props, "box_alignment", text="", index=i)
text_lines = ["Text box alignment:", literal_props.attributes["BoxAlignment"].string_value, ""]
for i in range(3):
split = rows[i].split(factor=0.1, align=False)
split.column()
split.label(text=text_lines[i])
def cancel(self, context):
# disable editing when dialog is closed
bpy.ops.bim.disable_editing_text()
def execute(self, context):
drawing = tool.Ifc.get_entity(context.scene.camera)
target_view = tool.Drawing.get_drawing_target_view(drawing)
subcontext = ifcopenshell.util.representation.get_context(
IfcStore.get_file(), "Plan", "Annotation", target_view
)
if not subcontext:
# can't use invoke() because this operator
# will be run indirectly by hotkey
# so we use execute() and track whether it's the first run of the operator
if self.first_run:
bpy.ops.bim.enable_editing_text()
self.first_run = False
return context.window_manager.invoke_props_dialog(self)
else:
bpy.ops.bim.edit_text()
return {"FINISHED"}
proj_coll = helper.get_project_collection(context.scene)
view_coll, camera = helper.get_active_drawing(context.scene)
is_ortho = camera.data.type == "ORTHO"
bounds = helper.ortho_view_frame(camera.data) if is_ortho else None
clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW")
elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW")
def grep(coll):
results = []
for obj in coll.all_objects:
element = tool.Ifc.get_entity(obj)
if element and element.is_a("IfcAnnotation") and element.ObjectType == "GRID":
results.append(obj)
return results
def clone(src):
dst = src.copy()
dst.data = dst.data.copy()
dst.name = dst.name.replace("IfcGridAxis/", "")
dst.BIMObjectProperties.ifc_definition_id = 0
dst.data.BIMMeshProperties.ifc_definition_id = 0
return dst
def disassemble(obj):
mesh = bmesh.new()
mesh.verts.ensure_lookup_table()
mesh.from_mesh(obj.data)
return obj, mesh
def assemble(obj, mesh):
mesh.to_mesh(obj.data)
return obj
def localize(obj, mesh):
# convert to camera coords
mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
obj.matrix_world = camera.matrix_world
return obj, mesh
def clip(mesh):
# clip segment against camera view bounds
mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]]
points = helper.clip_segment(bounds, points)
if points is None:
return None
mesh.verts[0].co = points[0]
mesh.verts[1].co = points[1]
return mesh
def elev(mesh):
# put camera-perpendicular segments vertically
mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]]
points = helper.elevate_segment(bounds, points)
if points is None:
return None
points = helper.clip_segment(bounds, points)
if points is None:
return None
mesh.verts[0].co = points[0]
mesh.verts[1].co = points[1]
return mesh
for obj in grep(view_coll):
view_coll.objects.unlink(obj)
grids = []
for element in tool.Ifc.get().by_type("IfcGridAxis"):
obj = tool.Ifc.get_object(element)
if not obj:
continue
grids.append((*localize(*disassemble(clone(obj))), element))
if clipping:
grids = [(obj, clip(mesh), element) for obj, mesh, element in grids]
elif elevating:
grids = [(obj, elev(mesh), element) for obj, mesh, element in grids]
for obj, mesh, element in grids:
if mesh is not None:
view_coll.objects.link(assemble(obj, mesh))
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcAnnotation", context_id=subcontext.id())
annotation = tool.Ifc.get_entity(obj)
annotation.Name = element.AxisTag
annotation.ObjectType = "GRID"
return {"FINISHED"}
class AddSectionsAnnotations(bpy.types.Operator):
bl_idname = "bim.add_sections_annotations"
bl_label = "Add Sections"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
camera = helper.get_active_drawing(context.scene)[1]
return camera and camera.data.type == "ORTHO"
def execute(self, context):
scene = context.scene
view_coll, camera = helper.get_active_drawing(scene)
bounds = helper.ortho_view_frame(camera.data)
drawings = [
d
for d in scene.DocProperties.drawings
if (
d.camera is not camera
and d.camera.data.type == "ORTHO"
and d.camera.data.BIMCameraProperties.target_view == "SECTION_VIEW"
)
]
def sideview(cam):
# leftmost and rightmost points of camera view area, local coords
xmin, xmax, _, _, _, _ = helper.ortho_view_frame(cam.data, margin=0)
proj = camera.matrix_world.inverted() @ cam.matrix_world
p_l = proj @ Vector((xmin, 0, 0))
p_r = proj @ Vector((xmax, 0, 0))
return helper.clip_segment(bounds, (p_l, p_r))
def annotation(drawing, points):
# object with path geometry
name = drawing.name
curve = bpy.data.curves.new(f"Section/{name}", "CURVE")
spline = curve.splines.new("POLY") # has 1 initial point
spline.points.add(1)
p1, p2 = points
z = bounds[4] - 1e-5 # zmin
spline.points[0].co = (p1.x, p1.y, z, 1)
spline.points[1].co = (p2.x, p2.y, z, 1)
obj = bpy.data.objects.new(f"IfcAnnotation/Section/{name}", curve)
obj.matrix_world = camera.matrix_world
return obj
for d in drawings:
points = sideview(d.camera)
if points is None:
continue
obj = annotation(d, points)
view_coll.objects.link(obj)
return {"FINISHED"}
class EditText(bpy.types.Operator, Operator):
bl_idname = "bim.edit_text"
@@ -1304,7 +1273,8 @@ class EditText(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.edit_text(tool.Ifc, tool.Drawing, obj=context.active_object)
core.edit_text(tool.Drawing, obj=context.active_object)
tool.Blender.update_viewport()
class EnableEditingText(bpy.types.Operator, Operator):
@@ -1324,6 +1294,58 @@ class DisableEditingText(bpy.types.Operator, Operator):
def _execute(self, context):
core.disable_editing_text(tool.Drawing, obj=context.active_object)
# force update this object's font size for viewport display
DecoratorData.data.pop(context.object.name, None)
tool.Blender.update_viewport()
class AddTextLiteral(bpy.types.Operator):
bl_idname = "bim.add_text_literal"
bl_label = "Add text literal"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
# similar to `tool.Drawing.import_text_attributes`
literal_props = obj.BIMTextProperties.literals.add()
literal_attributes = literal_props.attributes
literal_attr_values = {
"Literal": "Literal",
"Path": "RIGHT",
"BoxAlignment": "bottom_left",
}
# emulates `blenderbim.bim.helper.import_attributes2(ifc_literal, literal_props.attributes)`
for attr_name in literal_attr_values:
attr = literal_attributes.add()
attr.name = attr_name
if attr_name == "Path":
attr.data_type = "enum"
attr.enum_items = '["DOWN", "LEFT", "RIGHT", "UP"]'
attr.enum_value = literal_attr_values[attr_name]
else:
attr.data_type = "string"
attr.string_value = literal_attr_values[attr_name]
box_alignment_mask = [False] * 9
box_alignment_mask[6] = True # bottom_left box_alignment
literal_props.box_alignment = box_alignment_mask
return {"FINISHED"}
class RemoveTextLiteral(bpy.types.Operator):
bl_idname = "bim.remove_text_literal"
bl_label = "Remove text literal"
bl_options = {"REGISTER", "UNDO"}
literal_prop_id: bpy.props.IntProperty()
def execute(self, context):
obj = context.active_object
obj.BIMTextProperties.literals.remove(self.literal_prop_id)
return {"FINISHED"}
class EditAssignedProduct(bpy.types.Operator, Operator):
bl_idname = "bim.edit_assigned_product"
@@ -1335,6 +1357,7 @@ class EditAssignedProduct(bpy.types.Operator, Operator):
if context.active_object.BIMAssignedProductProperties.relating_product:
product = tool.Ifc.get_entity(context.active_object.BIMAssignedProductProperties.relating_product)
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=context.active_object, product=product)
tool.Blender.update_viewport()
class EnableEditingAssignedProduct(bpy.types.Operator, Operator):
@@ -25,7 +25,8 @@ import blenderbim.tool as tool
import blenderbim.core.drawing as core
import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.decoration as decoration
from blenderbim.bim.module.drawing.data import DrawingsData
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData
from blenderbim.bim.module.drawing.data import refresh as refresh_drawing_data
from pathlib import Path
from blenderbim.bim.prop import Attribute, StrProperty
from bpy.types import PropertyGroup
@@ -38,6 +39,7 @@ from bpy.props import (
FloatProperty,
FloatVectorProperty,
CollectionProperty,
BoolVectorProperty,
)
@@ -212,6 +214,7 @@ def toggleDecorations(self, context):
collection = context.scene.camera.users_collection[0]
for obj in collection.objects:
tool.Drawing.update_text_value(obj)
refresh_drawing_data()
decoration.DecorationsHandler.install(context)
else:
decoration.DecorationsHandler.uninstall()
@@ -227,10 +230,6 @@ def getVectorStyles(self, context):
return vector_styles_enum
def refreshFontSize(self, context):
annotation.Annotator.resize_text(context.active_object)
class Variable(PropertyGroup):
name: StringProperty(name="Name")
prop_key: StringProperty(name="Property Key")
@@ -381,10 +380,66 @@ class BIMCameraProperties(PropertyGroup):
return False
DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
BOX_ALIGNMENT_POSITIONS = [
"top-left",
"top-middle",
"top-right",
"middle-left",
"center",
"middle-right",
"bottom-left",
"bottom-middle",
"bottom-right",
]
class Literal(PropertyGroup):
def set_box_alignment(self, new_value):
markers = new_value.count(True)
if not markers:
return
if markers > 1:
prev_value = self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
# looking for the first value changed to positive
first_changed_value = next((i for i in range(9) if new_value[i] and new_value[i] != prev_value[i]), None)
# if nothing have changed we just keep the previous value
if first_changed_value is None:
return
new_value = [False] * 9
new_value[first_changed_value] = True
self["box_alignment"] = new_value
position_string = BOX_ALIGNMENT_POSITIONS[next(i for i in range(9) if new_value[i])]
self.attributes["BoxAlignment"].set_value(position_string)
def get_box_alignment(self):
return self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
attributes: CollectionProperty(name="Attributes", type=Attribute)
# Current text value with evaluated experessions stored in `value`.
# The original (Literal) value stored in `attributes['Literal']`
# and can be accessed with `get_text()`
value: StringProperty(name="Value", default="TEXT")
box_alignment: BoolVectorProperty(
name="Box alignment", size=9, set=set_box_alignment, get=get_box_alignment, default=DEFAULT_BOX_ALIGNMENT
)
ifc_definition_id: IntProperty(name="IFC definition ID", default=0)
def get_literal_edited_data(self):
text_data = {
"CurrentValue": self.attributes["Literal"].string_value,
"Literal": self.attributes["Literal"].string_value,
"BoxAlignment": self.attributes["BoxAlignment"].string_value,
}
return text_data
class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
attributes: CollectionProperty(name="Attributes", type=Attribute)
value: StringProperty(name="Value", default="TEXT")
literals: CollectionProperty(name="Literals", type=Literal)
font_size: EnumProperty(
items=[
("1.8", "1.8 - Small", ""),
@@ -394,10 +449,24 @@ class BIMTextProperties(PropertyGroup):
("7.0", "7.0 - Title", ""),
],
default="2.5",
update=refreshFontSize,
name="Font Size",
)
def get_text_edited_data(self):
"""should be called only if `is_editing`
otherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
because this data could be out of date
"""
literals_data = []
for literal in self.literals:
literal_data = literal.get_literal_edited_data()
literals_data.append(literal_data)
text_data = {
"Literals": literals_data,
"FontSize": float(self.font_size),
}
return text_data
class BIMAssignedProductProperties(PropertyGroup):
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
@@ -17,11 +17,13 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import uuid
import ntpath
import pystache
import urllib.parse
import xml.etree.ElementTree as ET
import blenderbim.tool as tool
import ifcopenshell.util.geolocation
from shutil import copy
from xml.dom import minidom
@@ -182,6 +184,9 @@ class SheetBuilder:
tree = ET.parse(sheet_path)
root = tree.getroot()
self.defs = ET.Element("defs")
root.append(self.defs)
self.build_titleblock(root, sheet)
self.build_drawings(root, sheet)
self.build_schedules(root)
@@ -192,7 +197,14 @@ class SheetBuilder:
def build_titleblock(self, root, sheet):
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
titleblock.append(self.parse_embedded_svg(image, sheet.get_info()))
g = self.parse_embedded_svg(image, sheet.get_info())
grid_north = ifcopenshell.util.geolocation.get_grid_north(tool.Ifc.get()) * -1
true_north = ifcopenshell.util.geolocation.get_true_north(tool.Ifc.get()) * -1
for north in g.iterfind('.//{http://www.w3.org/2000/svg}g[@data-type="grid-north"]'):
north.attrib["transform"] = f"rotate({grid_north})"
for north in g.iterfind('.//{http://www.w3.org/2000/svg}g[@data-type="true-north"]'):
north.attrib["transform"] = f"rotate({true_north})"
titleblock.append(g)
titleblock.remove(image)
def build_drawings(self, root, sheet):
@@ -271,6 +283,20 @@ class SheetBuilder:
group.attrib["transform"] = "translate({},{})".format(
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
)
# Convert viewBox into a clip path
clip_id = str(uuid.uuid4())
group.attrib["clip-path"] = f"url(#{clip_id})"
clip_path = ET.Element("clipPath")
clip_path.attrib["id"] = clip_id
rect = ET.Element("rect")
rect.attrib["x"] = "0"
rect.attrib["y"] = "0"
rect.attrib["width"] = str(self.convert_to_mm(image.attrib.get("width")))
rect.attrib["height"] = str(self.convert_to_mm(image.attrib.get("height")))
clip_path.append(rect)
self.defs.append(clip_path)
svg_path = self.get_href(image)
with open(os.path.join(self.data_dir, "sheets", svg_path), "r") as template:
embedded = ET.fromstring(pystache.render(template.read(), data))
@@ -282,7 +308,10 @@ class SheetBuilder:
for image in images:
new_href = ntpath.basename(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
image.attrib["{http://www.w3.org/1999/xlink}href"] = new_href
group.append(embedded)
for child in embedded:
if "namedview" in child.tag:
continue
group.append(child)
return group
def convert_to_mm(self, value):
@@ -31,10 +31,12 @@ import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.module.drawing.annotation as annotation
from math import pi
from mathutils import Vector
from mathutils import geometry
from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.ifc import IfcStore
from math import pi, ceil, atan, degrees
from mathutils import geometry, Vector
from bpy_extras import view3d_utils
@@ -63,8 +65,10 @@ class SvgWriter:
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
self.related_paths = None
def create_blank_svg(self, output_path):
self.define_related_paths() # making sure all paths are defined
self.calculate_scale()
self.svg = svgwrite.Drawing(
output_path,
@@ -90,39 +94,66 @@ class SvgWriter:
)
return self
def define_boilerplate(self, stylesheet=None, markers=None, symbols=None, patterns=None):
self.add_stylesheet(stylesheet)
self.add_markers(markers)
self.add_symbols(symbols)
self.add_patterns(patterns)
def define_related_paths(self, **related_paths):
if not self.related_paths:
self.related_paths = {}
if not related_paths:
related_paths = {
"Stylesheet": os.path.join(self.data_dir, "styles", f"default.css"),
"Markers": os.path.join(self.data_dir, "templates", "markers.svg"),
"Symbols": os.path.join(self.data_dir, "templates", "symbols.svg"),
"Patterns": os.path.join(self.data_dir, "templates", "patterns.svg"),
}
for path_name in list(related_paths.keys()):
if path_name in self.related_paths:
del related_paths[path_name]
for path_name in related_paths:
uri = related_paths[path_name]
custom_path = tool.Ifc.resolve_uri(uri)
if custom_path:
self.related_paths[path_name] = custom_path
def define_boilerplate(self):
self.add_stylesheet()
self.add_markers()
self.add_symbols()
self.add_patterns()
return self
def calculate_scale(self):
# svg_scale is for conversion from m to paper units
# paper units = mm x drawing scale
self.svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
self.raw_width = self.camera_width
self.raw_height = self.camera_height
self.width = self.raw_width * self.svg_scale
self.height = self.raw_height * self.svg_scale
def add_stylesheet(self, uri):
default_stylesheet = os.path.join(self.data_dir, "styles", f"default.css")
with open(tool.Ifc.resolve_uri(uri) or default_stylesheet, "r") as stylesheet:
def add_stylesheet(self):
with open(self.related_paths["Stylesheet"], "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self, uri):
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "markers.svg"))
def add_markers(self):
tree = ET.parse(self.related_paths["Markers"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_symbols(self, uri):
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "symbols.svg"))
def add_symbols(self):
tree = ET.parse(self.related_paths["Symbols"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_patterns(self, uri):
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "patterns.svg"))
def find_xml_symbol_by_id(self, id):
tree = ET.parse(self.related_paths["Symbols"])
xml_symbol = tree.find(f'.//*[@id="{id}"]')
return External(xml_symbol) if xml_symbol else None
def add_patterns(self):
tree = ET.parse(self.related_paths["Patterns"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
@@ -160,6 +191,8 @@ class SvgWriter:
self.draw_section_level_annotation(obj)
elif element.ObjectType == "TEXT":
self.draw_text_annotation(obj, obj.location)
elif element.ObjectType in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.draw_fall_annotations(obj)
else:
self.draw_misc_annotation(obj)
@@ -207,7 +240,7 @@ class SvgWriter:
"alignment-baseline": "baseline",
"dominant-baseline": "baseline",
}
self.svg.add(self.svg.text(f"RL +{rl}", insert=tuple(text_position), **text_style))
self.svg.add(self.svg.text(f"RL +{rl}", insert=tuple(text_position), class_="SECTIONLEVEL", **text_style))
if tag:
self.svg.add(self.svg.text(tag, insert=(text_position[0], text_position[1] - 5), **text_style))
@@ -234,6 +267,7 @@ class SvgWriter:
self.svg.text(
"UP",
insert=tuple(text_position),
class_="STAIR",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
@@ -266,6 +300,7 @@ class SvgWriter:
self.svg.text(
axis_tag,
insert=tuple(start * self.svg_scale),
class_="GRID",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
@@ -277,6 +312,7 @@ class SvgWriter:
self.svg.text(
axis_tag,
insert=tuple(end * self.svg_scale),
class_="GRID",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
@@ -328,7 +364,7 @@ class SvgWriter:
if custom_classes:
classes.extend(custom_classes.split())
for key in self.metadata:
value = ifcopenshell.util.selector.Selector.get_element_value(element, key)
value = ifcopenshell.util.selector.get_element_value(element, key)
if value:
classes.append(self.canonicalise_class_name(key) + "-" + self.canonicalise_class_name(str(value)))
return classes
@@ -360,23 +396,135 @@ class SvgWriter:
self.draw_edge_annotation(obj, classes)
def draw_edge_annotation(self, obj, classes):
predefined_type = classes[2].split("-", 1)[1]
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
matrix_world = obj.matrix_world
for edge in obj.data.edges:
v0_global = matrix_world @ obj.data.vertices[edge.vertices[0]].co.xyz
v1_global = matrix_world @ obj.data.vertices[edge.vertices[1]].co.xyz
def draw_simple_edge_annotation(v0, v1):
v0_global = matrix_world @ obj.data.vertices[v0].co.xyz
v1_global = matrix_world @ obj.data.vertices[v1].co.xyz
v0 = self.project_point_onto_camera(v0_global)
v1 = self.project_point_onto_camera(v1_global)
start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
vector = end - start
line = self.svg.add(
self.svg.line(
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
)
self.svg.line(start=start * self.svg_scale, end=end * self.svg_scale, class_=" ".join(classes))
)
# BATTING ANNOTATIONS
if predefined_type == "BATTING":
v0_global = matrix_world @ obj.data.vertices[0].co.xyz
v1_global = matrix_world @ obj.data.vertices[1].co.xyz
v0 = self.project_point_onto_camera(v0_global)
v1 = self.project_point_onto_camera(v1_global)
start_svg = Vector(((x_offset + v0.x), (y_offset - v0.y))) * self.svg_scale
end_svg = Vector(((x_offset + v1.x), (y_offset - v1.y))) * self.svg_scale
element = tool.Ifc.get_entity(obj)
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Batting") or {}
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# default thickness set to 15 mm in paper space to keep the insulation visible
thickness = pset_data["Thickness"] * unit_scale * self.svg_scale if "Thickness" in pset_data else 15.0
reverse_x = pset_data.get("Reverse pattern direction", False)
if reverse_x:
start_svg, end_svg = end_svg, start_svg
pattern_dir = (end_svg - start_svg).normalized()
pattern_length = (end_svg - start_svg).length
segment_width = thickness / 2.5
segments = ceil(pattern_length / segment_width)
end_marker_width = pattern_length - segment_width * (segments - 1)
points = [start_svg + pattern_dir * segment_width * i for i in range(segments)]
marker_id = f"batting-{element.GlobalId}"
marker_end_id = f"batting-end-{element.GlobalId}"
path_data = f"""M 0 {0.2*thickness}
A {0.5*segment_width} {0.2*thickness} 0 0 1 {segment_width} {0.2*thickness}
L {0.5*segment_width} {0.8*thickness}
M 0 {0.2*thickness}
L {0.5*segment_width} {0.8*thickness}
A {0.5*segment_width} {0.2*thickness} 0 0 0 {segment_width} {1.0*thickness}
M {0.5*segment_width} {0.8*thickness}
A {0.5*segment_width} {0.2*thickness} 0 0 1 0 {1.0*thickness}
"""
path_data = " ".join(path_data.split())
svg_path = self.svg.path(style="fill: none; stroke:black; stroke-width:0.18", d=path_data)
if reverse_x:
svg_path.update({"transform": f"scale(-1,-1) translate(-{segment_width}, -{thickness})"})
marker = self.svg.marker(
markerUnits="userSpaceOnUse",
insert=(0, thickness / 2),
size=(segment_width, thickness),
orient="auto",
id=marker_id,
)
marker.add(svg_path)
self.svg.add(marker)
# separate marker for the end of the pattern to truncate it more gracefully
marker_end = marker.copy()
marker_end.update({"markerWidth": end_marker_width, "id": marker_end_id})
self.svg.add(marker_end)
polyline_style = (
f"marker-start: url(#{marker_id}); "
f"marker-mid: url(#{marker_id}); stroke: none; "
f"marker-end: url(#{marker_end_id}); stroke: none; "
)
self.svg.add(self.svg.polyline(points=points, class_=" ".join(classes), style=polyline_style))
elif predefined_type == "SECTION":
display_data = DecoratorData.get_section_markers_display_data(obj)
connect_markers = display_data["connect_markers"]
if connect_markers:
for edge in obj.data.edges:
draw_simple_edge_annotation(*edge.vertices[:])
else:
for edge in obj.data.edges:
v0_marker_position = "start" if edge.vertices[0] == 0 else "end"
v1_marker_position = "start" if edge.vertices[1] == 0 else "end"
v0_global = matrix_world @ obj.data.vertices[edge.vertices[0]].co.xyz
v1_global = matrix_world @ obj.data.vertices[edge.vertices[1]].co.xyz
v0 = self.project_point_onto_camera(v0_global)
v1 = self.project_point_onto_camera(v1_global)
start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
edge_dir = (end - start).normalized()
circle_radius = 5
segment_size = circle_radius * 3
if display_data[v0_marker_position]["add_symbol"]:
self.svg.add(
self.svg.line(
start=start * self.svg_scale,
end=start * self.svg_scale + edge_dir * segment_size,
class_=" ".join(classes),
)
)
if display_data[v1_marker_position]["add_symbol"]:
self.svg.add(
self.svg.line(
start=end * self.svg_scale,
end=end * self.svg_scale - edge_dir * segment_size,
class_=" ".join(classes),
)
)
else:
for edge in obj.data.edges:
draw_simple_edge_annotation(*edge.vertices[:])
def draw_leader_annotation(self, obj):
self.draw_line_annotation(obj)
spline = obj.data.splines[0]
@@ -393,65 +541,89 @@ class SvgWriter:
self.draw_line_annotation(obj)
v1 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, 0)))
v2 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, -1)))
angle = -math.degrees((v2 - v1).xy.angle_signed(Vector((0, 1))))
display_data = DecoratorData.get_section_markers_display_data(obj)
for vert in obj.data.vertices:
point = obj.matrix_world @ vert.co
symbol_position = self.project_point_onto_camera(point)
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
transform = "rotate({}, {}, {})".format(
angle,
(symbol_position * self.svg_scale)[0],
(symbol_position * self.svg_scale)[1],
)
for edge in obj.data.edges:
edge_verts = [obj.data.vertices[v_i] for v_i in edge.vertices]
self.svg.add(
self.svg.use("#section-arrow", insert=tuple(symbol_position * self.svg_scale), transform=transform)
)
self.svg.add(self.svg.use("#section-tag", insert=tuple(symbol_position * self.svg_scale)))
# convert edge vertiices coordinates to svg space
# to calculate the edge angle and for later use
edge_verts_svg = []
for vert in edge_verts:
point = obj.matrix_world @ vert.co
symbol_position = self.project_point_onto_camera(point)
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
symbol_position_svg = symbol_position * self.svg_scale
edge_verts_svg.append(symbol_position_svg)
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
text_position = list(symbol_position * self.svg_scale)
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
self.svg.add(self.svg.text(reference_id, insert=(text_position[0], text_position[1] - 2.5), **text_style))
self.svg.add(self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), **text_style))
edge_dir = (edge_verts_svg[1] - edge_verts_svg[0]).normalized()
angle = degrees(edge_dir.xy.angle_signed(Vector([1, 0])))
for v_i, symbol_position_svg in zip(edge.vertices, edge_verts_svg):
current_marker_position = "start" if v_i == 0 else "end"
# marker arrow symbol
if display_data[current_marker_position]["add_symbol"]:
transform = "rotate({}, {}, {})".format(angle, *symbol_position_svg.xy)
symbol_id = display_data[current_marker_position]["symbol"]
self.svg.add(self.svg.use(f"#{symbol_id}", insert=symbol_position_svg, transform=transform))
# marker circle and it's text
if display_data[current_marker_position]["add_circle"]:
self.svg.add(self.svg.use("#section-tag", insert=symbol_position_svg))
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
text_position = symbol_position_svg
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
self.svg.add(
self.svg.text(
reference_id,
insert=(text_position[0], text_position[1] - 2.5),
class_="SECTION",
**text_style,
)
)
self.svg.add(
self.svg.text(
sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="SECTION", **text_style
)
)
def draw_elevation_annotation(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
symbol_position = self.project_point_onto_camera(obj.location)
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
symbol_position_svg = symbol_position * self.svg_scale
v1 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, 0)))
v2 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, -1)))
angle = -math.degrees((v2 - v1).xy.angle_signed(Vector((0, 1))))
transform = "rotate({}, {}, {})".format(
angle,
(symbol_position * self.svg_scale)[0],
(symbol_position * self.svg_scale)[1],
)
transform = "rotate({}, {}, {})".format(angle, *symbol_position_svg.xy)
self.svg.add(
self.svg.use("#elevation-arrow", insert=tuple(symbol_position * self.svg_scale), transform=transform)
)
self.svg.add(self.svg.use("#elevation-tag", insert=tuple(symbol_position * self.svg_scale)))
self.svg.add(self.svg.use("#elevation-arrow", insert=symbol_position_svg, transform=transform))
self.svg.add(self.svg.use("#elevation-tag", insert=symbol_position_svg))
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
text_position = list(symbol_position * self.svg_scale)
text_position = symbol_position_svg
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
self.svg.add(self.svg.text(reference_id, insert=(text_position[0], text_position[1] - 2.5), **text_style))
self.svg.add(self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), **text_style))
self.svg.add(
self.svg.text(
reference_id, insert=(text_position[0], text_position[1] - 2.5), class_="ELEVATION", **text_style
)
)
self.svg.add(
self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="ELEVATION", **text_style)
)
def get_reference_and_sheet_id_from_annotation(self, element):
reference_id = "-"
@@ -474,11 +646,12 @@ class SvgWriter:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
element = tool.Ifc.get_entity(text_obj)
rep = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
text_literal = [i for i in rep.Items if i.is_a("IfcTextLiteral")][0]
text_literals = tool.Drawing.get_text_literal(text_obj, return_list=True)
product = tool.Drawing.get_assigned_product(element)
text_position = self.project_point_onto_camera(position)
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
text_position_svg = text_position * self.svg_scale
local_x_axis = text_obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
projected_x_axis = self.project_point_onto_camera(position + local_x_axis)
@@ -488,67 +661,82 @@ class SvgWriter:
)
)
transform = "rotate({}, {}, {})".format(
angle,
(text_position * self.svg_scale)[0],
(text_position * self.svg_scale)[1],
)
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
classes_str = " ".join(self.get_attribute_classes(text_obj))
symbol = tool.Drawing.get_annotation_symbol(element)
template_text_fields = []
if symbol:
self.svg.add(self.svg.use(f"#{symbol}", insert=tuple(text_position * self.svg_scale)))
symbol_svg = self.find_xml_symbol_by_id(symbol)
if symbol_svg:
symbol_xml = symbol_svg.get_xml()
template_text_fields = symbol_xml.findall('.//text[@data-type="text-template"]')
# if there is a symbol with template text fields
# then we just populate it's fields with the data from text literals
if template_text_fields:
symbol_xml.attrib["transform"] = f"translate({', '.join(map(str, text_position_svg))})"
symbol_xml.attrib.pop("id")
# note: zip makes sure that we iterate over the shortest list
for field, text_literal in zip(template_text_fields, text_literals):
field.text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
field.attrib["class"] = classes_str
self.svg.add(symbol_svg)
return None
if text_literal.BoxAlignment == "top-left":
alignment_baseline = "hanging"
text_anchor = "start"
elif text_literal.BoxAlignment == "top-middle":
alignment_baseline = "hanging"
text_anchor = "middle"
elif text_literal.BoxAlignment == "top-right":
alignment_baseline = "hanging"
text_anchor = "end"
elif text_literal.BoxAlignment == "middle-left":
alignment_baseline = "middle"
text_anchor = "start"
elif text_literal.BoxAlignment == "center":
alignment_baseline = "middle"
text_anchor = "middle"
elif text_literal.BoxAlignment == "middle-right":
alignment_baseline = "middle"
text_anchor = "end"
elif text_literal.BoxAlignment == "bottom-left":
alignment_baseline = "baseline"
text_anchor = "start"
elif text_literal.BoxAlignment == "bottom-middle":
alignment_baseline = "baseline"
text_anchor = "middle"
elif text_literal.BoxAlignment == "bottom-right":
alignment_baseline = "baseline"
text_anchor = "end"
if not symbol_svg or not template_text_fields:
self.svg.add(self.svg.use(f"#{symbol}", insert=text_position_svg))
literal = text_literal.Literal
def get_box_alignment_parameters(box_alignment):
# reference for alignment values:
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/alignment-baseline
vertical_alignment = {
"top": "hanging",
"bottom": "baseline",
"center": "middle",
"middle": "middle",
}
alignment_baseline = vertical_alignment[
next(align for align in vertical_alignment if align in box_alignment)
]
horizontal_alignment = {
"left": "start",
"right": "end",
"center": "middle",
"middle": "middle",
}
text_anchor = horizontal_alignment[next(align for align in horizontal_alignment if align in box_alignment)]
product = tool.Drawing.get_assigned_product(element)
selector = ifcopenshell.util.selector.Selector
variables = {}
if product != None:
for variable in re.findall("{{.*?}}", literal):
literal = literal.replace(variable, str(selector.get_element_value(product, variable[2:-2]) or ""))
# using dominant-baseline because we plan to use <tspan> subtags
# otherwise alignment-baseline would be sufficient
return {
"dominant-baseline": alignment_baseline,
"text-anchor": text_anchor,
}
for line_number, text_line in enumerate(literal.replace("\\n", "\n").split("\n")):
self.svg.add(
self.svg.text(
text_line,
insert=tuple((text_position * self.svg_scale) + Vector((0, 3.5 * line_number))),
class_=" ".join(self.get_attribute_classes(text_obj)),
**{
"text-anchor": text_anchor,
"alignment-baseline": alignment_baseline,
"dominant-baseline": alignment_baseline,
"transform": transform,
},
)
for text_literal in text_literals:
# after pretty indentation some redundant spaces can occur in svg tags
# this is why we apply "font-size: 0;" to the text tag to remove those spaces
# and add clases to the tspan tags
# ref: https://github.com/IfcOpenShell/IfcOpenShell/issues/2833#issuecomment-1471584960
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
text_tag = self.svg.text(
"",
**{
"transform": transform,
"style": "font-size: 0;",
},
**get_box_alignment_parameters(text_literal.BoxAlignment),
)
self.svg.add(text_tag)
for line_number, text_line in enumerate(text.replace("\\n", "\n").split("\n")):
# position has to be inserted at tspan to avoid x offset between tspans
t_span = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
t_span.update({"dy": f"{line_number}em"})
text_tag.add(t_span)
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
@@ -613,6 +801,7 @@ class SvgWriter:
self.svg.text(
"RL +{}".format(rl),
insert=tuple(text_position),
class_="PLANLEVEL",
**{
"text-anchor": text_anchor,
"alignment-baseline": "baseline",
@@ -625,7 +814,7 @@ class SvgWriter:
points = obj.data.splines[0].points
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
points_chunked = [points[i:i+3] for i in range(len(points)-2)]
points_chunked = [points[i : i + 3] for i in range(len(points) - 2)]
for points_chunk in points_chunked:
points_2d = [view3d_utils.location_3d_to_region_2d(region, region_3d, p.co.xyz) for p in points_chunk]
@@ -651,7 +840,7 @@ class SvgWriter:
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(p) for p in points_chunk]
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in ((0, 1), (1, 2))]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in ((0, 1), (1, 2))]
self.draw_svg_3point_arc(obj, bm)
def draw_svg_3point_arc(self, obj, bm):
@@ -742,7 +931,7 @@ class SvgWriter:
angle_text = abs(round(math.degrees(angle), 3))
if is_reflex:
angle_text = 360 - angle_text
self.svg.add(self.svg.text(f"{angle_text}deg", insert=tuple(text_position), **text_style))
self.svg.add(self.svg.text(f"{angle_text}deg", insert=tuple(text_position), class_="ANGLE", **text_style))
# Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html
arc_proj_end_pts = [self.project_point_onto_camera(v) for v in arc_end_pts]
@@ -805,11 +994,85 @@ class SvgWriter:
"dominant-baseline": "middle",
}
radius = ((matrix_world @ points[-1].co) - (matrix_world @ points[-2].co)).length
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius)
tag = element.Description or f"R{radius}"
self.svg.add(self.svg.text(tag, insert=tuple(text_position), **text_style))
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
def draw_fall_annotations(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
matrix_world = obj.matrix_world
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
p0 = Vector(
(
(x_offset + projected_points[0].x) * self.svg_scale,
(y_offset - projected_points[0].y) * self.svg_scale,
)
)
p1 = Vector(
(
(x_offset + projected_points[1].x) * self.svg_scale,
(y_offset - projected_points[1].y) * self.svg_scale,
)
)
# generate label text
# same function as in decoration.py
def get_label_text():
B, A = [v.co.xyz for v in points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
O.z = B.z
run = (B - O).length
if run != 0:
angle_tg = rise / run
angle = round(degrees(atan(angle_tg)))
else:
angle = 90
# ues SLOPE_ANGLE as default
if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif element.ObjectType == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{helper.format_distance(rise)} / {helper.format_distance(run)}"
elif element.ObjectType == "SLOPE_PERCENT":
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
tag = element.Description or get_label_text()
text_offset = (p0 - p1).xy.normalized() * len(tag)
text_position = projected_points[0]
text_position = Vector(
((x_offset + text_position.x) * self.svg_scale, (y_offset - text_position.y) * self.svg_scale)
)
text_position += text_offset
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
def draw_diameter_annotations(self, obj):
classes = self.get_attribute_classes(obj)
@@ -886,6 +1149,7 @@ class SvgWriter:
self.svg.text(
text,
insert=tuple(text_position),
class_="DIMENSION",
**{
"transform": "rotate({} {} {})".format(rotation, text_position.x, text_position.y),
"text-anchor": "middle",
@@ -20,7 +20,13 @@ import bpy
import blenderbim.bim.helper
import blenderbim.tool as tool
from bpy.types import Panel
from blenderbim.bim.module.drawing.data import ProductAssignmentsData, TextData, SheetsData, SchedulesData, DrawingsData
from blenderbim.bim.module.drawing.data import (
ProductAssignmentsData,
SheetsData,
SchedulesData,
DrawingsData,
DecoratorData,
)
class BIM_PT_camera(Panel):
@@ -174,7 +180,9 @@ class BIM_PT_drawings(Panel):
col.alignment = "LEFT"
row2 = col.row(align=True)
row2.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
row2.operator("bim.duplicate_drawing", icon="COPYDOWN", text="").drawing = active_drawing.ifc_definition_id
row2.operator(
"bim.duplicate_drawing", icon="COPYDOWN", text=""
).drawing = active_drawing.ifc_definition_id
col = row.column()
col.alignment = "RIGHT"
op = row.operator("bim.open_view", icon="URL", text="")
@@ -291,6 +299,7 @@ class BIM_PT_product_assignments(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
@classmethod
def poll(cls, context):
@@ -325,6 +334,7 @@ class BIM_PT_text(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 0
@classmethod
def poll(cls, context):
@@ -336,24 +346,62 @@ class BIM_PT_text(Panel):
return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"]
def draw(self, context):
if not TextData.is_loaded:
TextData.load()
props = context.active_object.BIMTextProperties
obj = context.active_object
props = obj.BIMTextProperties
if props.is_editing:
# shares most of the code with EditTextPopup.draw()
# need to keep them in sync or move to some common function
row = self.layout.row(align=True)
row.operator("bim.edit_text", icon="CHECKMARK")
row.operator("bim.add_text_literal", icon="ADD", text="")
row.operator("bim.disable_editing_text", icon="CANCEL", text="")
blenderbim.bim.helper.draw_attributes(props.attributes, self.layout)
row = self.layout.row(align=True)
row.prop(props, "font_size")
for i, literal_props in enumerate(props.literals):
box = self.layout.box()
row = self.layout.row(align=True)
row = box.row(align=True)
row.label(text=f"Literal[{i}]:")
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
# skip BoxAlignment since we're going to format it ourselves
attributes = [a for a in literal_props.attributes if a.name != "BoxAlignment"]
blenderbim.bim.helper.draw_attributes(attributes, box)
# a bit hacky way to align box alignment widget
rows = [box.row(align=True) for i in range(3)]
for i in range(9):
if i % 3 == 0:
split = rows[i // 3].split(factor=0.1, align=True)
split.column()
split.prop(literal_props, "box_alignment", text="", index=i)
text_lines = ["Text box alignment:", literal_props.attributes["BoxAlignment"].string_value, ""]
for i in range(3):
split = rows[i].split(factor=0.1, align=False)
split.column()
split.label(text=text_lines[i])
else:
text_data = DecoratorData.get_ifc_text_data(obj)
row = self.layout.row()
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
for attribute in TextData.data["attributes"]:
row = self.layout.row(align=True)
row.label(text=attribute["name"])
row.label(text=attribute["value"])
row = self.layout.row(align=True)
row.label(text="FontSize")
row.label(text=str(text_data["FontSize"]))
for literal_data in text_data["Literals"]:
box = self.layout.box()
for attribute in literal_data:
row = box.row(align=True)
row.label(text=attribute)
row.label(text=literal_data[attribute])
class BIM_PT_annotation_utilities(Panel):
@@ -417,9 +465,17 @@ class BIM_PT_annotation_utilities(Panel):
op.data_type = "mesh"
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Batting", icon="FORCE_FORCE")
op.object_type = "BATTING"
op.data_type = "mesh"
op = row.operator("bim.add_annotation", text="Fill Area", icon="NODE_TEXTURE")
op.object_type = "FILL_AREA"
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Fall", icon="SORT_ASC")
op.object_type = "FALL"
op.data_type = "curve"
row = layout.row(align=True)
row.prop(self.props, "should_draw_decorations", text="Viewport Annotations")
row.enabled = context.scene.camera is not None
@@ -27,6 +27,9 @@ classes = (
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
operator.OverrideJoin,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
operator.OverrideOutlinerDelete,
operator.OverridePasteBuffer,
operator.RemoveConnection,
@@ -35,6 +38,7 @@ classes = (
operator.SwitchRepresentation,
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
prop.BIMObjectGeometryProperties,
prop.BIMGeometryProperties,
ui.BIM_PT_derived_placements,
ui.BIM_PT_representations,
@@ -48,23 +52,37 @@ addon_keymaps = []
def register():
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
bpy.types.OBJECT_PT_transform.append(ui.BIM_PT_transform)
bpy.types.VIEW3D_MT_object.append(ui.object_menu)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_object_join", "J", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.override_object_duplicate_move", "D", "PRESS", shift=True)
kmi.properties.is_interactive = True
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked", "D", "PRESS", alt=True)
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
kmi.properties["name"] = "bim.override_paste_buffer"
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
kmi.properties.confirm = False
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
kmi.properties["name"] = "bim.override_paste_buffer"
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS")
kmi = km.keymap_items.new("bim.override_outliner_delete", "DEL", "PRESS")
def unregister():
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
bpy.types.OBJECT_PT_transform.remove(ui.BIM_PT_transform)
del bpy.types.Scene.BIMGeometryProperties
del bpy.types.Object.BIMGeometryProperties
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
@@ -35,12 +35,18 @@ class RepresentationsData:
@classmethod
def load(cls):
cls.data = {"representations": cls.representations()}
cls.data["contexts"] = cls.contexts()
cls.is_loaded = True
@classmethod
def representations(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
active_representation_id = None
if bpy.context.active_object.data and hasattr(bpy.context.active_object.data, "BIMMeshProperties"):
active_representation_id = bpy.context.active_object.data.BIMMeshProperties.ifc_definition_id
representations = []
if element.is_a("IfcProduct") and element.Representation:
representations = element.Representation.Representations
@@ -57,6 +63,7 @@ class RepresentationsData:
"ContextIdentifier": "",
"TargetView": "",
"RepresentationType": representation_type or "",
"is_active": representation.id() == active_representation_id,
}
if representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext"):
data["ContextIdentifier"] = representation.ContextOfItems.ContextIdentifier or ""
@@ -64,6 +71,25 @@ class RepresentationsData:
results.append(data)
return results
@classmethod
def contexts(cls):
results = []
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
results.append((str(element.id()), element.ContextType or "Unnamed", ""))
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationSubContext", include_subtypes=False):
results.append(
(
str(element.id()),
"{}/{}/{}".format(
element.ContextType or "Unnamed",
element.ContextIdentifier or "Unnamed",
element.TargetView or "Unnamed",
),
"",
)
)
return results
class ConnectionsData:
data = {}
@@ -38,6 +38,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bm.faces.ensure_lookup_table()
face = None
@@ -61,6 +62,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -88,6 +90,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -289,6 +292,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -385,6 +389,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bm.faces.ensure_lookup_table()
faces = bm.faces
@@ -28,12 +28,12 @@ import ifcopenshell.api
import blenderbim.core.geometry as core
import blenderbim.core.style
import blenderbim.core.root
import blenderbim.core.drawing
import blenderbim.tool as tool
import blenderbim.bim.handler
from mathutils import Vector
from blenderbim.bim import import_ifc
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.root.prop import get_contexts
class Operator:
@@ -59,27 +59,20 @@ class AddRepresentation(bpy.types.Operator, Operator):
bl_idname = "bim.add_representation"
bl_label = "Add Representation"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
context_id: bpy.props.IntProperty()
ifc_representation_class: bpy.props.StringProperty()
profile_set_usage: bpy.props.IntProperty()
def _execute(self, context):
ifc_context = self.context_id
if not ifc_context and get_contexts(self, context):
ifc_context = int(context.scene.BIMRootProperties.contexts or "0") or None
ifc_context = int(context.active_object.BIMGeometryProperties.contexts or "0") or None
if ifc_context:
ifc_context = tool.Ifc.get().by_id(ifc_context)
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
obj=context.active_object,
context=ifc_context,
ifc_representation_class=self.ifc_representation_class,
profile_set_usage=tool.Ifc.get().by_id(self.profile_set_usage) if self.profile_set_usage else None,
ifc_representation_class=None,
profile_set_usage=None,
)
@@ -357,6 +350,8 @@ class OverrideDeleteTrait:
element = tool.Ifc.get_entity(obj)
if not element:
return
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
return blenderbim.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
IfcStore.delete_element(element)
if obj.users_collection and obj.users_collection[0].name == obj.name:
parent = ifcopenshell.util.element.get_aggregate(element)
@@ -396,8 +391,8 @@ class OverrideDeleteTrait:
class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
bl_idname = "object.delete"
bl_label = "Delete"
bl_idname = "bim.override_object_delete"
bl_label = "IFC Delete"
bl_options = {"REGISTER", "UNDO"}
use_global: bpy.props.BoolProperty(default=False)
confirm: bpy.props.BoolProperty(default=True)
@@ -425,15 +420,19 @@ class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
def _execute(self, context):
for obj in context.selected_objects:
self.delete_ifc_object(obj)
bpy.data.objects.remove(obj)
try:
obj.name
bpy.data.objects.remove(obj)
except:
pass
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
return {"FINISHED"}
class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
bl_idname = "outliner.delete"
bl_label = "Delete"
bl_idname = "bim.override_outliner_delete"
bl_label = "IFC Delete"
bl_options = {"REGISTER", "UNDO"}
hierarchy: bpy.props.BoolProperty(default=False)
@@ -500,9 +499,10 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
class OverrideDuplicateMove(bpy.types.Operator):
bl_idname = "object.duplicate_move"
bl_label = "Duplicate Objects"
bl_idname = "bim.override_object_duplicate_move"
bl_label = "IFC Duplicate Objects"
bl_options = {"REGISTER", "UNDO"}
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@classmethod
def poll(cls, context):
@@ -529,7 +529,8 @@ class OverrideDuplicateMove(bpy.types.Operator):
new_obj.select_set(True)
if new_active_obj:
context.view_layer.objects.active = new_active_obj
bpy.ops.transform.translate("INVOKE_DEFAULT")
if self.is_interactive:
bpy.ops.transform.translate("INVOKE_DEFAULT")
return {"FINISHED"}
def _execute(self, context):
@@ -553,13 +554,14 @@ class OverrideDuplicateMove(bpy.types.Operator):
old_to_new[tool.Ifc.get_entity(obj)] = [new]
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
bpy.ops.transform.translate("INVOKE_DEFAULT")
if self.is_interactive:
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
class OverrideDuplicateMoveLinked(bpy.types.Operator):
bl_idname = "object.duplicate_move_linked"
bl_label = "Duplicate Linked"
bl_idname = "bim.override_object_duplicate_move_linked"
bl_label = "IFC Duplicate Linked"
@classmethod
def poll(cls, context):
@@ -611,9 +613,96 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
return {"FINISHED"}
class OverrideJoin(bpy.types.Operator, Operator):
bl_idname = "bim.override_object_join"
bl_label = "IFC Join"
def _execute(self, context):
if not tool.Ifc.get():
return bpy.ops.object.join()
if not context.active_object:
return
self.target = context.active_object
self.target_element = tool.Ifc.get_entity(self.target)
if self.target_element:
return self.join_ifc_obj()
return self.join_blender_obj()
def join_ifc_obj(self):
representation = tool.Ifc.get().by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in ("Tessellation", "Brep"):
for obj in bpy.context.selected_objects:
if obj == self.target:
continue
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
bpy.ops.object.join()
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
elif representation.RepresentationType == "SweptSolid":
target_placement = np.array(self.target.matrix_world)
items = list(representation.Items)
for obj in bpy.context.selected_objects:
if obj == self.target:
continue
element = tool.Ifc.get_entity(obj)
# Non IFC elements cannot be join since we cannot guarantee SweptSolid compliance
if not element:
obj.select_set(False)
continue
# Only objects of the same representation type can be joined
obj_rep = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if obj_rep.RepresentationType != "SweptSolid":
obj.select_set(False)
continue
placement = np.array(obj.matrix_world)
for item in obj_rep.Items:
copied_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
if copied_item.Position:
position = ifcopenshell.util.placement.get_axis2placement(copied_item.Position)
else:
position = np.eye(4)
position = placement @ position
position = np.linalg.inv(target_placement) @ position
copied_item.Position = tool.Ifc.get().createIfcAxis2Placement3D(
tool.Ifc.get().createIfcCartesianPoint([float(n) for n in position[:, 3][:3]]),
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 2][:3]]),
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 0][:3]]),
)
items.append(copied_item)
tool.Ifc.delete(element)
representation.Items = items
bpy.ops.object.join()
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=self.target,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
def join_blender_obj(self):
for obj in bpy.context.selected_objects:
if obj == self.target:
continue
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
bpy.ops.object.join()
class OverridePasteBuffer(bpy.types.Operator):
bl_idname = "bim.override_paste_buffer"
bl_label = "Paste BIM Objects"
bl_label = "IFC Paste BIM Objects"
def execute(self, context):
bpy.ops.view3d.pastebuffer()
@@ -621,3 +710,134 @@ class OverridePasteBuffer(bpy.types.Operator):
for obj in context.selected_objects:
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
return {"FINISHED"}
class OverrideModeSetEdit(bpy.types.Operator):
bl_idname = "bim.override_mode_set_edit"
bl_label = "IFC Mode Set Edit"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
objs = context.selected_objects or [context.active_object]
if context.active_object:
context.active_object.select_set(True)
edited_objs = []
for obj in objs:
if not obj:
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
# We are switching from OBJECT to EDIT mode.
usage_type = tool.Model.get_usage_type(element)
if usage_type:
# Parametric objects shall not be edited as meshes as they
# can be modified to be incompatible with the parametric
# constraints.
obj.select_set(False)
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in ("Tessellation", "Brep"):
if element.HasOpenings:
# Mesh elements with openings must disable openings
# so that you can edit the original topology.
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=False,
)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
if not context.selected_objects:
return {"FINISHED"}
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
return {"FINISHED"}
def invoke(self, context, event):
if not tool.Ifc.get():
return bpy.ops.object.mode_set(mode="EDIT", toggle=True)
return self.execute(context)
class OverrideModeSetObject(bpy.types.Operator):
bl_idname = "bim.override_mode_set_object"
bl_label = "IFC Mode Set Object"
bl_options = {"REGISTER", "UNDO"}
should_save: bpy.props.BoolProperty(name="Should Save", default=True)
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
for obj in self.edited_objs:
if self.should_save:
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
if tool.Ifc.get_entity(obj).HasOpenings:
self.reload_representation(obj)
else:
self.reload_representation(obj)
for obj in self.unchanged_objs_with_openings:
self.reload_representation(obj)
return {"FINISHED"}
def reload_representation(self, obj):
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
def draw(self, context):
row = self.layout.row(align=True)
row.prop(self, "should_save")
def invoke(self, context, event):
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
if not tool.Ifc.get():
return {"FINISHED"}
objs = context.selected_objects or [context.active_object]
self.edited_objs = []
self.unchanged_objs_with_openings = []
for obj in objs:
if not obj:
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in (
"Tessellation",
"Brep",
) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
elif element.HasOpenings:
self.unchanged_objs_with_openings.append(obj)
if self.edited_objs:
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
@@ -18,6 +18,7 @@
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.module.geometry.data import RepresentationsData
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -31,6 +32,16 @@ from bpy.props import (
)
def get_contexts(self, context):
if not RepresentationsData.is_loaded:
RepresentationsData.load()
return RepresentationsData.data["contexts"]
class BIMObjectGeometryProperties(PropertyGroup):
contexts: EnumProperty(items=get_contexts, name="Contexts")
class BIMGeometryProperties(PropertyGroup):
# Revit workaround
should_use_presentation_style_assignment: BoolProperty(name="Force Presentation Style Assignment", default=False)
@@ -23,6 +23,12 @@ from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.geometry.data import RepresentationsData, ConnectionsData, DerivedPlacementsData
def object_menu(self, context):
self.layout.operator("bim.override_object_duplicate_move", icon="PLUGIN")
self.layout.operator("bim.override_object_delete", icon="PLUGIN")
self.layout.operator("bim.override_paste_buffer", icon="PLUGIN")
class BIM_PT_representations(Panel):
bl_label = "IFC Representations"
bl_idname = "BIM_PT_representations"
@@ -30,6 +36,7 @@ class BIM_PT_representations(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "BIM_PT_geometry_object"
@classmethod
@@ -51,7 +58,7 @@ class BIM_PT_representations(Panel):
layout.label(text="No representations found")
row = layout.row(align=True)
prop_with_search(row, context.scene.BIMRootProperties, "contexts", text="")
prop_with_search(row, context.active_object.BIMGeometryProperties, "contexts", text="")
row.operator("bim.add_representation", icon="ADD", text="")
for representation in RepresentationsData.data["representations"]:
@@ -60,7 +67,7 @@ class BIM_PT_representations(Panel):
row.label(text=representation["ContextIdentifier"])
row.label(text=representation["TargetView"])
row.label(text=representation["RepresentationType"])
op = row.operator("bim.switch_representation", icon="OUTLINER_DATA_MESH", text="")
op = row.operator("bim.switch_representation", icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH", text="")
op.should_switch_all_meshes = True
op.should_reload = True
op.ifc_definition_id = representation["id"]
@@ -75,6 +82,7 @@ class BIM_PT_connections(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "BIM_PT_geometry_object"
@classmethod
@@ -179,6 +187,7 @@ class BIM_PT_derived_placements(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "OBJECT_PT_transform"
def draw(self, context):
@@ -75,6 +75,7 @@ class BIM_PT_object_groups(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "BIM_PT_utilities_object"
@classmethod
@@ -100,6 +100,7 @@ class BIM_PT_library_references(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "BIM_PT_misc_object"
@classmethod
@@ -242,7 +242,7 @@ class RemoveConstituent(bpy.types.Operator, tool.Ifc.Operator):
constituent: bpy.props.IntProperty()
def _execute(self, context):
for inverse in tool.Ifc.get().get_inverse(layer):
for inverse in tool.Ifc.get().get_inverse(tool.Ifc.get().by_id(self.constituent)):
if inverse.is_a("IfcMaterialConstituentSet") and len(inverse.MaterialConstituents) == 1:
return
ifcopenshell.api.run(
@@ -277,7 +277,7 @@ class RemoveProfile(bpy.types.Operator, tool.Ifc.Operator):
profile: bpy.props.IntProperty()
def _execute(self, context):
for inverse in tool.Ifc.get().get_inverse(layer):
for inverse in tool.Ifc.get().get_inverse(tool.Ifc.get().by_id(self.profile)):
if inverse.is_a("IfcMaterialProfileSet") and len(inverse.MaterialProfiles) == 1:
return
ifcopenshell.api.run("material.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile))
@@ -335,7 +335,7 @@ class RemoveLayer(bpy.types.Operator, tool.Ifc.Operator):
layer: bpy.props.IntProperty()
def _execute(self, context):
for inverse in tool.Ifc.get().get_inverse(layer):
for inverse in tool.Ifc.get().get_inverse(tool.Ifc.get().by_id(self.layer)):
if inverse.is_a("IfcMaterialLayerSet") and len(inverse.MaterialLayers) == 1:
return
ifcopenshell.api.run("material.remove_layer", tool.Ifc.get(), layer=tool.Ifc.get().by_id(self.layer))
@@ -583,7 +583,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
blenderbim.bim.helper.import_attributes2(material_set_item, self.props.material_set_item_attributes)
if material_set_item.is_a("IfcMaterialProfile"):
if material_set_item.Profile:
if material_set_item.Profile and material_set_item.Profile.ProfileName:
self.mprops.profiles = str(material_set_item.Profile.id())
return {"FINISHED"}
@@ -664,35 +664,23 @@ class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.file = IfcStore.get_file()
material = ifcopenshell.util.element.get_material(
self.file.by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
for obj in tool.Blender.get_selected_objects():
if obj == context.active_object:
continue
if not obj.BIMObjectProperties.ifc_definition_id:
continue
ifcopenshell.api.run(
"material.copy_material",
self.file,
**{
"material": material,
"element": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
},
)
self.set_default_material(obj, material)
blender_material = context.active_object.active_material
material = tool.Ifc.get_entity(blender_material)
copied_material = ifcopenshell.api.run("material.copy_material", tool.Ifc.get(), material=material)
copied_blender_material = bpy.data.materials.new(blender_material.name)
copied_style = self.get_style(copied_material)
tool.Ifc.link(copied_material, copied_blender_material)
if copied_style:
tool.Ifc.link(copied_style, copied_blender_material)
context.active_object.active_material = copied_blender_material
def set_default_material(self, obj, material):
object_material_ids = [
om.BIMObjectProperties.ifc_definition_id
for om in obj.data.materials
if om is not None and om.BIMObjectProperties.ifc_definition_id
]
if material.id() in object_material_ids:
return
obj.data.materials.append(IfcStore.get_element(material.id()))
def get_style(self, material):
for material_representation in material.HasRepresentation:
for representation in material_representation.Representations:
for item in representation.Items:
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
return style
class ExpandMaterialCategory(bpy.types.Operator):
@@ -92,6 +92,7 @@ class BIM_PT_material(Panel):
material_id = context.active_object.active_material.BIMObjectProperties.ifc_definition_id
if bool(material_id):
row.operator("bim.remove_material", icon="X", text="Remove IFC Material").material = material_id
row.operator("bim.copy_material", icon="DUPLICATE", text="")
row.operator("bim.unlink_material", icon="UNLINKED", text="")
else:
op = row.operator("bim.add_material", icon="ADD", text="Create IFC Material")
@@ -158,8 +159,6 @@ class BIM_PT_object_material(Panel):
op.material_set_usage = ObjectMaterialData.data["material_id"]
row.operator("bim.disable_editing_assigned_material", icon="CANCEL", text="")
else:
if ObjectMaterialData.data["material_class"] == "IfcMaterial":
row.operator("bim.copy_material", icon="COPYDOWN", text="")
row.operator("bim.enable_editing_assigned_material", icon="GREASEPENCIL", text="")
row.operator("bim.unassign_material", icon="X", text="")
@@ -25,7 +25,9 @@ from . import (
array,
product,
wall,
roof,
slab,
space,
stair,
window,
opening,
@@ -34,6 +36,8 @@ from . import (
profile,
sverchok_modifier,
door,
railing,
roof,
)
classes = (
@@ -47,9 +51,8 @@ classes = (
product.AddEmptyType,
product.AlignProduct,
product.ChangeTypePage,
product.DisplayConstrTypes,
product.LoadTypeThumbnails,
product.ReinvokeOperator,
product.MirrorElements,
workspace.Hotkey,
wall.AlignWall,
wall.ChangeExtrusionDepth,
@@ -74,9 +77,13 @@ classes = (
opening.ShowOpenings,
profile.ChangeCardinalPoint,
profile.ChangeProfileDepth,
profile.DisableEditingExtrusionAxis,
profile.EditExtrusionAxis,
profile.EnableEditingExtrusionAxis,
profile.ExtendProfile,
profile.RecalculateProfile,
profile.Rotate90,
roof.GenerateHippedRoof,
slab.DisableEditingExtrusionProfile,
slab.DisableEditingSketchExtrusionProfile,
slab.EditExtrusionProfile,
@@ -85,6 +92,7 @@ classes = (
slab.EnableEditingSketchExtrusionProfile,
slab.ResetVertex,
slab.SetArcIndex,
space.GenerateSpace,
prop.ConstrTypeInfo,
prop.ConstrClassInfo,
prop.ConstrBrowserState,
@@ -94,13 +102,16 @@ classes = (
prop.BIMSverchokProperties,
prop.BIMWindowProperties,
prop.BIMDoorProperties,
prop.BIMRailingProperties,
prop.BIMRoofProperties,
ui.BIM_PT_authoring,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
ui.BIM_PT_window,
ui.BIM_PT_door,
ui.DisplayConstrTypesUI,
ui.BIM_PT_railing,
ui.BIM_PT_roof,
ui.LaunchTypeManager,
ui.HelpConstrTypes,
ui.BIM_MT_model,
@@ -134,6 +145,24 @@ classes = (
door.FinishEditingDoor,
door.EnableEditingDoor,
door.RemoveDoor,
railing.BIM_OT_add_railing,
railing.AddRailing,
railing.CancelEditingRailing,
railing.FinishEditingRailing,
railing.EnableEditingRailing,
railing.CancelEditingRailingPath,
railing.FinishEditingRailingPath,
railing.EnableEditingRailingPath,
railing.RemoveRailing,
roof.BIM_OT_add_roof,
roof.AddRoof,
roof.CancelEditingRoof,
roof.FinishEditingRoof,
roof.EnableEditingRoof,
roof.CancelEditingRoofPath,
roof.FinishEditingRoofPath,
roof.EnableEditingRoofPath,
roof.RemoveRoof,
)
addon_keymaps = []
@@ -148,10 +177,14 @@ def register():
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
bpy.types.Object.BIMWindowProperties = bpy.props.PointerProperty(type=prop.BIMWindowProperties)
bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties)
bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(window.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(railing.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(roof.add_object_button)
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
wm = bpy.context.window_manager
@@ -171,11 +204,15 @@ def unregister():
del bpy.types.Object.BIMSverchokProperties
del bpy.types.Object.BIMWindowProperties
del bpy.types.Object.BIMDoorProperties
del bpy.types.Object.BIMRailingProperties
del bpy.types.Object.BIMRoofProperties
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(railing.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(roof.add_object_button)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
@@ -36,8 +36,7 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0}
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("BBIM_Array", None)
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
if pset:
data = json.loads(pset["Data"])
@@ -77,8 +76,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMArrayProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Array"]
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
data = json.loads(pset["Data"])
data[self.item] = {
"children": data[self.item]["children"],
@@ -112,8 +110,7 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
psets = ifcopenshell.util.element.get_psets(element)
data = json.loads(psets["BBIM_Array"]["Data"])[self.item]
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props = obj.BIMArrayProperties
props.count = data["count"]
props.x = data["x"]
@@ -134,8 +131,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMArrayProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Array"]
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
data = json.loads(pset["Data"])
data[self.item]["count"] = 1
@@ -35,6 +35,8 @@ def refresh():
SverchokData.is_loaded = False
WindowData.is_loaded = False
DoorData.is_loaded = False
RailingData.is_loaded = False
RoofData.is_loaded = False
class AuthoringData:
@@ -68,7 +70,14 @@ class AuthoringData:
declaration = tool.Ifc.schema().declaration_by_name("IfcElementType")
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
names.extend(("IfcDoorStyle", "IfcWindowStyle"))
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names.extend([d.name() for d in declarations])
if tool.Ifc.get_schema() in ("IFC2X3", "IFC4"):
names.extend(("IfcDoorStyle", "IfcWindowStyle"))
version = tool.Ifc.get_schema()
return [(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted(names)]
@@ -90,7 +99,7 @@ class AuthoringData:
@classmethod
def type_thumbnail(cls):
if not cls.props.relating_type_id:
if not cls.data["relating_types_ids"]:
return 0
element = tool.Ifc.get().by_id(int(cls.props.relating_type_id))
return cls.type_thumbnails.get(element.id(), None) or 0
@@ -176,12 +185,7 @@ class AuthoringData:
def active_material_usage(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
material = ifcopenshell.util.element.get_material(element, should_inherit=False)
if material:
if material.is_a("IfcMaterialLayerSetUsage"):
return f"LAYER{material.LayerSetDirection[-1]}"
elif material.is_a("IfcMaterialProfileSetUsage"):
return "PROFILE"
return tool.Model.get_usage_type(element)
@classmethod
def ifc_classes(cls):
@@ -221,7 +225,8 @@ class AuthoringData:
@classmethod
def relating_types_browser(cls):
return cls.relating_types(ifc_class=cls.props.ifc_class_browser)
if cls.data["ifc_classes"]:
return cls.relating_types(ifc_class=cls.props.ifc_class_browser)
@classmethod
def new_constr_class_info(cls, ifc_class):
@@ -321,14 +326,14 @@ class AuthoringData:
def new_relating_type(cls, ifc_class=None, relating_type_id=None):
if ifc_class is None:
bpy.ops.bim.add_constr_type_instance(
ifc_class=cls.props.ifc_class, relating_type_id=int(cls.props.relating_type_id), link_to_scene=True
ifc_class=cls.props.ifc_class, relating_type_id=int(cls.props.relating_type_id)
)
else:
cls.props.updating = True
cls.props.ifc_class = ifc_class
cls.props.relating_type_id = str(relating_type_id)
cls.props.updating = False
bpy.ops.bim.add_constr_type_instance(link_to_scene=True)
bpy.ops.bim.add_constr_type_instance()
return bpy.context.selected_objects[-1]
@staticmethod
@@ -459,3 +464,43 @@ class DoorData:
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
class RailingData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Railing", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
class RoofData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Roof", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
@@ -0,0 +1,269 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import gpu
import bgl
import bmesh
import blenderbim.tool as tool
from math import pi, degrees, sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
class ProfileDecorator:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def __call__(self, context):
obj = context.active_object
if obj.mode != "EDIT":
return
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
all_vertices = []
error_vertices = []
selected_vertices = []
unselected_vertices = []
special_vertices = []
special_vertex_indices = {}
selected_edges = []
unselected_edges = []
special_edges = [] # edges that have a circle or an arc associated with them
arc_groups = []
circle_groups = []
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
arc_groups.append(i)
elif "IFCCIRCLE" in group.name:
circle_groups.append(i)
arcs = {}
circles = {}
bm = bmesh.from_edit_mesh(obj.data)
# https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess
# This is how we access vertex groups via bmesh, apparently, it's not very intuitive
deform_layer = bm.verts.layers.deform.active
for vertex in bm.verts:
co = tuple(obj.matrix_world @ vertex.co)
all_vertices.append(co)
if vertex.hide:
continue
is_arc = False
for group_index in arc_groups:
if group_index in vertex[deform_layer]:
is_arc = True
break
if is_arc:
arcs.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
is_circle = False
for group_index in circle_groups:
if group_index in vertex[deform_layer]:
is_circle = True
break
if is_circle:
circles.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
if vertex.select:
selected_vertices.append(co)
else:
if len(vertex.link_edges) > 1 and is_circle:
error_vertices.append(co)
elif is_circle:
special_vertices.append(co)
elif len(vertex.link_edges) != 2:
error_vertices.append(co)
elif is_arc:
special_vertices.append(co)
else:
unselected_vertices.append(co)
for edge in bm.edges:
edge_indices = [v.index for v in edge.verts]
if edge.hide:
continue
if edge.select:
selected_edges.append(edge_indices)
else:
i1, i2 = edge.verts[0].index, edge.verts[1].index
if i1 in special_vertex_indices and special_vertex_indices[i1] == special_vertex_indices.get(i2, None):
special_edges.append(edge_indices)
else:
unselected_edges.append(edge_indices)
indices = [[v.index for v in e.verts] for e in bm.edges]
white = (1, 1, 1, 1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
blue = (0.157, 0.565, 1, 1)
grey = (0.2, 0.2, 0.2, 1)
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges)
self.shader.uniform_float("color", green)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=special_edges)
self.shader.uniform_float("color", grey)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": error_vertices})
self.shader.uniform_float("color", red)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": special_vertices})
self.shader.uniform_float("color", blue)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
# Draw arcs
arc_centroids = []
arc_segments = []
for arc in arcs.values():
if len(arc) != 3:
continue
sorted_arc = [None, None, None]
for v1 in arc:
connections = 0
for link_edge in v1.link_edges:
v2 = link_edge.other_vert(v1)
if v2 in arc:
connections += 1
if connections == 2: # Midpoint
sorted_arc[1] = v1
else:
sorted_arc[2 if sorted_arc[2] is None else 0] = v1
points = [tuple(obj.matrix_world @ v.co) for v in sorted_arc]
centroid = tool.Cad.get_center_of_arc(points)
if centroid:
arc_centroids.append(tuple(centroid))
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
batch = batch_for_shader(self.shader, "POINTS", {"pos": arc_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
for verts, edges in arc_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
# Draw circles
circle_centroids = []
circle_segments = []
for circle in circles.values():
if len(circle) != 2:
continue
p1 = obj.matrix_world @ circle[0].co
p2 = obj.matrix_world @ circle[1].co
radius = (p2 - p1).length / 2
centroid = p1.lerp(p2, 0.5)
circle_centroids.append(tuple(centroid))
segments = self.create_circle_segments(360, 20, radius)
matrix = obj.matrix_world.copy()
matrix.col[3] = centroid.to_4d()
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
circle_segments.append(segments)
batch = batch_for_shader(self.shader, "POINTS", {"pos": circle_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
for verts, edges in circle_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
# https://github.com/nortikin/sverchok/blob/master/nodes/generator/basic_3pt_arc.py
# This function is taken from Sverchok, licensed under GPL v2-or-later.
# This is a combination of the make_verts and make_edges function.
def create_circle_segments(self, Angle, Vertices, Radius):
if Angle < 360:
theta = Angle / (Vertices - 1)
else:
theta = Angle / Vertices
listVertX = []
listVertY = []
for i in range(Vertices):
listVertX.append(Radius * cos(radians(theta * i)))
listVertY.append(Radius * sin(radians(theta * i)))
if Angle < 360 and self.mode_ == 0:
sigma = radians(Angle)
listVertX[-1] = Radius * cos(sigma)
listVertY[-1] = Radius * sin(sigma)
elif Angle < 360 and self.mode_ == 1:
listVertX.append(0.0)
listVertY.append(0.0)
points = list((x, y, 0) for x, y in zip(listVertX, listVertY))
listEdg = [(i, i + 1) for i in range(Vertices - 1)]
if Angle < 360 and self.mode_ == 1:
listEdg.append((0, Vertices))
listEdg.append((Vertices - 1, Vertices))
else:
listEdg.append((Vertices - 1, 0))
return points, listEdg
@@ -30,7 +30,6 @@ import blenderbim.core.geometry as core
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.window import create_bm_window, create_bm_box, update_simple_openings
from blenderbim.bim.module.model.helper import replace_ifc_representation_for_object
from mathutils import Vector, Matrix
from pprint import pprint
@@ -80,20 +79,20 @@ def update_door_modifier_representation(context):
elevation_representation = ifcopenshell.api.run(
"geometry.add_door_representation", ifc_file, **representation_data
)
replace_ifc_representation_for_object(ifc_file, profile, obj, elevation_representation)
tool.Model.replace_object_ifc_representation(profile, obj, elevation_representation)
# MODEL_VIEW representation
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
representation_data["context"] = body
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
replace_ifc_representation_for_object(ifc_file, body, obj, model_representation)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# PLAN_VIEW representation
plan = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
if plan:
representation_data["context"] = plan
plan_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
replace_ifc_representation_for_object(ifc_file, plan, obj, plan_representation)
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
# adding switch representation at the end instead of changing order of representations
# to prevent #2744
@@ -107,11 +106,19 @@ def update_door_modifier_representation(context):
should_sync_changes_first=True,
)
# type attributes
element.OperationType = props.door_type
# occurences attributes
occurences = tool.Ifc.get_all_element_occurences(element)
for occurence in occurences:
occurence.OverallWidth = props.overall_width
occurence.OverallHeight = props.overall_height
update_simple_openings(element, props.overall_width, props.overall_height)
# TODO: move it out to tools
def bm_sort_out_geom(geom_data):
geom_dict = {"verts": [], "edges": [], "faces": []}
@@ -433,12 +440,12 @@ def update_door_modifier_bmesh(context):
obj.data.update()
class BIM_OT_add_door(Operator):
class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_door"
bl_label = "Door"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a door.")
@@ -453,7 +460,6 @@ class BIM_OT_add_door(Operator):
mesh = bpy.data.meshes.new("IfcDoor")
obj = bpy.data.objects.new("IfcDoor", mesh)
obj.location = spawn_location
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcDoor", should_add_representation=False
)
@@ -493,12 +499,9 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
door_data["lining_properties"] = lining_props
door_data["panel_properties"] = panel_props
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("BBIM_Door", None)
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Door")
ifcopenshell.api.run(
@@ -519,8 +522,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
psets = ifcopenshell.util.element.get_psets(element)
data = json.loads(psets["BBIM_Door"]["Data"])
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
props = obj.BIMDoorProperties
# restore previous settings since editing was canceled
for prop_name in data:
@@ -551,8 +553,6 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Door"]
door_data = props.get_general_kwargs()
lining_props = props.get_lining_kwargs()
panel_props = props.get_panel_kwargs()
@@ -564,7 +564,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
update_door_modifier_representation(context)
pset = tool.Ifc.get().by_id(pset["id"])
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
door_data = json.dumps(door_data, default=list)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
return {"FINISHED"}
@@ -579,8 +579,7 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_psets(element)
data = json.loads(pset["BBIM_Door"]["Data"])
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
data.update(data.pop("lining_properties"))
data.update(data.pop("panel_properties"))
@@ -609,8 +608,7 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
obj.BIMDoorProperties.is_editing = -1
pset = ifcopenshell.util.element.get_psets(element)
pset = tool.Ifc.get().by_id(pset["BBIM_Door"]["id"])
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
props.door_added_previously = True
@@ -1,67 +0,0 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 @Andrej730
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bmesh
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.core.geometry as core
import blenderbim.tool as tool
def get_ifc_context_or_create(ifc_file, context_type, context_identifier, target_view):
ifc_context = ifcopenshell.util.representation.get_context(ifc_file, context_type, context_identifier, target_view)
if not ifc_context:
parent_context = ifcopenshell.util.representation.get_context(ifc_file, context_type)
ifc_context = ifcopenshell.api.run(
"context.add_context",
ifc_file,
context_type=context_type,
context_identifier=context_identifier,
target_view=target_view,
parent=parent_context,
)
return ifc_context
def replace_ifc_representation_for_object(ifc_file, ifc_context, obj, new_representation):
ifc_element = tool.Ifc.get_entity(obj)
old_representation = ifcopenshell.util.representation.get_representation(
ifc_element, ifc_context.ContextType, ifc_context.ContextIdentifier, ifc_context.TargetView
)
if old_representation:
old_representation = tool.Geometry.resolve_mapped_representation(old_representation)
for inverse in ifc_file.get_inverse(old_representation):
ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation)
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=old_representation)
else:
ifcopenshell.api.run(
"geometry.assign_representation", ifc_file, product=ifc_element, representation=new_representation
)
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@@ -38,7 +38,7 @@ class MepGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
def generate(self, link_to_scene=True):
def generate(self):
self.file = tool.Ifc.get()
self.collection = bpy.context.view_layer.active_layer_collection.collection
@@ -55,15 +55,15 @@ class MepGenerator:
self.height = dimensions.get("NominalHeight")
self.length = 1
return self.derive_from_cursor(link_to_scene=link_to_scene)
return self.derive_from_cursor()
elif self.relating_type.is_a("IfcPipeSegmentType"):
pass
def derive_from_cursor(self, link_to_scene):
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
return self.create_rectangle_segment(link_to_scene)
return self.create_rectangle_segment()
def create_rectangle_segment(self, link_to_scene):
def create_rectangle_segment(self):
verts = [
Vector((-self.width / 2, self.height / 2, 0)),
Vector((-self.width / 2, -self.height / 2, 0)),
@@ -89,11 +89,11 @@ class MepGenerator:
ifc_class = ifc_classes[0]
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
if link_to_scene:
obj.location = self.location
obj.rotation_euler[0] = math.pi / 2
obj.rotation_euler[2] = math.pi / 2
self.collection.objects.link(obj)
obj.location = self.location
obj.rotation_euler[0] = math.pi / 2
obj.rotation_euler[2] = math.pi / 2
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(
obj=obj.name,
@@ -53,38 +53,47 @@ class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
class FilledOpeningGenerator:
def generate(self, filling_obj, voided_obj):
def generate(self, filling_obj, voided_obj, target=None):
props = bpy.context.scene.BIMModelProperties
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
filling = tool.Ifc.get_entity(filling_obj)
element = tool.Ifc.get_entity(voided_obj)
if not voided_obj or not filling_obj:
return
element = tool.Ifc.get_entity(voided_obj)
target = bpy.context.scene.cursor.location
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.01)
if not raycast[0]:
target = filling_obj.matrix_world.col[3].to_3d().copy()
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.5)
if filling.FillsVoids:
ifcopenshell.api.run(
"void.remove_opening", tool.Ifc.get(), opening=filling.FillsVoids[0].RelatingOpeningElement
)
if target is None:
target = bpy.context.scene.cursor.location
# Sometimes, the voided_obj may be an aggregate, which won't have any representation.
if voided_obj.data:
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.01)
if not raycast[0]:
return
target = filling_obj.matrix_world.col[3].to_3d().copy()
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.5)
if not raycast[0]:
return
# In this prototype, we assume openings are only added to axis-based elements
layers = tool.Model.get_material_layer_parameters(element)
axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"]
# In this prototype, we assume openings are only added to axis-based elements
layers = tool.Model.get_material_layer_parameters(element)
axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"]
new_matrix = voided_obj.matrix_world.copy()
new_matrix.col[3] = tool.Cad.point_on_edge(target, axis).to_4d()
new_matrix = voided_obj.matrix_world.copy()
new_matrix.col[3] = tool.Cad.point_on_edge(target, axis).to_4d()
if filling.is_a("IfcDoor"):
new_matrix[2][3] = voided_obj.matrix_world[2][3]
else:
new_matrix[2][3] = voided_obj.matrix_world[2][3] + (props.rl2 * unit_scale)
if filling.is_a("IfcDoor"):
new_matrix[2][3] = voided_obj.matrix_world[2][3]
else:
new_matrix[2][3] = voided_obj.matrix_world[2][3] + (props.rl2 * unit_scale)
filling_obj.matrix_world = new_matrix
bpy.context.view_layer.update()
filling_obj.matrix_world = new_matrix
bpy.context.view_layer.update()
if tool.Ifc.is_moved(voided_obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=voided_obj)
@@ -133,22 +142,29 @@ class FilledOpeningGenerator:
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element)
ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling)
representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
voided_objs = [voided_obj]
# Openings affect all subelements of an aggregate
for subelement in ifcopenshell.util.element.get_decomposition(element):
subobj = tool.Ifc.get_object(subelement)
if subobj:
voided_objs.append(subobj)
return {"FINISHED"}
for voided_obj in voided_objs:
if voided_obj.data:
representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
filling = settings["related_object"]
if not filling.FillsVoids:
if not getattr(filling, "FillsVoids", None):
return
opening = filling.FillsVoids[0].RelatingOpeningElement
@@ -223,10 +239,34 @@ class FilledOpeningGenerator:
return shape_builder.get_representation(context, [extrusion])
x, y, z = filling_obj.dimensions
opening_position = Vector([0.0, -0.1 / unit_scale, 0.0])
opening_size = Vector([x, 0, z]) / unit_scale
# Windows and doors can have a casing that overlaps the wall
# but shouldn't affect the size of the opening.
# So we shouldn't use object dimensions in that case. More: #2784
# Just keeping it for windows and doors for now to be safe
x_redefined, z_redefined = False, False
if filling.is_a() in ["IfcWindow", "IfcDoor"]:
if filling.OverallWidth:
opening_size.x = filling.OverallWidth
x_redefined = True
if filling.OverallHeight:
opening_size.z = filling.OverallHeight
z_redefined = True
# making sure if min_x or min_z != 0 to shift the opening accordingly
# to prevent something like #2784
if not x_redefined:
opening_position.x = min(v[0] for v in filling_obj.bound_box)
if not z_redefined:
opening_position.z = min(v[2] for v in filling_obj.bound_box)
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=Vector([x / unit_scale, 0.0, z / unit_scale])),
shape_builder.rectangle(size=opening_size),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
position=opening_position,
extrusion_vector=Vector([0.0, 1.0, 0.0]),
)
@@ -276,21 +316,28 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=obj.matrix_world
)
decomposed_building_elements = set()
for building_element in building_elements:
decomposed_building_elements.add(building_element)
decomposed_building_elements.update(ifcopenshell.util.element.get_decomposition(building_element))
for building_element in decomposed_building_elements:
building_obj = tool.Ifc.get_object(building_element)
body = ifcopenshell.util.representation.get_representation(
building_element, "Model", "Body", "MODEL_VIEW"
)
if body:
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=building_obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
if building_obj and building_obj.data:
body = ifcopenshell.util.representation.get_representation(
building_element, "Model", "Body", "MODEL_VIEW"
)
if body:
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=building_obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -605,7 +652,14 @@ class ShowOpenings(Operator, tool.Ifc.Operator):
continue
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
openings = tool.Model.load_openings(element, [r.RelatedOpeningElement for r in element.HasOpenings])
openings = tool.Model.load_openings(
element,
[
r.RelatedOpeningElement
for r in element.HasOpenings
if not tool.Ifc.get_object(r.RelatedOpeningElement)
],
)
for opening in openings:
new = props.openings.add()
new.obj = opening
@@ -665,18 +719,27 @@ class EditOpenings(Operator, tool.Ifc.Operator):
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
decomposed_building_objs = set()
for obj in building_objs:
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
decomposed_building_objs.add(obj)
for subelement in ifcopenshell.util.element.get_decomposition(tool.Ifc.get_entity(obj)):
subobj = tool.Ifc.get_object(subelement)
if subobj:
decomposed_building_objs.add(subobj)
for obj in decomposed_building_objs:
if obj.data:
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
def get_all_building_objects_of_similar_openings(self, opening):
@@ -31,11 +31,10 @@ import blenderbim.core.geometry
from . import wall, slab, profile, mep
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.prop import store_cursor_position
from blenderbim.bim.helper import draw_image_for_ifc_profile
from mathutils import Vector, Matrix
from bpy_extras.object_utils import AddObjectHelper
from . import prop
import json
def select_and_activate_single_object(context, obj):
@@ -69,7 +68,6 @@ class AddConstrTypeInstance(bpy.types.Operator):
ifc_class: bpy.props.StringProperty()
relating_type_id: bpy.props.IntProperty()
from_invoke: bpy.props.BoolProperty(default=False)
link_to_scene: bpy.props.BoolProperty(default=True)
def invoke(self, context, event):
return self.execute(context)
@@ -95,14 +93,14 @@ class AddConstrTypeInstance(bpy.types.Operator):
material = ifcopenshell.util.element.get_material(relating_type)
if material and material.is_a("IfcMaterialProfileSet"):
if profile.DumbProfileGenerator(relating_type).generate(link_to_scene=self.link_to_scene):
if profile.DumbProfileGenerator(relating_type).generate():
return {"FINISHED"}
elif material and material.is_a("IfcMaterialLayerSet"):
if self.generate_layered_element(ifc_class, relating_type, link_to_scene=self.link_to_scene):
if self.generate_layered_element(ifc_class, relating_type):
select_and_activate_single_object(context, context.selected_objects[-1])
return {"FINISHED"}
if relating_type.is_a("IfcFlowSegmentType") and not relating_type.RepresentationMaps:
if mep.MepGenerator(relating_type).generate(link_to_scene=self.link_to_scene):
if mep.MepGenerator(relating_type).generate():
return {"FINISHED"}
building_obj = None
@@ -133,17 +131,27 @@ class AddConstrTypeInstance(bpy.types.Operator):
mesh = bpy.data.meshes.new(name="Instance")
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(relating_type, instance_class), mesh)
if self.link_to_scene:
obj.location = context.scene.cursor.location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
collection_obj = bpy.data.objects.get(collection.name)
obj.location = context.scene.cursor.location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
collection_obj = bpy.data.objects.get(collection.name)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
element = tool.Ifc.get_entity(obj)
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
if self.link_to_scene:
# Update required as core.type.assign_type may change obj.data
context.view_layer.update()
# Update required as core.type.assign_type may change obj.data
context.view_layer.update()
# set occurences properties for the types defined with modifiers
if instance_class in ["IfcWindow", "IfcDoor"]:
pset_name = f"BBIM_{instance_class[3:]}"
bbim_pset = ifcopenshell.util.element.get_psets(element).get(pset_name, None)
if bbim_pset:
bbim_prop_data = json.loads(bbim_pset["Data"])
element.OverallWidth = bbim_prop_data["overall_width"]
element.OverallHeight = bbim_prop_data["overall_height"]
if (
building_obj
@@ -153,7 +161,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
if instance_class in ["IfcWindow", "IfcDoor"]:
# TODO For now we are hardcoding windows and doors as a prototype
bpy.ops.bim.add_filled_opening(voided_obj=building_obj.name, filling_obj=obj.name)
elif self.link_to_scene:
else:
if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id:
obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box])
@@ -175,7 +183,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
return {"FINISHED"}
@staticmethod
def generate_layered_element(ifc_class, relating_type, link_to_scene=True):
def generate_layered_element(ifc_class, relating_type):
layer_set_direction = None
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
@@ -189,9 +197,9 @@ class AddConstrTypeInstance(bpy.types.Operator):
obj = None
if layer_set_direction == "AXIS3":
obj = slab.DumbSlabGenerator(relating_type).generate(link_to_scene=link_to_scene)
obj = slab.DumbSlabGenerator(relating_type).generate()
elif layer_set_direction == "AXIS2":
obj = wall.DumbWallGenerator(relating_type).generate(link_to_scene=link_to_scene)
obj = wall.DumbWallGenerator(relating_type).generate()
else:
pass # Dumb block generator? Eh? :)
@@ -214,66 +222,6 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class DisplayConstrTypes(bpy.types.Operator):
bl_idname = "bim.display_constr_types"
bl_label = "Browse Construction Types"
bl_options = {"REGISTER"}
bl_description = "Display all available Construction Types to add new instances"
def invoke(self, context, event):
if not AuthoringData.is_loaded:
AuthoringData.load()
props = context.scene.BIMModelProperties
ifc_class = props.ifc_class
constr_class_info = AuthoringData.constr_class_info(ifc_class)
if constr_class_info is None or not constr_class_info.fully_loaded:
AuthoringData.assetize_constr_class(ifc_class)
bpy.ops.bim.display_constr_types_ui("INVOKE_DEFAULT")
return {"FINISHED"}
class ReinvokeOperator(bpy.types.Operator):
bl_idname = "bim.reinvoke_operator"
bl_label = "Reinvoke Popup Operator"
bl_options = {"REGISTER"}
bl_description = "Reinvoke a popup operator"
operator: bpy.props.StringProperty()
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
browser_state = context.scene.BIMModelProperties.constr_browser_state
store_cursor_position(context, event, window=False)
cursor_x, cursor_y = browser_state.cursor_x, browser_state.cursor_y
window_x, window_y = browser_state.window_x, browser_state.window_y
window = context.window
operator = self.operator
self.move_cursor_away(context, window)
def move_cursor_to_window():
window.cursor_warp(window_x, window_y)
def run_operator(operator, *args, **kwargs):
reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs)
def reinvoke():
run_operator(operator, "INVOKE_DEFAULT", reinvoked=True)
window.cursor_warp(cursor_x, cursor_y)
bpy.app.timers.register(move_cursor_to_window, first_interval=browser_state.update_delay)
bpy.app.timers.register(reinvoke, first_interval=3 * browser_state.update_delay)
return {"FINISHED"}
def move_cursor_away(self, context, window): # closes current popup
browser_state = context.scene.BIMModelProperties.constr_browser_state
window.cursor_warp(browser_state.far_away_x, browser_state.far_away_y)
@staticmethod
def run_operator(operator, *args, **kwargs):
reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs)
class AlignProduct(bpy.types.Operator):
bl_idname = "bim.align_product"
bl_label = "Align Product"
@@ -374,7 +322,7 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
material = ifcopenshell.util.element.get_material(element)
if material and material.is_a("IfcMaterialProfileSet"):
profile = material.MaterialProfiles[0].Profile
draw_image_for_ifc_profile(draw, profile, size)
tool.Profile.draw_image_for_ifc_profile(draw, profile, size)
elif material and material.is_a("IfcMaterialLayerSet"):
thicknesses = [l.LayerThickness for l in material.MaterialLayers]
@@ -450,6 +398,64 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mirror_elements"
bl_label = "Mirror Elements"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Faux-mirrors the selected object by an active empty along a mirror plane."
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
# This is not a true mirror operation. In BIM, objects that are
# mirrored are not the same type. (i.e. a mirrored asymmetric desk is a
# completely different product). To preserve types, we calculate
# bounding box centroids, and mirror the position of the object. The
# mirrored object performs the necessary rotation to preserve the
# mirrored intention, but never actually truly mirror anything (i.e.
# scale = -1).
# Objects are mirrored along the YZ plane of the mirror object.
# Mirrored objects have their relative local Y and Z axes preserved,
# and the new X axis is calculated.
# In theory, untyped objects may be truly mirrored, but this is not yet
# implemented.
mirror = context.active_object
reflection = Matrix()
reflection[0][0] = -1
mirror.select_set(False)
bpy.ops.bim.override_object_duplicate_move(is_interactive=False)
for obj in context.selected_objects:
if obj == mirror:
continue
objmat = mirror.matrix_world.inverted() @ obj.matrix_world.copy()
x, y, z = objmat.to_3x3().col
centroid = Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5)
c = mirror.matrix_world.inverted() @ obj.matrix_world @ centroid
newy = mirror.matrix_world.to_quaternion() @ (y @ reflection)
newz = mirror.matrix_world.to_quaternion() @ (z @ reflection)
newx = newy.cross(newz)
newc = mirror.matrix_world @ (c @ reflection)
newmat = Matrix((newx.to_4d(), newy.to_4d(), newz.to_4d(), newc.to_4d())).transposed()
newmat.col[3][0:3] = Vector(newmat.col[3][0:3]) - (newmat.to_quaternion() @ centroid)
obj.matrix_world = newmat
def copy_obj(self, obj):
new = obj.copy()
new.data = wall2.data.copy()
wall1.users_collection[0].objects.link(wall2)
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
def generate_box(usecase_path, ifc_file, settings):
box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW")
if not box_context:
@@ -472,7 +478,7 @@ def generate_box(usecase_path, ifc_file, settings):
"geometry.assign_representation",
ifc_file,
should_run_listeners=False,
**{"product": product, "representation": new_box}
**{"product": product, "representation": new_box},
)
@@ -18,7 +18,6 @@
import bpy
import copy
import math
import bmesh
import mathutils.geometry
import ifcopenshell
@@ -29,10 +28,10 @@ import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.type
import blenderbim.core.geometry
from blenderbim.bim.ifc import IfcStore
from math import pi, degrees, inf
from mathutils import Vector, Matrix
from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.decorator import ProfileDecorator
def element_listener(element, obj):
@@ -48,16 +47,15 @@ def mode_callback(obj, data):
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
product = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
return
if obj.mode == "EDIT":
IfcStore.edited_objs.add(obj)
tool.Ifc.edit(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
bm.free()
else:
material_usage = ifcopenshell.util.element.get_material(product)
x, y = obj.dimensions[0:2]
@@ -96,9 +94,9 @@ class DumbProfileGenerator:
self.relating_type = relating_type
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self, link_to_scene=True):
self.file = IfcStore.get_file()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
def generate(self):
self.file = tool.Ifc.get()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
material = ifcopenshell.util.element.get_material(self.relating_type)
if material and material.is_a("IfcMaterialProfileSet"):
self.profile_set = material
@@ -114,13 +112,13 @@ class DumbProfileGenerator:
self.rotation = 0
self.location = Vector((0, 0, 0))
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
return self.derive_from_cursor(link_to_scene=link_to_scene)
return self.derive_from_cursor()
def derive_from_cursor(self, link_to_scene):
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
return self.create_profile(link_to_scene)
return self.create_profile()
def create_profile(self, link_to_scene):
def create_profile(self):
ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema)
# Standard cases are deprecated, so let's cull them
ifc_class = [c for c in ifc_classes if "StandardCase" not in c][0]
@@ -132,10 +130,9 @@ class DumbProfileGenerator:
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"]:
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
matrix_world.col[3] = self.location.to_4d()
if link_to_scene and self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world[2][3] = self.collection_obj.location[2]
if link_to_scene:
self.collection.objects.link(obj)
self.collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -190,8 +187,7 @@ class DumbProfileGenerator:
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"})
if link_to_scene:
obj.select_set(True)
obj.select_set(True)
return obj
@@ -330,7 +326,7 @@ class DumbProfileJoiner:
body = copy.deepcopy(axis1)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
si_length = unit_scale * length
end = profile1.matrix_world @ Vector((si_length, 0, 0))
end = profile1.matrix_world @ Vector((0, 0, si_length))
axis[1] = end
body[1] = end
self.recreate_profile(element1, profile1, axis, body)
@@ -502,9 +498,7 @@ class DumbProfileJoiner:
# Openings should move with the host overall ...
# ... except their position should stay the same along the local Z axis of the wall
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
percent = tool.Cad.edge_percent(
self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1))))
)
percent = tool.Cad.edge_percent(self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1)))))
is_z_offset_increased = True if percent < 0 else False
change_in_z = (self.body[0] - previous_origin).length / self.unit_scale
@@ -915,3 +909,121 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(objs)
return {"FINISHED"}
class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_extrusion_axis"
bl_label = "Enable Editing Extrusion Axis"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
obj = context.active_object
element = tool.Ifc.get_entity(obj)
axis = ifcopenshell.util.representation.get_representation(element, "Model", "Axis", "GRAPH_VIEW")
if axis:
position = obj.matrix_world.copy()
tool.Model.import_axis(axis.Items[0], obj=obj)
else:
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(body)
if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
position[0][3] *= self.unit_scale
position[1][3] *= self.unit_scale
position[2][3] *= self.unit_scale
else:
position = Matrix()
direction = Vector(extrusion.ExtrudedDirection.DirectionRatios).normalized()
tool.Model.import_axis([Vector((0, 0, 0)), direction * extrusion.Depth], obj=obj, position=position)
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context)
if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
return {"FINISHED"}
class DisableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_extrusion_axis"
bl_label = "Disable Editing Extrusion Axis"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_extrusion_axis"
bl_label = "Edit Extrusion Axis"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
start = obj.matrix_world @ obj.data.vertices[0].co.copy()
end = obj.matrix_world @ obj.data.vertices[1].co.copy()
depth = (end - start).length
z_axis = (end - start).normalized()
y_axis = Vector((0, 0, 1))
x_axis = y_axis.cross(z_axis).normalized()
y_axis = z_axis.cross(x_axis).normalized()
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
matrix = Matrix(
(
[x_axis[0], y_axis[0], z_axis[0], start.x],
[x_axis[1], y_axis[1], z_axis[1], start.y],
[x_axis[2], y_axis[2], z_axis[2], start.z],
[0, 0, 0, 1],
)
)
obj.matrix_world = matrix
bpy.context.view_layer.update()
joiner = DumbProfileJoiner()
joiner.set_depth(obj, depth / self.unit_scale)
return {"FINISHED"}
@@ -55,14 +55,6 @@ def get_type_predefined_type(self, context):
return AuthoringData.data["type_predefined_type"]
def store_cursor_position(context, event, cursor=True, window=True):
browser_state = context.scene.BIMModelProperties.constr_browser_state
if cursor:
browser_state.cursor_x, browser_state.cursor_y = event.mouse_x, event.mouse_y
if window:
browser_state.window_x, browser_state.window_y = event.mouse_x, event.mouse_y
def update_icon_id(self, context, browser=False):
if context == "lost_context" or (context.region is not None and context.region.type != "TOOL_HEADER"):
ifc_class = self.ifc_class_browser if browser else self.ifc_class
@@ -244,12 +236,15 @@ class BIMModelProperties(PropertyGroup):
("WINDOW", "Window", ""),
("DOOR", "Door", ""),
("STAIR", "Stair", ""),
("RAILING", "Railing", ""),
("ROOF", "Roof", ""),
),
name="Type Template",
default="MESH",
)
type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class)
type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
class BIMArrayProperties(PropertyGroup):
@@ -264,6 +259,7 @@ class BIMStairProperties(PropertyGroup):
stair_types = (
("CONCRETE", "Concrete", ""),
("WOOD/STEEL", "Wood / Steel", ""),
("GENERIC", "Generic", ""),
)
stair_added_previously: bpy.props.BoolProperty(default=False)
@@ -284,7 +280,6 @@ class BIMStairProperties(PropertyGroup):
"width": self.width,
"height": self.height,
"number_of_treads": self.number_of_treads,
"tread_depth": self.tread_depth,
"tread_run": self.tread_run,
}
@@ -294,11 +289,20 @@ class BIMStairProperties(PropertyGroup):
"base_slab_depth": self.base_slab_depth,
"top_slab_depth": self.top_slab_depth,
"has_top_nib": self.has_top_nib,
"tread_depth": self.tread_depth,
}
)
return stair_kwargs
elif self.stair_type == "WOOD/STEEL":
stair_kwargs.update(
{
"tread_depth": self.tread_depth,
}
)
return stair_kwargs
elif self.stair_type == "GENERIC":
return stair_kwargs
@@ -541,3 +545,54 @@ class BIMDoorProperties(PropertyGroup):
kwargs["frame_depth"] = self.frame_depth
return kwargs
class BIMRailingProperties(PropertyGroup):
railing_types = (("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),)
railing_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
is_editing_path: bpy.props.BoolProperty(default=False)
railing_type: bpy.props.EnumProperty(name="Railing Type", items=railing_types, default="FRAMELESS_PANEL")
height: bpy.props.FloatProperty(name="Height", default=1.0)
thickness: bpy.props.FloatProperty(name="Thickness", default=0.050)
spacing: bpy.props.FloatProperty(name="Spacing", default=0.050)
def get_general_kwargs(self):
return {
"railing_type": self.railing_type,
"height": self.height,
"thickness": self.thickness,
"spacing": self.spacing,
}
class BIMRoofProperties(PropertyGroup):
roof_types = (("HIP_ROOF", "HIP_ROOF", ""),)
roof_generation_methods = (
("HEIGHT", "HEIGHT", ""),
("ANGLE", "ANGLE", ""),
)
roof_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
is_editing_path: bpy.props.BoolProperty(default=False)
roof_type: bpy.props.EnumProperty(name="Roof Type", items=roof_types, default="HIP_ROOF")
generation_method: bpy.props.EnumProperty(
name="Roof Generation Method", items=roof_generation_methods, default="HEIGHT"
)
height: bpy.props.FloatProperty(name="Height", default=1.0)
angle: bpy.props.FloatProperty(name="Slope Angle", default=10, description="In degrees")
def get_general_kwargs(self):
kwargs = {
"roof_type": self.roof_type,
"generation_method": self.generation_method,
}
if self.generation_method == "HEIGHT":
kwargs["height"] = self.height
else:
kwargs["angle"] = self.angle
return kwargs
@@ -0,0 +1,432 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 @Andrej730
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import Operator
import bmesh
import ifcopenshell
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.tool as tool
import blenderbim.core.geometry as core
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.door import bm_sort_out_geom
from blenderbim.bim.module.model.data import RailingData, refresh
from blenderbim.bim.module.model.decorator import ProfileDecorator
from mathutils import Vector, Matrix
from pprint import pprint
import json
# reference:
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailing.htm
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailingType.htm
def bm_split_edge_at_offset(edge, offset):
v0, v1 = edge.verts
offset = offset / 2
edge_len = (v0.co - v1.co).xy.length
split_output_0 = bmesh.utils.edge_split(edge, v0, offset / edge_len)
split_output_1 = bmesh.utils.edge_split(edge, v1, offset / (edge_len - offset))
new_geometry = bm_sort_out_geom(split_output_0 + split_output_1)
return new_geometry
def update_railing_modifier_ifc_data(context):
"""should be called after new geometry settled
since it's going to update ifc representation
"""
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
# type attributes
element.PredefinedType = "USERDEFINED"
# occurences attributes
occurences = tool.Ifc.get_all_element_occurences(element)
for occurence in occurences:
occurence.ObjectType = props.railing_type
# update pset
pset_common = tool.Pset.get_element_pset(element, "Pset_RailingCommon")
if not pset_common:
pset_common = ifcopenshell.api.run("pset.add_pset", ifc_file, product=element, name="Pset_RailingCommon")
ifcopenshell.api.run(
"pset.edit_pset",
ifc_file,
pset=pset_common,
properties={
"Height": props.height,
},
)
tool.Ifc.edit(obj)
def update_bbim_railing_pset(element, railing_data):
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Railing")
railing_data = json.dumps(railing_data, default=list)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": railing_data})
def update_railing_modifier_bmesh(context):
obj = context.object
props = obj.BIMRailingProperties
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]["data_dict"]["path_data"]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# need to make sure we support edit mode
# since users will probably be in edit mode when they'll be changing railing path
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
# generating railing path
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in path_data["edges"]]
bm.verts.index_update()
bm.edges.index_update()
if props.is_editing_path:
tool.Blender.apply_bmesh(obj.data, bm)
return
# generating the entire railing
height = props.height * si_conversion
thickness = props.thickness * si_conversion
spacing = props.spacing * si_conversion
# spacing
# split each edge in 3 segments by 0.5 * spacing by x-y plane
main_edges = bm.edges[:]
for main_edge in main_edges:
bm_split_edge_at_offset(main_edge, spacing)
# thickness
# keep track of translated verts so we won't translate the same
# vert twice
edge_dissolving_verts = []
for main_edge in main_edges:
v0, v1 = main_edge.verts
edge_dissolving_verts.extend([v0, v1])
edge_dir = ((v1.co - v0.co) * V(1, 1, 0)).normalized()
ortho_vector = edge_dir.cross(V(0, 0, 1))
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
# dissolve middle edge
bmesh.ops.dissolve_edges(bm, edges=[main_edge])
# height
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
extrusion_vector = Vector((0, 0, 1)) * height
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
# dissolve middle edges
edges_to_dissolve = []
verts_to_dissolve = []
for v in edge_dissolving_verts:
for e in v.link_edges:
other_vert = e.other_vert(v)
if other_vert in extruded_verts:
edges_to_dissolve.append(e)
verts_to_dissolve.append(other_vert)
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bmesh.ops.dissolve_verts(bm, verts=verts_to_dissolve)
# to remove unnecessary verts in 0 spacing case
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
tool.Blender.apply_bmesh(obj.data, bm)
def get_path_data(obj):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if obj.mode == "EDIT":
# otherwise mesh may not contain all changes
# added in edit mode
obj.update_from_editmode()
mesh = obj.data
path_data = dict()
path_data["edges"] = [e.vertices[:] for e in mesh.edges]
path_data["verts"] = [v.co / si_conversion for v in mesh.vertices]
if not path_data["edges"] or not path_data["verts"]:
return None
return path_data
class BIM_OT_add_railing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_railing"
bl_label = "Railing"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a railing.")
return {"CANCELLED"}
if context.object is not None:
spawn_location = context.object.location.copy()
context.object.select_set(False)
else:
spawn_location = bpy.context.scene.cursor.location.copy()
mesh = bpy.data.meshes.new("IfcRailing")
obj = bpy.data.objects.new("IfcRailing", mesh)
obj.location = spawn_location
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcRailing",
should_add_representation=True,
context=body_context,
)
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.bim.add_railing()
return {"FINISHED"}
# UI operators
class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_railing"
bl_label = "Add Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if element.is_a() not in ("IfcRailing", "IfcRailingType"):
self.report({"ERROR"}, "Object has to be IfcRailing/IfcRailingType type to add a railing.")
return {"CANCELLED"}
# need to make sure all default props will have correct units
if not props.railing_added_previously:
skip_props = ("is_editing", "railing_type", "railing_added_previously")
convert_property_group_from_si(props, skip_props=skip_props)
railing_data = props.get_general_kwargs()
path_data = get_path_data(obj)
if not path_data:
path_data = {
"edges": [[0, 1], [1, 2]],
"verts": [
Vector([-1.0, 0.0, 0.0]) / si_conversion,
Vector([0.0, 0.0, 0.0]) / si_conversion,
Vector([1.0, 0.0, 0.0]) / si_conversion,
],
}
railing_data["path_data"] = path_data
update_bbim_railing_pset(element, railing_data)
refresh()
update_railing_modifier_bmesh(context)
update_railing_modifier_ifc_data(context)
return {"FINISHED"}
class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_railing"
bl_label = "Enable Editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Railing", "Data"))
data["path_data"] = json.dumps(data["path_data"])
# required since we could load pset from .ifc and BIMRailingProperties won't be set
for prop_name in data:
setattr(props, prop_name, data[prop_name])
# need to make sure all props that weren't used before
# will have correct units
skip_props = ("is_editing", "railing_type", "railing_added_previously")
skip_props += tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
return {"FINISHED"}
class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_railing"
bl_label = "Cancel editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Railing", "Data"))
props = obj.BIMRailingProperties
# restore previous settings since editing was canceled
for prop_name in data:
setattr(props, prop_name, data[prop_name])
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = -1
return {"FINISHED"}
class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_railing"
bl_label = "Finish editing railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]["data_dict"]["path_data"]
railing_data = props.get_general_kwargs()
railing_data["path_data"] = path_data
props.is_editing = -1
update_bbim_railing_pset(element, railing_data)
update_railing_modifier_ifc_data(context)
return {"FINISHED"}
class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_railing_path"
bl_label = "Enable Editing Railing Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRailingProperties
props.is_editing_path = True
update_railing_modifier_bmesh(context)
if bpy.context.object.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
ProfileDecorator.install(context)
return {"FINISHED"}
class CancelEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_railing_path"
bl_label = "Cancel Editing Railing Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRailingProperties
ProfileDecorator.uninstall()
props.is_editing_path = False
update_railing_modifier_bmesh(context)
if bpy.context.object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT")
return {"FINISHED"}
class FinishEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_railing_path"
bl_label = "Finish Editing Railing Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
railing_data = props.get_general_kwargs()
path_data = get_path_data(obj)
railing_data["path_data"] = path_data
ProfileDecorator.uninstall()
props.is_editing_path = False
update_bbim_railing_pset(element, railing_data)
refresh() # RailingData has to be updated before run update_railing_modifier_bmesh
update_railing_modifier_bmesh(context)
if bpy.context.object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT")
update_railing_modifier_ifc_data(context)
return {"FINISHED"}
class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_railing"
bl_label = "Remove Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
obj.BIMRailingProperties.is_editing = -1
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
props.railing_added_previously = True
return {"FINISHED"}
def add_object_button(self, context):
self.layout.operator(BIM_OT_add_railing.bl_idname, icon="PLUGIN")
@@ -0,0 +1,467 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>, @Andrej730
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import Operator
import bmesh
import ifcopenshell
import blenderbim
import blenderbim.tool as tool
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.module.model.door import bm_sort_out_geom
from blenderbim.bim.module.model.data import RoofData, refresh
from blenderbim.bim.module.model.decorator import ProfileDecorator
import json
from math import tan, radians
from mathutils import Vector, Matrix
from bpypolyskel import bpypolyskel
import shapely
# reference:
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoof.htm
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoofType.htm
class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_hipped_roof"
bl_label = "Generate Hipped Roof"
bl_options = {"REGISTER"}
mode: bpy.props.StringProperty(default="ANGLE")
height: bpy.props.FloatProperty(default=1)
angle: bpy.props.FloatProperty(default=10)
def _execute(self, context):
obj = bpy.context.active_object
if not obj:
self.report({"ERROR"}, "Need to select some object first.")
return {"CANCELLED"}
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
# argument values are the defaults for `bpy.ops.mesh.dissolve_limited`
bmesh.ops.dissolve_limit(
bm, angle_limit=0.0872665, use_dissolve_boundaries=False, delimit={"NORMAL"}, edges=bm.edges[:], verts=bm.verts[:]
)
tool.Blender.apply_bmesh(obj.data, bm)
generate_hipped_roof(obj, self.mode, self.height, self.angle)
return {"FINISHED"}
def generate_hipped_roof(obj, mode="ANGLE", height=1.0, angle=10):
boundary_lines = []
for edge in obj.data.edges:
boundary_lines.append(
shapely.LineString([obj.data.vertices[edge.vertices[0]].co, obj.data.vertices[edge.vertices[1]].co])
)
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
roof_polygon = None
biggest_area = 0
for polygon in closed_polygons.geoms:
area = polygon.area
if area > biggest_area:
roof_polygon = polygon
biggest_area = area
roof_polygon = shapely.force_3d(roof_polygon)
if not shapely.is_ccw(roof_polygon):
roof_polygon = roof_polygon.reverse()
# Define vertices for the base footprint of the building at height 0.0
# counterclockwise order
verts = [Vector(v) for v in roof_polygon.exterior.coords[0:-1]]
total_exterior_verts = len(verts)
next_index = total_exterior_verts
inner_loops = None
for interior in roof_polygon.interiors:
if inner_loops is None:
inner_loops = []
loop = interior.coords[0:-1]
total_verts = len(loop)
verts.extend([Vector(v) for v in loop])
inner_loops.append((next_index, total_verts))
next_index += total_verts
unit_vectors = None # we have no unit vectors, let them computed by polygonize()
start_exterior_index = 0
faces = []
if mode == "HEIGHT":
height = height
angle = 0.0
else:
angle = tan(radians(round(angle, 4)))
height = 0.0
faces = bpypolyskel.polygonize(
verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors
)
edges = []
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
new_verts = [bm.verts.new(v) for v in verts]
new_edges = [bm.edges.new([new_verts[vi] for vi in edge]) for edge in edges]
new_faces = [bm.faces.new([new_verts[vi] for vi in face]) for face in faces]
extrusion_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
extruded_verts = bm_sort_out_geom(extrusion_geom)["verts"]
bmesh.ops.translate(bm, vec=[0.0, 0.0, 0.1], verts=extruded_verts)
tool.Blender.apply_bmesh(obj.data, bm)
def bm_get_indices(sequence):
return [i.index for i in sequence]
def update_roof_modifier_ifc_data(context):
"""should be called after new geometry settled
since it's going to update ifc representation
"""
obj = context.active_object
props = obj.BIMRoofProperties
element = tool.Ifc.get_entity(obj)
# type attributes
element.PredefinedType = props.roof_type
# occurences attributes
# occurences = tool.Ifc.get_all_element_occurences(element)
# TODO: add Qto_RoofBaseQuantities, need to calculate GrossArea, NetArea, ProjectedArea
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/Qto_RoofBaseQuantities.htm
tool.Ifc.edit(obj)
def update_bbim_roof_pset(element, roof_data):
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Roof")
roof_data = json.dumps(roof_data, default=list)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": roof_data})
def update_roof_modifier_bmesh(context):
obj = context.object
props = obj.BIMRoofProperties
if not RoofData.is_loaded:
RoofData.load()
path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# need to make sure we support edit mode
# since users will probably be in edit mode when they'll be changing roof path
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
# generating roof path
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in path_data["edges"]]
if props.is_editing_path:
tool.Blender.apply_bmesh(obj.data, bm)
return
# apply dissolve limit seems to get more correct results with `generate_hipped_roof`
# argument values are the defaults for `bpy.ops.mesh.dissolve_limited`
bmesh.ops.dissolve_limit(
bm, angle_limit=0.0872665, use_dissolve_boundaries=False, delimit={"NORMAL"}, edges=bm.edges[:], verts=bm.verts[:]
)
tool.Blender.apply_bmesh(obj.data, bm)
height = props.height * si_conversion
angle = props.angle * si_conversion
generation_method = props.generation_method
generate_hipped_roof(obj, generation_method, height, angle)
def get_path_data(obj):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if obj.mode == "EDIT":
# otherwise mesh may not contain all changes
# added in edit mode
obj.update_from_editmode()
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
# remove internal edges and faces
# adding missing faces so we could rely on `e.is_boundary` later
bmesh.ops.contextual_create(bm, geom=bm.edges[:])
edges_to_dissolve = [e for e in bm.edges if not e.is_boundary]
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bmesh.ops.delete(bm, geom=bm.faces[:], context="FACES_ONLY")
path_data = dict()
path_data["edges"] = [bm_get_indices(e.verts) for e in bm.edges]
path_data["verts"] = [v.co / si_conversion for v in bm.verts]
if not path_data["edges"] or not path_data["verts"]:
return None
return path_data
class BIM_OT_add_roof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_roof"
bl_label = "Roof"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a roof.")
return {"CANCELLED"}
if context.object is not None:
spawn_location = context.object.location.copy()
context.object.select_set(False)
else:
spawn_location = bpy.context.scene.cursor.location.copy()
mesh = bpy.data.meshes.new("IfcRoof")
obj = bpy.data.objects.new("IfcRoof", mesh)
obj.location = spawn_location
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcRoof",
should_add_representation=True,
context=body_context,
)
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.bim.add_roof()
return {"FINISHED"}
# UI operators
class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_roof"
bl_label = "Add Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRoofProperties
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if element.is_a() not in ("IfcRoof", "IfcRoofType"):
self.report({"ERROR"}, "Object has to be IfcRoof/IfcRoofType type to add a roof.")
return {"CANCELLED"}
# need to make sure all default props will have correct units
if not props.roof_added_previously:
skip_props = ("is_editing", "roof_type", "roof_added_previously", "generation_method")
convert_property_group_from_si(props, skip_props=skip_props)
roof_data = props.get_general_kwargs()
path_data = get_path_data(obj)
if not path_data:
path_data = {
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
"verts": [
Vector([-5.0, -5.0, 0.0]) / si_conversion,
Vector([-5.0, 5.0, 0.0]) / si_conversion,
Vector([5.0, 5.0, 0.0]) / si_conversion,
Vector([5.0, -5.0, 0.0]) / si_conversion,
],
}
roof_data["path_data"] = path_data
update_bbim_roof_pset(element, roof_data)
refresh()
update_roof_modifier_bmesh(context)
update_roof_modifier_ifc_data(context)
return {"FINISHED"}
class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_roof"
bl_label = "Enable Editing Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRoofProperties
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data"))
data["path_data"] = json.dumps(data["path_data"])
# required since we could load pset from .ifc and BIMRoofProperties won't be set
for prop_name in data:
setattr(props, prop_name, data[prop_name])
# need to make sure all props that weren't used before
# will have correct units
skip_props = ("is_editing", "roof_type", "roof_added_previously", "generation_method")
skip_props += tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
return {"FINISHED"}
class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_roof"
bl_label = "Cancel editing Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data"))
props = obj.BIMRoofProperties
# restore previous settings since editing was canceled
for prop_name in data:
setattr(props, prop_name, data[prop_name])
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = -1
return {"FINISHED"}
class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_roof"
bl_label = "Finish editing roof"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRoofProperties
if not RoofData.is_loaded:
RoofData.load()
path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
roof_data = props.get_general_kwargs()
roof_data["path_data"] = path_data
props.is_editing = -1
update_bbim_roof_pset(element, roof_data)
update_roof_modifier_ifc_data(context)
return {"FINISHED"}
class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_roof_path"
bl_label = "Enable Editing Roof Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRoofProperties
props.is_editing_path = True
update_roof_modifier_bmesh(context)
if bpy.context.object.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
ProfileDecorator.install(context)
return {"FINISHED"}
class CancelEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_roof_path"
bl_label = "Cancel Editing Roof Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRoofProperties
ProfileDecorator.uninstall()
props.is_editing_path = False
update_roof_modifier_bmesh(context)
if bpy.context.object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT")
return {"FINISHED"}
class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_roof_path"
bl_label = "Finish Editing Roof Path"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRoofProperties
roof_data = props.get_general_kwargs()
path_data = get_path_data(obj)
roof_data["path_data"] = path_data
ProfileDecorator.uninstall()
props.is_editing_path = False
update_bbim_roof_pset(element, roof_data)
refresh() # RoofData has to be updated before run update_roof_modifier_bmesh
update_roof_modifier_bmesh(context)
if bpy.context.object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT")
update_roof_modifier_ifc_data(context)
return {"FINISHED"}
class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_roof"
bl_label = "Remove Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMRoofProperties
element = tool.Ifc.get_entity(obj)
obj.BIMRoofProperties.is_editing = -1
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
props.roof_added_previously = True
return {"FINISHED"}
def add_object_button(self, context):
self.layout.operator(BIM_OT_add_roof.bl_idname, icon="PLUGIN")
@@ -16,30 +16,21 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import gpu
import blf
import bgl
import bpy
import math
import json
import bmesh
import ifcopenshell
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
import mathutils.geometry
import blenderbim.bim.handler
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from blenderbim.bim.ifc import IfcStore
from math import pi, degrees, sin, cos, radians
from math import radians
from mathutils import Vector, Matrix
from blenderbim.bim.module.geometry.helper import Helper
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
from blenderbim.bim.module.model.decorator import ProfileDecorator
def calculate_quantities(usecase_path, ifc_file, settings):
@@ -122,9 +113,9 @@ class DumbSlabGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
def generate(self, link_to_scene=True):
self.file = IfcStore.get_file()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
def generate(self):
self.file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
thicknesses = []
for rel in self.relating_type.HasAssociations:
if rel.is_a("IfcRelAssociatesMaterial"):
@@ -149,13 +140,13 @@ class DumbSlabGenerator:
self.rotation = 0
self.location = Vector((0, 0, 0))
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
return self.derive_from_cursor(link_to_scene=link_to_scene)
return self.derive_from_cursor()
def derive_from_cursor(self, link_to_scene):
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
return self.create_slab(link_to_scene)
return self.create_slab()
def create_slab(self, link_to_scene):
def create_slab(self):
ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema)
# Standard cases are deprecated, so let's cull them
ifc_class = [c for c in ifc_classes if "StandardCase" not in c][0]
@@ -163,16 +154,15 @@ class DumbSlabGenerator:
mesh = bpy.data.meshes.new("Dummy")
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
if link_to_scene:
matrix_world = Matrix()
matrix_world.col[3] = self.location.to_4d()
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world[2][3] = self.collection_obj.location[2] - self.depth
else:
matrix_world[2][3] -= self.depth
obj.matrix_world = Matrix.Rotation(self.x_angle, 4, "X") @ matrix_world
bpy.context.view_layer.update()
self.collection.objects.link(obj)
matrix_world = Matrix()
matrix_world.col[3] = self.location.to_4d()
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world[2][3] = self.collection_obj.location[2] - self.depth
else:
matrix_world[2][3] -= self.depth
obj.matrix_world = Matrix.Rotation(self.x_angle, 4, "X") @ matrix_world
bpy.context.view_layer.update()
self.collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -269,7 +259,7 @@ class DumbSlabPlaner:
def change_thickness(self, element, thickness):
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
obj = IfcStore.get_element(element.id())
obj = tool.Ifc.get_object(element)
if not obj:
return
@@ -574,7 +564,7 @@ class DisableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects
def _execute(self, context):
DecorationsHandler.uninstall()
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
@@ -621,8 +611,9 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position)
bpy.ops.object.mode_set(mode="EDIT")
DecorationsHandler.install(context)
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
ProfileDecorator.install(context)
if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
return {"FINISHED"}
@@ -633,7 +624,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
DecorationsHandler.uninstall()
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
@@ -657,7 +648,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
DecorationsHandler.install(context)
ProfileDecorator.install(context)
bpy.ops.object.mode_set(mode="EDIT")
return
@@ -749,245 +740,3 @@ class SetArcIndex(bpy.types.Operator):
group.add(selected_vertices, 1, "REPLACE")
bpy.ops.object.mode_set(mode="EDIT")
return {"FINISHED"}
class DecorationsHandler:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def __call__(self, context):
obj = context.active_object
if obj.mode != "EDIT":
return
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
all_vertices = []
error_vertices = []
selected_vertices = []
unselected_vertices = []
special_vertices = []
special_vertex_indices = {}
selected_edges = []
unselected_edges = []
special_edges = [] # edges that have a circle or an arc associated with them
arc_groups = []
circle_groups = []
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
arc_groups.append(i)
elif "IFCCIRCLE" in group.name:
circle_groups.append(i)
arcs = {}
circles = {}
bm = bmesh.from_edit_mesh(obj.data)
# https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess
# This is how we access vertex groups via bmesh, apparently, it's not very intuitive
deform_layer = bm.verts.layers.deform.active
for vertex in bm.verts:
co = tuple(obj.matrix_world @ vertex.co)
all_vertices.append(co)
if vertex.hide:
continue
is_arc = False
for group_index in arc_groups:
if group_index in vertex[deform_layer]:
is_arc = True
break
if is_arc:
arcs.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
is_circle = False
for group_index in circle_groups:
if group_index in vertex[deform_layer]:
is_circle = True
break
if is_circle:
circles.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
if vertex.select:
selected_vertices.append(co)
else:
if len(vertex.link_edges) > 1 and is_circle:
error_vertices.append(co)
elif is_circle:
special_vertices.append(co)
elif len(vertex.link_edges) != 2:
error_vertices.append(co)
elif is_arc:
special_vertices.append(co)
else:
unselected_vertices.append(co)
for edge in bm.edges:
edge_indices = [v.index for v in edge.verts]
if edge.hide:
continue
if edge.select:
selected_edges.append(edge_indices)
else:
i1, i2 = edge.verts[0].index, edge.verts[1].index
if i1 in special_vertex_indices and special_vertex_indices[i1] == special_vertex_indices.get(i2, None):
special_edges.append(edge_indices)
else:
unselected_edges.append(edge_indices)
indices = [[v.index for v in e.verts] for e in bm.edges]
white = (1, 1, 1, 1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
blue = (0.157, 0.565, 1, 1)
grey = (0.2, 0.2, 0.2, 1)
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges)
self.shader.uniform_float("color", green)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=special_edges)
self.shader.uniform_float("color", grey)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": error_vertices})
self.shader.uniform_float("color", red)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": special_vertices})
self.shader.uniform_float("color", blue)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
# Draw arcs
arc_centroids = []
arc_segments = []
for arc in arcs.values():
if len(arc) != 3:
continue
sorted_arc = [None, None, None]
for v1 in arc:
connections = 0
for link_edge in v1.link_edges:
v2 = link_edge.other_vert(v1)
if v2 in arc:
connections += 1
if connections == 2: # Midpoint
sorted_arc[1] = v1
else:
sorted_arc[2 if sorted_arc[2] is None else 0] = v1
points = [tuple(obj.matrix_world @ v.co) for v in sorted_arc]
centroid = tool.Cad.get_center_of_arc(points)
if centroid:
arc_centroids.append(tuple(centroid))
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
batch = batch_for_shader(self.shader, "POINTS", {"pos": arc_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
for verts, edges in arc_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
# Draw circles
circle_centroids = []
circle_segments = []
for circle in circles.values():
if len(circle) != 2:
continue
p1 = obj.matrix_world @ circle[0].co
p2 = obj.matrix_world @ circle[1].co
radius = (p2 - p1).length / 2
centroid = p1.lerp(p2, 0.5)
circle_centroids.append(tuple(centroid))
segments = self.create_circle_segments(360, 20, radius)
matrix = obj.matrix_world.copy()
matrix.col[3] = centroid.to_4d()
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
circle_segments.append(segments)
batch = batch_for_shader(self.shader, "POINTS", {"pos": circle_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
for verts, edges in circle_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
# https://github.com/nortikin/sverchok/blob/master/nodes/generator/basic_3pt_arc.py
# This function is taken from Sverchok, licensed under GPL v2-or-later.
# This is a combination of the make_verts and make_edges function.
def create_circle_segments(self, Angle, Vertices, Radius):
if Angle < 360:
theta = Angle / (Vertices - 1)
else:
theta = Angle / Vertices
listVertX = []
listVertY = []
for i in range(Vertices):
listVertX.append(Radius * cos(radians(theta * i)))
listVertY.append(Radius * sin(radians(theta * i)))
if Angle < 360 and self.mode_ == 0:
sigma = radians(Angle)
listVertX[-1] = Radius * cos(sigma)
listVertY[-1] = Radius * sin(sigma)
elif Angle < 360 and self.mode_ == 1:
listVertX.append(0.0)
listVertY.append(0.0)
points = list((x, y, 0) for x, y in zip(listVertX, listVertY))
listEdg = [(i, i + 1) for i in range(Vertices - 1)]
if Angle < 360 and self.mode_ == 1:
listEdg.append((0, Vertices))
listEdg.append((Vertices - 1, Vertices))
else:
listEdg.append((Vertices - 1, 0))
return points, listEdg
@@ -0,0 +1,168 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bmesh
import shapely
import ifcopenshell
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.core.type
from math import pi
from mathutils import Vector, Matrix
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_options = {"REGISTER"}
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
# layered walls (i.e. prismatic) in the currently active storey (though
# we can easily extend it to include other "bounding" elements). For
# now we generate rooms that exclude walls (i.e. not to wall midpoint
# or exterior / interior edge).
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
props = context.scene.BIMModelProperties
relating_type_id = props.relating_type_id
relating_type = None
if relating_type_id:
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
if not relating_type.is_a("IfcSpaceType"):
relating_type = None
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
active_obj = bpy.context.active_object
element = None
if bpy.context.selected_objects and active_obj:
element = tool.Ifc.get_entity(active_obj)
x, y, z = active_obj.matrix_world.translation.xyz
mat = active_obj.matrix_world
h = active_obj.dimensions.z
else:
x, y = context.scene.cursor.location.xy
z = collection_obj.matrix_world.translation.z
mat = Matrix()
mat[0][3], mat[1][3], mat[2][3] = x, y, z
h = 3
boundary_elements = []
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
if subelement.is_a("IfcWall") and tool.Model.get_usage_type(subelement) == "LAYER2":
boundary_elements.append(subelement)
boundary_lines = []
for boundary_element in boundary_elements:
obj = tool.Ifc.get_object(boundary_element)
if not obj:
continue
axis = self.get_wall_axis(obj)
for ypos in ["miny", "maxy"]:
start, end = axis[ypos]
offset = (end - start).normalized() * 0.3 # Offset wall lines by 300mm
boundary_lines.append(shapely.LineString([start - offset, end + offset]))
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
space_polygon = None
for polygon in closed_polygons.geoms:
if shapely.contains_xy(polygon, x, y):
space_polygon = shapely.force_3d(polygon)
if not space_polygon:
return
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
mat_invert = mat.inverted()
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], z])) for v in space_polygon.exterior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
for interior in space_polygon.interiors:
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], z])) for v in interior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.triangle_fill(bm, edges=bm.edges)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
mesh = bpy.data.meshes.new(name="Space")
bm.to_mesh(mesh)
bm.free()
if element and element.is_a("IfcSpace"):
mesh.name = active_obj.data.name
mesh.BIMMeshProperties.ifc_definition_id = active_obj.data.BIMMeshProperties.ifc_definition_id
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
tool.Ifc.edit(active_obj)
else:
if relating_type:
name = tool.Model.generate_occurrence_name(relating_type, "IfcSpace")
else:
name = "Space"
obj = bpy.data.objects.new("Space", mesh)
obj.matrix_world = mat
collection.objects.link(obj)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
element = tool.Ifc.get_entity(obj)
if relating_type:
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
def get_wall_axis(self, obj):
x_values = [v[0] for v in obj.bound_box]
y_values = [v[1] for v in obj.bound_box]
min_x = min(x_values)
max_x = max(x_values)
min_y = min(y_values)
max_y = max(y_values)
return {
"miny": [
(obj.matrix_world @ Vector((min_x, min_y, 0.0))).to_2d(),
(obj.matrix_world @ Vector((max_x, min_y, 0.0))).to_2d(),
],
"maxy": [
(obj.matrix_world @ Vector((min_x, max_y, 0.0))).to_2d(),
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
],
}
@@ -25,9 +25,9 @@ import bmesh
from bmesh.types import BMVert
import ifcopenshell
from ifcopenshell.util.shape_builder import V, ShapeBuilder
import blenderbim
import blenderbim.tool as tool
from blenderbim.bim.module.model.prop import BIMStairProperties
from blenderbim.bim.helper import convert_property_group_from_si
from mathutils import Vector
@@ -88,8 +88,9 @@ def generate_stair_2d_profile(
height,
width,
tread_run,
tread_depth,
stair_type,
# WOOD/STEEL CONCRETE STAIR ARGUMENTS
tread_depth=None,
# CONCRETE STAIR ARGUMENTS
has_top_nib=None,
top_slab_depth=None,
@@ -104,12 +105,16 @@ def generate_stair_2d_profile(
length = tread_run * number_of_risers
if stair_type == "WOOD/STEEL":
builder = ShapeBuilder(None)
tread_shape = builder.get_rectangle_coords(
size=V(tread_run, 0, tread_depth),
position=V(0, 0, -(tread_depth-tread_rise))
)
tread_offset = V(tread_run, 0, tread_rise)
for i in range(number_of_risers):
v1 = Vector(((i + 1) * tread_run, 0, (i + 1) * tread_rise))
v0 = v1 - Vector((tread_run, 0, 0))
v2 = v1 - Vector((0, 0, tread_depth))
v3 = v0 - Vector((0, 0, tread_depth))
vertices.extend([v0, v1, v2, v3])
cur_trade_shape = [v + tread_offset * i for v in tread_shape]
vertices.extend(cur_trade_shape)
cur_vertex = i * 4
edges.extend([
@@ -118,9 +123,37 @@ def generate_stair_2d_profile(
(cur_vertex+2, cur_vertex+3),
(cur_vertex+3, cur_vertex),
])
faces.append(list(range(4 * i, 4 * i + 1)))
faces.append(list(range(cur_vertex, cur_vertex + 1)))
return (vertices, edges, faces)
elif stair_type == "GENERIC":
vertices.append(Vector([0, 0, 0]))
tread_verts = [
Vector([0, 0, tread_rise]),
Vector([tread_run, 0, tread_rise])
]
tread_offset = Vector([tread_run, 0, tread_rise])
for i in range(number_of_risers):
current_tread_verts = [v + tread_offset * i for v in tread_verts]
last_vert_i = len(vertices) - 1
edges.extend([
(last_vert_i, last_vert_i + 1),
(last_vert_i + 1, last_vert_i + 2)
])
vertices.extend(current_tread_verts)
last_vert_i = len(vertices)
vertices.append(vertices[-1] * V(1,0,0))
edges.extend([
(last_vert_i - 1, last_vert_i),
(last_vert_i, 0)
])
return (vertices, edges, faces)
elif stair_type == "CONCRETE":
for i in range(number_of_risers):
vertices.extend([
@@ -218,11 +251,15 @@ def update_stair_modifier(context):
obj.data.update()
def update_ifc_stair_props(obj, psets):
def update_ifc_stair_props(obj):
"""should be called after new geometry settled
since it's going to update ifc representation
"""
element = tool.Ifc.get_entity(obj)
props = obj.BIMStairProperties
ifc_file = tool.Ifc.get()
element.PredefinedType = "STRAIGHT"
number_of_risers = props.number_of_treads + 1
# update IfcStairFlight properties (seems already deprecated but keep it for now)
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcStairFlight.htm
@@ -233,10 +270,8 @@ def update_ifc_stair_props(obj, psets):
element.TreadLength = props.tread_depth
# update pset with ifc properties
pset_common = psets.get("Pset_StairFlightCommon", None)
if pset_common:
pset_common = ifc_file.by_id(pset_common["id"])
else:
pset_common = tool.Pset.get_element_pset(element, "Pset_StairFlightCommon")
if not pset_common:
pset_common = ifcopenshell.api.run("pset.add_pset", ifc_file, product=element, name="Pset_StairFlightCommon")
ifcopenshell.api.run(
@@ -250,14 +285,15 @@ def update_ifc_stair_props(obj, psets):
"TreadLength": props.tread_depth,
},
)
tool.Ifc.edit(obj)
class BIM_OT_add_clever_stair(Operator):
class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_clever_stair"
bl_label = "Clever Stair"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a stair.")
@@ -316,12 +352,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
)
stair_data = props.get_props_kwargs()
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("BBIM_Stair", None)
if pset:
pset = ifc_file.by_id(pset["id"])
else:
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", ifc_file, product=element, name="BBIM_Stair")
ifcopenshell.api.run(
@@ -331,7 +363,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
properties={"Data": json.dumps(stair_data)},
)
update_stair_modifier(context)
update_ifc_stair_props(obj, psets)
update_ifc_stair_props(obj)
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
@@ -342,8 +374,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
psets = ifcopenshell.util.element.get_psets(element)
data = json.loads(psets["BBIM_Stair"]["Data"])
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Stair", "Data"))
props = obj.BIMStairProperties
# restore previous settings since editing was canceled
for prop_name in data:
@@ -365,21 +396,16 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMStairProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Stair"]
data = props.get_props_kwargs()
props.is_editing = -1
update_stair_modifier(context)
pset = tool.Ifc.get().by_id(pset["id"])
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
data = json.dumps(data)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
# update IfcStairFlight properties
element.PredefinedType = "STRAIGHT"
update_ifc_stair_props(obj, psets)
update_ifc_stair_props(obj)
return {"FINISHED"}
@@ -392,8 +418,7 @@ class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
props = obj.BIMStairProperties
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_psets(element)
data = json.loads(pset["BBIM_Stair"]["Data"])
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Stair", "Data"))
# required since we could load pset from .ifc and BIMStairProperties won't be set
for prop_name in data:
setattr(props, prop_name, data[prop_name])
@@ -419,8 +444,7 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
obj.BIMStairProperties.is_editing = -1
pset = ifcopenshell.util.element.get_psets(element)
pset = tool.Ifc.get().by_id(pset["BBIM_Stair"]["id"])
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
props.stair_added_previously = True
@@ -17,19 +17,11 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty, BoolProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
import bmesh
import ifcopenshell
import blenderbim
import blenderbim.tool as tool
from blenderbim.bim.module.model.prop import BIMStairProperties
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector
from pprint import pprint
import json
import zipfile
import os.path
@@ -65,7 +57,7 @@ def update_sverchok_modifier(context):
bm.to_mesh(obj.data)
bm.free()
obj.data.update()
IfcStore.edited_objs.add(obj)
tool.Ifc.edit(obj)
# UI operators
+133 -85
View File
@@ -19,11 +19,22 @@
import bpy
import blenderbim.tool as tool
from bpy.types import Panel, Operator, Menu
from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData, WindowData, DoorData
from blenderbim.bim.module.model.prop import store_cursor_position
from blenderbim.bim.module.model.data import (
AuthoringData,
ArrayData,
StairData,
SverchokData,
WindowData,
DoorData,
RailingData,
RoofData,
)
from blenderbim.bim.module.model.prop import get_ifc_class
from blenderbim.bim.module.model.stair import update_stair_modifier
from blenderbim.bim.module.model.window import update_window_modifier_bmesh
from blenderbim.bim.module.model.door import update_door_modifier_bmesh
from blenderbim.bim.module.model.railing import update_railing_modifier_bmesh
from blenderbim.bim.module.model.roof import update_roof_modifier_bmesh
from blenderbim.bim.helper import prop_with_search
@@ -40,11 +51,9 @@ class LaunchTypeManager(bpy.types.Operator):
def invoke(self, context, event):
props = context.scene.BIMModelProperties
props.type_page = 1
if props.ifc_class:
if get_ifc_class(None, context):
props.type_class = props.ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class, offset=0, limit=9)
if not AuthoringData.is_loaded:
AuthoringData.load()
return context.window_manager.invoke_popup(self, width=550)
def draw(self, context):
@@ -63,6 +72,7 @@ class LaunchTypeManager(bpy.types.Operator):
row.alignment = "CENTER"
row.prop(props, "type_predefined_type", text="")
row.prop(props, "type_template", text="")
row.prop(props, "type_name", text="")
row.operator("bim.add_type", icon="ADD", text="")
row = columns.row(align=True)
@@ -127,86 +137,6 @@ class BIM_PT_authoring(Panel):
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
class DisplayConstrTypesUI(Operator):
bl_idname = "bim.display_constr_types_ui"
bl_label = "Browse Construction Types"
bl_options = {"REGISTER"}
bl_description = "Display all available Construction Types to add new instances"
reinvoked: bpy.props.BoolProperty(default=False)
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
store_cursor_position(context, event, window=not self.reinvoked)
return context.window_manager.invoke_popup(self, width=550)
def reinvoke(self, context):
browser_state = context.scene.BIMModelProperties.constr_browser_state
def set_updating_transaction():
browser_state.updating = True
def run_operator():
bpy.ops.bim.reinvoke_operator("INVOKE_DEFAULT", operator="bim.display_constr_types_ui")
browser_state.updating = False
if not browser_state.updating:
bpy.app.timers.register(set_updating_transaction)
bpy.app.timers.register(run_operator, first_interval=browser_state.update_delay)
def draw(self, context):
props = context.scene.BIMModelProperties
if AuthoringData.data["ifc_classes"]:
row = self.layout.row()
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, props, "ifc_class_browser", text="")
ifc_class = props.ifc_class_browser
num_cols = 3
self.layout.row().separator(factor=0.25)
flow = self.layout.grid_flow(row_major=True, columns=num_cols, even_columns=True, even_rows=True, align=True)
relating_types = AuthoringData.relating_types_browser()
num_types = len(relating_types)
for idx, (relating_type_id, name, desc) in enumerate(relating_types):
outer_col = flow.column()
box = outer_col.box()
row = box.row()
row.label(text=name, icon="FILE_3D")
row.alignment = "CENTER"
row = box.row()
if ifc_class in props.constr_classes:
constr_class_info = props.constr_classes[ifc_class]
constr_types_info = constr_class_info.constr_types
if relating_type_id in constr_types_info:
icon_id = constr_types_info[relating_type_id].icon_id
row.template_icon(icon_value=icon_id, scale=6.0)
else:
self.reinvoke(context)
return
row = box.row()
op = row.operator("bim.add_constr_type_instance", icon="ADD")
op.from_invoke = True
op.ifc_class = ifc_class
if relating_type_id.isnumeric():
op.relating_type_id = int(relating_type_id)
last_row_cols = num_types % num_cols
if last_row_cols != 0:
for _ in range(num_cols - last_row_cols):
flow.column()
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.help_relating_types", text="", icon="QUESTION")
class HelpConstrTypes(Operator):
bl_idname = "bim.help_relating_types"
bl_label = "Construction Types Help"
@@ -611,6 +541,124 @@ class BIM_PT_door(bpy.types.Panel):
row.operator("bim.add_door", icon="ADD", text="")
class BIM_PT_railing(bpy.types.Panel):
bl_label = "IFC Railing"
bl_idname = "BIM_PT_railing"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "modifier"
bl_options = {"DEFAULT_CLOSED"}
@classmethod
def poll(cls, context):
# always display modifier if it's IFC object
return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object)
def draw(self, context):
if not RailingData.is_loaded:
RailingData.load()
props = context.active_object.BIMRailingProperties
if RailingData.data["parameters"]:
row = self.layout.row(align=True)
row.label(text="Railing parameters", icon="OUTLINER_OB_LATTICE")
railing_data = RailingData.data["parameters"]["data_dict"]
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish editing")
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
update_railing_modifier_bmesh(context)
elif props.is_editing_path:
row.operator("bim.finish_editing_railing_path", icon="CHECKMARK", text="")
row.operator("bim.cancel_editing_railing_path", icon="CANCEL", text="")
else:
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
row.operator("bim.remove_railing", icon="X", text="")
box = self.layout.box()
general_props = props.get_general_kwargs()
for prop in general_props:
prop_value = railing_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=str(prop_value))
else:
row = self.layout.row()
row.label(text="No Railing Found")
row.operator("bim.add_railing", icon="ADD", text="")
class BIM_PT_roof(bpy.types.Panel):
bl_label = "IFC Roof"
bl_idname = "BIM_PT_roof"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "modifier"
bl_options = {"DEFAULT_CLOSED"}
@classmethod
def poll(cls, context):
# always display modifier if it's IFC object
return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object)
def draw(self, context):
if not RoofData.is_loaded:
RoofData.load()
props = context.active_object.BIMRoofProperties
if RoofData.data["parameters"]:
row = self.layout.row(align=True)
row.label(text="Roof parameters", icon="OUTLINER_OB_LATTICE")
roof_data = RoofData.data["parameters"]["data_dict"]
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish editing")
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
update_roof_modifier_bmesh(context)
elif props.is_editing_path:
row.operator("bim.finish_editing_roof_path", icon="CHECKMARK", text="")
row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="")
else:
row.operator("bim.enable_editing_roof", icon="GREASEPENCIL", text="")
row.operator("bim.enable_editing_roof_path", icon="ANIM", text="")
row.operator("bim.remove_roof", icon="X", text="")
box = self.layout.box()
general_props = props.get_general_kwargs()
for prop in general_props:
prop_value = roof_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=str(prop_value))
else:
row = self.layout.row()
row.label(text="No Roof Found")
row.operator("bim.add_roof", icon="ADD", text="")
class BIM_MT_model(Menu):
bl_idname = "BIM_MT_model"
bl_label = "IFC Objects"
@@ -34,6 +34,7 @@ import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from math import pi, sin, cos, degrees, radians
from mathutils import Vector, Matrix
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
def element_listener(element, obj):
@@ -54,11 +55,10 @@ def mode_callback(obj, data):
if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2":
return
if obj.mode == "EDIT":
IfcStore.edited_objs.add(obj)
tool.Ifc.edit(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
bm.free()
else:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
obj.data.transform(
@@ -108,12 +108,18 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
if self.join_type == "T":
elements = [tool.Ifc.get_entity(o) for o in context.selected_objects]
targets = [e for e in elements if e and e.is_a() in ("IfcSlab", "IfcSlabStandardCase", "IfcRoof")]
if targets:
target = tool.Ifc.get_object(targets[0])
for element in elements:
if element.is_a("IfcWall"):
joiner.join_Z(tool.Ifc.get_object(element), target)
layer2_elements = []
layer3_elements = []
for element in elements:
usage = tool.Model.get_usage_type(element)
if usage == "LAYER2":
layer2_elements.append(element)
elif usage == "LAYER3":
layer3_elements.append(element)
if layer3_elements:
target = tool.Ifc.get_object(layer3_elements[0])
for element in layer2_elements:
joiner.join_Z(tool.Ifc.get_object(element), target)
else:
for obj in selected_objs:
if obj == context.active_object:
@@ -149,7 +155,7 @@ class AlignWall(bpy.types.Operator):
aligner.align_first_layer()
elif self.align_type == "INTERIOR":
aligner.align_last_layer()
IfcStore.edited_objs.add(obj)
tool.Ifc.edit(obj)
return {"FINISHED"}
@@ -193,6 +199,7 @@ class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_wall"
bl_label = "Merge Wall"
bl_description = "Merge selected walls into one object"
bl_options = {"REGISTER", "UNDO"}
@classmethod
@@ -231,7 +238,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects
def _execute(self, context):
wall_objs = []
layer2_objs = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -245,7 +252,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
extrusion.Depth = self.depth * (1 / cos(x_angle))
if element.is_a("IfcWall"):
if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
ifcopenshell.api.run(
@@ -254,9 +261,9 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
relating_element=rel.RelatingElement,
related_element=element,
)
wall_objs.append(obj)
if wall_objs:
DumbWallRecalculator().recalculate(wall_objs)
layer2_objs.append(obj)
if layer2_objs:
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
@@ -271,7 +278,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects
def _execute(self, context):
wall_objs = []
layer2_objs = []
other_objs = []
x_angle = radians(self.x_angle)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -290,8 +297,8 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
perpendicular_depth = extrusion.Depth / (1 / cos(existing_x_angle))
extrusion.Depth = perpendicular_depth * (1 / cos(x_angle))
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
if element.is_a("IfcWall"):
wall_objs.append(obj)
if tool.Model.get_usage_type(element) == "LAYER2":
layer2_objs.append(obj)
else:
blenderbim.core.geometry.switch_representation(
tool.Ifc,
@@ -308,8 +315,8 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
new_matrix = euler.to_matrix().to_4x4()
new_matrix.col[3] = obj.matrix_world.col[3]
obj.matrix_world = new_matrix
if wall_objs:
DumbWallRecalculator().recalculate(wall_objs)
if layer2_objs:
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
@@ -345,28 +352,6 @@ def recalculate_dumb_wall_origin(wall, new_origin=None):
child.matrix_parent_inverse = wall.matrix_world.inverted()
class DumbWallFlipper:
def __init__(self, wall):
self.wall = wall
def flip(self):
if (
self.wall.matrix_world.translation - self.wall.matrix_world @ Vector(self.wall.bound_box[0])
).length < 0.001:
recalculate_dumb_wall_origin(self.wall, self.wall.matrix_world @ Vector(self.wall.bound_box[7]))
self.rotate_wall_180()
bpy.context.view_layer.update()
for child in self.wall.children:
child.matrix_parent_inverse = self.wall.matrix_world.inverted()
else:
recalculate_dumb_wall_origin(self.wall)
def rotate_wall_180(self):
flip_matrix = Matrix.Rotation(pi, 4, "Z")
self.wall.data.transform(flip_matrix)
self.wall.rotation_euler.rotate(flip_matrix)
class DumbWallAligner:
# An alignment shifts the origin of all walls to the closest point on the
# local X axis of the reference wall. In addition, the Z rotation is copied.
@@ -376,61 +361,58 @@ class DumbWallAligner:
self.reference_wall = reference_wall
def align_centerline(self):
recalculate_dumb_wall_origin(self.wall)
recalculate_dumb_wall_origin(self.reference_wall)
self.align_rotation()
width = (Vector(self.wall.bound_box[3]) - Vector(self.wall.bound_box[0])).y
reference_width = (Vector(self.reference_wall.bound_box[3]) - Vector(self.reference_wall.bound_box[0])).y
l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5)
l_end = Vector(self.reference_wall.bound_box[4]).lerp(Vector(self.reference_wall.bound_box[7]), 0.5)
if self.is_rotation_flipped():
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, -(reference_width / 2) - (width / 2), 0))
else:
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, (reference_width / 2) - (width / 2), 0))
start = self.reference_wall.matrix_world @ l_start
end = self.reference_wall.matrix_world @ l_end
self.align(
self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[0]),
self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[4]),
offset,
)
l_snap_point = Vector(self.wall.bound_box[0]).lerp(Vector(self.wall.bound_box[3]), 0.5)
snap_point = self.wall.matrix_world @ l_snap_point
offset = snap_point - self.wall.matrix_world.translation
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_last_layer(self):
recalculate_dumb_wall_origin(self.wall)
recalculate_dumb_wall_origin(self.reference_wall)
self.align_rotation()
if self.is_rotation_flipped():
DumbWallFlipper(self.wall).flip()
DumbWallJoiner().flip(self.wall)
bpy.context.view_layer.update()
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[3])
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[7])
wall_width = (Vector(self.wall.bound_box[3]) - Vector(self.wall.bound_box[0])).y
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[3])
offset = snap_point - self.wall.matrix_world.translation
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, -wall_width, 0))
self.align(start, end, offset)
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_first_layer(self):
recalculate_dumb_wall_origin(self.wall)
recalculate_dumb_wall_origin(self.reference_wall)
self.align_rotation()
if self.is_rotation_flipped():
DumbWallFlipper(self.wall).flip()
DumbWallJoiner().flip(self.wall)
bpy.context.view_layer.update()
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[0])
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[4])
self.align(start, end)
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
offset = snap_point - self.wall.matrix_world.translation
def align(self, start, end, offset=None):
if offset is None:
offset = Vector((0, 0, 0))
point, distance = mathutils.geometry.intersect_point_line(self.wall.matrix_world.translation, start, end)
new_origin = point + offset
self.wall.matrix_world.translation[0] = new_origin[0]
self.wall.matrix_world.translation[1] = new_origin[1]
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_rotation(self):
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
@@ -463,7 +445,7 @@ class DumbWallRecalculator:
queue.add((rel.RelatingElement, tool.Ifc.get_object(rel.RelatingElement)))
joiner = DumbWallJoiner()
for element, wall in queue:
if element.is_a("IfcWall") and wall:
if tool.Model.get_usage_type(element) == "LAYER2" and wall:
joiner.recreate_wall(element, wall)
@@ -472,7 +454,7 @@ class DumbWallGenerator:
self.relating_type = relating_type
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self, link_to_scene=True):
def generate(self):
self.file = IfcStore.get_file()
self.layers = tool.Model.get_material_layer_parameters(self.relating_type)
if not self.layers["thickness"]:
@@ -491,10 +473,7 @@ class DumbWallGenerator:
self.location = Vector((0, 0, 0))
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
if self.has_sketch():
return # For now
return self.derive_from_sketch()
return self.derive_from_cursor(link_to_scene)
return self.derive_from_cursor()
def has_sketch(self):
return (
@@ -573,7 +552,7 @@ class DumbWallGenerator:
def is_near(self, point1, point2):
return (point1 - point2).length < 0.1
def derive_from_cursor(self, link_to_scene):
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
if self.collection:
for sibling_obj in self.collection.objects:
@@ -599,21 +578,21 @@ class DumbWallGenerator:
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
self.rotation = math.atan2(normal[1], normal[0])
break
return self.create_wall(link_to_scene)
return self.create_wall()
def create_wall(self, link_to_scene):
def create_wall(self):
props = bpy.context.scene.BIMModelProperties
ifc_class = self.get_relating_type_class(self.relating_type)
mesh = bpy.data.meshes.new("Dummy")
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
if link_to_scene:
matrix_world = Matrix.Rotation(self.rotation, 4, "Z")
matrix_world.col[3] = self.location.to_4d()
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world[2][3] = self.collection_obj.location[2] + (props.rl1 * self.unit_scale)
obj.matrix_world = matrix_world
bpy.context.view_layer.update()
self.collection.objects.link(obj)
matrix_world = Matrix.Rotation(self.rotation, 4, "Z")
matrix_world.col[3] = self.location.to_4d()
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world[2][3] = self.collection_obj.location[2] + (props.rl1 * self.unit_scale)
obj.matrix_world = matrix_world
bpy.context.view_layer.update()
self.collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -781,10 +760,10 @@ class DumbWallJoiner:
return
axis1 = tool.Model.get_wall_axis(wall1)
axis2 = copy.deepcopy(axis1)
intersect, connection = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
if connection < 0 or connection > 1 or tool.Cad.is_x(connection, (0, 1)):
intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)):
return
connection = "ATEND" if connection > 0.5 else "ATSTART"
connection = "ATEND" if cut_percentage > 0.5 else "ATSTART"
wall2 = self.duplicate_wall(wall1)
element2 = tool.Ifc.get_entity(wall2)
@@ -792,6 +771,40 @@ class DumbWallJoiner:
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element2, connection_type="ATSTART")
# During the duplication process, unfilled voids are copied, so we need
# to check openings on both element1 and element2. Let's check element1
# first.
for opening in [
r.RelatedOpeningElement for r in element1.HasOpenings if not r.RelatedOpeningElement.HasFillings
]:
opening_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement).tolist())
opening_location = opening_matrix.col[3].to_3d()
_, opening_position = mathutils.geometry.intersect_point_line(opening_location.to_2d(), *axis1["reference"])
if opening_position > cut_percentage:
# The opening should be removed from element1.
ifcopenshell.api.run("void.remove_opening", tool.Ifc.get(), opening=opening)
# Now let's check element2.
for opening in [
r.RelatedOpeningElement for r in element2.HasOpenings if not r.RelatedOpeningElement.HasFillings
]:
opening_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement).tolist())
opening_location = opening_matrix.col[3].to_3d()
_, opening_position = mathutils.geometry.intersect_point_line(opening_location.to_2d(), *axis1["reference"])
if opening_position < cut_percentage:
# The opening should be removed from element2.
ifcopenshell.api.run("void.remove_opening", tool.Ifc.get(), opening=opening)
# During the duplication process, filled voids are not copied. So we
# only need to check fillings on the original element1.
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings if r.RelatedOpeningElement.HasFillings]:
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
filling_location = filling_obj.matrix_world.translation
_, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis1["reference"])
if filling_position > cut_percentage:
# The filling should be moved from element1 to element2.
FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation)
axis1["reference"][1] = intersect
axis2["reference"][0] = intersect
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
@@ -952,6 +965,15 @@ class DumbWallJoiner:
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
for rel in element1.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
ifcopenshell.api.run(
"geometry.disconnect_element",
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element1,
)
ifcopenshell.api.run(
"geometry.connect_element",
tool.Ifc.get(),
@@ -1166,10 +1188,13 @@ class DumbWallJoiner:
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_body
)
if tool.Ifc.is_moved(obj):
wall_moved = tool.Ifc.is_moved(obj)
if wall_moved:
# Openings should move with the host overall ...
# ... except their position should stay the same along the local X axis of the wall
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
for opening in [
r.RelatedOpeningElement for r in element.HasOpenings if not r.RelatedOpeningElement.HasFillings
]:
percent = tool.Cad.edge_percent(
self.body[0], (previous_origin, (previous_matrix @ Vector((1, 0, 0))).to_2d())
)
@@ -1182,8 +1207,21 @@ class DumbWallJoiner:
else:
coordinates[0] -= change_in_x
opening.ObjectPlacement.RelativePlacement.Location.Coordinates = coordinates
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
# If opening has filling then stick to the filling's position
# We're applying new openings position only after wall position is applied
for opening in [r.RelatedOpeningElement for r in element.HasOpenings if r.RelatedOpeningElement.HasFillings]:
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
filling_moved = tool.Ifc.is_moved(filling_obj)
if filling_moved:
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=filling_obj)
if filling_moved or wall_moved:
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=filling_obj.matrix_world
)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -32,23 +32,13 @@ from bpy.props import FloatProperty, IntProperty, BoolProperty
from bmesh.types import BMVert
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.helper import replace_ifc_representation_for_object
from mathutils import Vector
# TODO: move to some utils helpers/tool module
def update_simple_openings(element, opening_width, opening_height):
element_type = None
ifc_file = tool.Ifc.get()
if element.is_a("IfcElementType"):
element_type = element
fillings = ifcopenshell.util.element.get_types(element_type)
else:
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
fillings = ifcopenshell.util.element.get_types(element_type)
else:
fillings = [element]
fillings = tool.Ifc.get_all_element_occurences(element)
voided_objs = set()
has_replaced_opening_representation = False
@@ -148,13 +138,13 @@ def update_window_modifier_representation(context):
elevation_representation = ifcopenshell.api.run(
"geometry.add_window_representation", ifc_file, **representation_data
)
replace_ifc_representation_for_object(ifc_file, profile, obj, elevation_representation)
tool.Model.replace_object_ifc_representation(profile, obj, elevation_representation)
# MODEL_VIEW representation
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
representation_data["context"] = body
model_representation = ifcopenshell.api.run("geometry.add_window_representation", ifc_file, **representation_data)
replace_ifc_representation_for_object(ifc_file, body, obj, model_representation)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# PLAN_VIEW representation
plan = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
@@ -163,7 +153,7 @@ def update_window_modifier_representation(context):
plan_representation = ifcopenshell.api.run(
"geometry.add_window_representation", ifc_file, **representation_data
)
replace_ifc_representation_for_object(ifc_file, plan, obj, plan_representation)
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
# adding switch representation at the end instead of changing order of representations
# to prevent #2744
@@ -177,8 +167,15 @@ def update_window_modifier_representation(context):
should_sync_changes_first=True,
)
# type attributes
element.PartitioningType = props.window_type
# occurences attributes
occurences = tool.Ifc.get_all_element_occurences(element)
for occurence in occurences:
occurence.OverallWidth = props.overall_width
occurence.OverallHeight = props.overall_height
update_simple_openings(element, props.overall_width, props.overall_height)
@@ -303,7 +300,7 @@ def update_window_modifier_bmesh(context):
lining_to_panel_offset_x = props.lining_to_panel_offset_x * si_conversion
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion
lining_thickness = props.lining_thickness * si_conversion
lining_offset = props.lining_offset
lining_offset = props.lining_offset * si_conversion
mullion_thickness = props.mullion_thickness * si_conversion / 2
first_mullion_offset = props.first_mullion_offset * si_conversion
@@ -409,12 +406,12 @@ def update_window_modifier_bmesh(context):
obj.data.update()
class BIM_OT_add_window(Operator):
class BIM_OT_add_window(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_window"
bl_label = "Window"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a window.")
@@ -429,7 +426,6 @@ class BIM_OT_add_window(Operator):
mesh = bpy.data.meshes.new("IfcWindow")
obj = bpy.data.objects.new("IfcWindow", mesh)
obj.location = spawn_location
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcWindow", should_add_representation=False
)
@@ -468,12 +464,8 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
window_data["lining_properties"] = lining_props
window_data["panel_properties"] = panel_props
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("BBIM_Window", None)
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Window")
ifcopenshell.api.run(
@@ -494,8 +486,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
psets = ifcopenshell.util.element.get_psets(element)
data = json.loads(psets["BBIM_Window"]["Data"])
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Window", "Data"))
props = obj.BIMWindowProperties
for prop_name in data:
setattr(props, prop_name, data[prop_name])
@@ -525,8 +516,6 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Window"]
window_data = props.get_general_kwargs()
lining_props = props.get_lining_kwargs()
panel_props = props.get_panel_kwargs()
@@ -538,7 +527,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
update_window_modifier_representation(context)
pset = tool.Ifc.get().by_id(pset["id"])
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
window_data = json.dumps(window_data, default=list)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": window_data})
return {"FINISHED"}
@@ -553,8 +542,7 @@ class EnableEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
props = obj.BIMWindowProperties
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_psets(element)
data = json.loads(pset["BBIM_Window"]["Data"])
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Window", "Data"))
data.update(data.pop("lining_properties"))
data.update(data.pop("panel_properties"))
@@ -583,8 +571,7 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
obj.BIMWindowProperties.is_editing = -1
pset = ifcopenshell.util.element.get_psets(element)
pset = tool.Ifc.get().by_id(pset["BBIM_Window"]["id"])
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
props.window_added_previously = True
@@ -24,8 +24,9 @@ import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
from blenderbim.bim.helper import prop_with_search, close_operator_panel
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model import prop
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.module.model.prop import get_ifc_class
class BimTool(WorkSpaceTool):
@@ -42,13 +43,23 @@ class BimTool(WorkSpaceTool):
# ("bim.wall_tool_op", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
# ("bim.sync_modeling", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
# replicate default blender selection behaviour with click and box selection
# Replicate default blender selection behaviour with click and box selection
# code below comes from blender_default.py which is part of default blender scripts licensed under GPL v2
# https://github.com/blender/blender/blob/master/release/scripts/presets/keyconfig/keymap_data/blender_default.py
# the code is the data from evaluating km_3d_view_tool_select() and km_3d_view_tool_select_box()
# TODO: call blender_default.py functions instead of copying?
# from evaluating km_3d_view_tool_select()
# https://github.com/blender/blender/blob/4815d0706fb57d6e4f897dbb4e9aba9d2323cdce/release/scripts/presets/keyconfig/keymap_data/blender_default.py#L6710
#
# You can run the snippet below in Blender console
# to regenerate those keybindings in case of errors in the future
# ```
# import os
# version = ".".join(bpy.app.version_string.split(".")[:2])
# fl = os.path.join(os.getcwd(), version, "scripts/presets/keyconfig/keymap_data/blender_default.py")
# def_keymap = bpy.utils.execfile(fl)
# params = def_keymap.Params
# box_keymap = def_keymap.km_3d_view_tool_select_box(def_keymap.Params(), fallback=None)[2]["items"]
# click_keymap = def_keymap.km_3d_view_tool_select(def_keymap.Params(select_mouse="LEFTMOUSE"), fallback=None)[2]["items"]
# ```
# box selection keymap
("view3d.select_box", {"type": "LEFTMOUSE", "value": "CLICK_DRAG"}, None),
(
"view3d.select_box",
@@ -65,32 +76,26 @@ class BimTool(WorkSpaceTool):
{"type": "LEFTMOUSE", "value": "CLICK_DRAG", "shift": True, "ctrl": True},
{"properties": [("mode", "AND")]},
),
# from evaluating km_3d_view_tool_select()
# https://github.com/blender/blender/blob/4815d0706fb57d6e4f897dbb4e9aba9d2323cdce/release/scripts/presets/keyconfig/keymap_data/blender_default.py#L6734
("view3d.select", {"type": "LEFTMOUSE", "value": "CLICK"}, {"properties": [("deselect_all", True)]}),
("view3d.select", {"type": "LEFTMOUSE", "value": "CLICK", "shift": True}, {"properties": [("toggle", True)]}),
("view3d.select", {"type": "LEFTMOUSE", "value": "CLICK", "alt": True}, {"properties": [("enumerate", True)]}),
(
"view3d.select",
{"type": "LEFTMOUSE", "value": "CLICK", "shift": True, "alt": True},
{"properties": [("toggle", True), ("enumerate", True)]},
),
# left-click selection keymap
("view3d.select", {"type": "LEFTMOUSE", "value": "PRESS"}, {"properties": [("deselect_all", True)]}),
("view3d.select", {"type": "LEFTMOUSE", "value": "PRESS", "shift": True}, {"properties": [("toggle", True)]}),
("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
("bim.hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
("bim.hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
("bim.hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
("bim.hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
("bim.hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
("bim.hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
("bim.hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
)
def draw_settings(context, layout, ws_tool):
@@ -98,6 +103,13 @@ class BimTool(WorkSpaceTool):
BimToolUI.draw(context, layout)
def add_layout_hotkey_operator(layout, text, hotkey, description):
op = layout.operator("bim.hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op
class BimToolUI:
@classmethod
def draw(cls, context, layout):
@@ -112,11 +124,6 @@ class BimToolUI:
if not AuthoringData.is_loaded:
AuthoringData.load()
# if ifc_classes and relating_types_ids and not cls.props.icon_id and not is_tool_header:
# # hack Dion won't like to show a preview also on the first time the sidebar is shown
# bpy.app.timers.register(lambda: prop.update_ifc_class(cls.props, "lost_context"))
# bpy.app.timers.register(lambda: prop.update_relating_type(cls.props, "lost_context"))
if context.region.type == "TOOL_HEADER":
cls.draw_header_interface()
elif context.region.type in ("UI", "WINDOW"):
@@ -160,6 +167,11 @@ class BimToolUI:
elif cls.props.ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcDoorType", "IfcDoorStyle"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl2", text="RL")
elif cls.props.ifc_class in ("IfcSpaceType"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
@classmethod
def draw_edit_object_interface(cls, context):
@@ -194,19 +206,15 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_M")
row.operator("bim.hotkey", text="Merge").hotkey = "S_M"
add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
op = row.operator("bim.hotkey", text="Flip")
op.hotkey = "S_F"
op.bim_operator = "flip_wall"
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_K")
op = row.operator("bim.hotkey", text="Split")
op.hotkey = "S_K"
op.bim_operator = "split_wall"
add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
@@ -214,9 +222,11 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.hotkey", text="Regen").hotkey = "S_G"
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
row.operator("bim.join_wall", icon="X", text="").join_type = ""
elif AuthoringData.data["active_material_usage"] == "LAYER3":
# unnecessary check because BIM Tool is not available in EDIT mode?
if context.active_object.mode == "OBJECT":
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -246,6 +256,10 @@ class BimToolUI:
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Axis").hotkey = "A_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
@@ -260,8 +274,9 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.hotkey", text="Regen").hotkey = "S_G"
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif AuthoringData.data["active_class"] in (
"IfcWindow",
"IfcWindowStandardCase",
@@ -278,12 +293,18 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.hotkey", text="Regen").hotkey = "S_G"
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
row.operator("bim.hotkey", text="Flip").hotkey = "S_F"
elif AuthoringData.data["active_class"] in ("IfcSpace",):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
elif AuthoringData.data["active_class"] in (
"IfcCableCarrierSegmentType",
"IfcCableSegmentType",
@@ -294,6 +315,32 @@ class BimToolUI:
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Extend", icon="EVENT_E")
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not context.active_object.BIMRailingProperties.is_editing_path
):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Railing Path").hotkey = "S_E"
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and not context.active_object.BIMRoofProperties.is_editing_path
):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Roof Path").hotkey = "S_E"
elif DecoratorData.get_ifc_text_data(bpy.context.object):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit text").hotkey = "S_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
@@ -301,6 +348,7 @@ class BimToolUI:
row.operator("bim.add_opening", text="Apply Void")
else:
row.operator("bim.add_potential_opening", text="Add Void")
if AuthoringData.data["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]:
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
@@ -320,13 +368,15 @@ class BimToolUI:
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Interior", icon="EVENT_V")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Mirror", icon="EVENT_M")
row = cls.layout.row(align=True)
row.label(text="Mode")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_O")
row.operator("bim.hotkey", text="Void").hotkey = "A_O"
add_layout_hotkey_operator(row, "Void", "A_O", "Show / edit openings")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_D")
@@ -368,12 +418,13 @@ class BimToolUI:
def draw_basic_bim_tool_interface(cls):
cls.draw_type_selection_interface()
box = cls.layout.box()
if AuthoringData.data["type_thumbnail"]:
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
else:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class
if cls.props.ifc_class:
box = cls.layout.box()
if AuthoringData.data["type_thumbnail"]:
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
else:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class
if AuthoringData.data["ifc_classes"]:
row = cls.layout.row(align=True)
@@ -391,7 +442,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
bim_operator: bpy.props.StringProperty()
description: bpy.props.StringProperty()
x: bpy.props.FloatProperty(name="X", default=0.5)
y: bpy.props.FloatProperty(name="Y", default=0.5)
z: bpy.props.FloatProperty(name="Z", default=0.5)
@@ -402,7 +453,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def description(cls, context, operator):
return "" if not operator.bim_operator else getattr(bpy.ops.bim, operator.bim_operator).__doc__
return operator.description or ""
def _execute(self, context):
self.props = context.scene.BIMModelProperties
@@ -413,20 +464,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(context.active_object)
if element:
self.active_class = element.is_a()
self.active_material_usage = tool.Model.get_usage_type(element)
material = ifcopenshell.util.element.get_material(element, should_inherit=False)
if material:
if material.is_a("IfcMaterialLayerSetUsage"):
self.active_material_usage = f"LAYER{material.LayerSetDirection[-1]}"
elif material.is_a("IfcMaterialProfileSetUsage"):
self.active_material_usage = "PROFILE"
try:
self.has_ifc_class = bool(self.props.ifc_class)
except:
pass
if get_ifc_class(None, None):
try:
self.has_ifc_class = bool(self.props.ifc_class)
except:
pass
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
@@ -437,7 +482,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def draw(self, context):
props = context.scene.BIMModelProperties
if self.hotkey == "S_O":
row = self.layout.row()
row.prop(self, "x")
@@ -450,9 +494,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.add_constr_type_instance()
def hotkey_S_Q(self):
if not bpy.context.selected_objects:
return
bpy.ops.bim.calculate_all_quantities()
def hotkey_S_C(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.align_wall(align_type="CENTERLINE")
else:
@@ -461,35 +510,110 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
def hotkey_S_E(self):
if not bpy.context.selected_objects:
return
elif self.active_material_usage == "LAYER2":
bpy.ops.bim.join_wall(join_type="T")
elif self.active_material_usage == "LAYER3":
if not bpy.context.active_object:
pass
elif bpy.context.active_object.mode == "OBJECT":
bpy.ops.bim.enable_editing_extrusion_profile()
elif self.active_material_usage == "PROFILE":
selected_usages = {}
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
obj.select_set(False)
continue
usage = tool.Model.get_usage_type(element)
if not usage:
obj.select_set(False)
continue
selected_usages.setdefault(usage, []).append(obj)
if len(bpy.context.selected_objects) == 1:
if self.active_material_usage == "LAYER3":
# Edit LAYER2 profile
if bpy.context.active_object and bpy.context.active_object.mode == "OBJECT":
bpy.ops.bim.enable_editing_extrusion_profile()
elif self.active_material_usage == "LAYER2":
# Extend LAYER2 to cursor
bpy.ops.bim.join_wall(join_type="T")
elif self.active_material_usage == "PROFILE":
# Extend PROFILE to cursor
bpy.ops.bim.extend_profile(join_type="T")
elif self.active_material_usage == "LAYER2" and selected_usages.get("PROFILE", []):
# Extend PROFILEs to LAYER2
[o.select_set(False) for o in selected_usages.get("LAYER3", [])]
[o.select_set(False) for o in selected_usages.get("LAYER2", []) if o != bpy.context.active_object]
bpy.ops.bim.extend_profile(join_type="T")
elif self.active_material_usage == "LAYER3" and selected_usages.get("LAYER2", []):
# Extend LAYER2s to LAYER3
[o.select_set(False) for o in selected_usages.get("PROFILE", [])]
[o.select_set(False) for o in selected_usages.get("LAYER3", []) if o != bpy.context.active_object]
bpy.ops.bim.join_wall(join_type="T")
elif self.active_material_usage == "LAYER2":
# Extend LAYER2s to LAYER2
[o.select_set(False) for o in selected_usages.get("LAYER3", [])]
[o.select_set(False) for o in selected_usages.get("PROFILE", [])]
bpy.ops.bim.join_wall(join_type="T")
elif self.active_material_usage == "PROFILE":
# Extend PROFILEs to PROFILE
[o.select_set(False) for o in selected_usages.get("LAYER3", [])]
[o.select_set(False) for o in selected_usages.get("LAYER2", [])]
bpy.ops.bim.extend_profile(join_type="T")
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
):
# undo the unselection done above because railing has no usage type 🙃
bpy.context.object.select_set(True)
bpy.ops.bim.enable_editing_railing_path()
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and not bpy.context.active_object.BIMRoofProperties.is_editing_path
):
# undo the unselection done above because roof has no usage type
bpy.context.object.select_set(True)
bpy.ops.bim.enable_editing_roof_path()
elif DecoratorData.get_ifc_text_data(bpy.context.object):
bpy.context.object.select_set(True)
bpy.ops.bim.edit_text_popup()
def hotkey_S_F(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.flip_wall()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.flip_fill()
def hotkey_S_G(self):
if not bpy.context.selected_objects:
if self.props.ifc_class == "IfcSpaceType":
bpy.ops.bim.generate_space()
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.recalculate_wall()
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.recalculate_profile()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.recalculate_fill()
elif self.active_class in ("IfcSpace"):
bpy.ops.bim.generate_space()
def hotkey_S_M(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.merge_wall()
else:
bpy.ops.bim.mirror_elements()
def hotkey_S_R(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.rotate_90(axis="Z")
elif self.active_class in ("IfcColumn", "IfcColumnStandardCase"):
@@ -498,22 +622,30 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.rotate_90(axis="Y")
def hotkey_S_K(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.split_wall()
def hotkey_S_T(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.join_wall(join_type="L")
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.extend_profile(join_type="L")
def hotkey_S_V(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.align_wall(align_type="INTERIOR")
else:
bpy.ops.bim.align_product(align_type="POSITIVE")
def hotkey_S_X(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
if bpy.ops.bim.align_wall.poll():
bpy.ops.bim.align_wall(align_type="EXTERIOR")
@@ -521,6 +653,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.align_product(align_type="NEGATIVE")
def hotkey_S_Y(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.join_wall(join_type="V")
elif self.active_material_usage == "PROFILE":
@@ -537,9 +671,19 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
self.props.z = self.z
def hotkey_A_D(self):
if not bpy.context.selected_objects:
return
bpy.ops.bim.select_decomposition()
def hotkey_A_E(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "PROFILE":
bpy.ops.bim.enable_editing_extrusion_axis()
def hotkey_A_O(self):
if not bpy.context.selected_objects:
return
if AuthoringData.data["has_visible_openings"]:
bpy.ops.bim.edit_openings()
else:
@@ -290,6 +290,7 @@ class BIM_PT_object_actor(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 1
bl_parent_id = "BIM_PT_misc_object"
@classmethod
@@ -72,4 +72,4 @@ class ProfileData:
@classmethod
def is_editing_arbitrary_profile(cls):
obj = bpy.context.active_object
return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile
return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "PROFILE"
@@ -21,7 +21,7 @@ import ifcopenshell.api
import blenderbim.bim.helper
import blenderbim.tool as tool
import blenderbim.bim.module.model.profile as model_profile
from blenderbim.bim.module.model.slab import DecorationsHandler
from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.profile.prop import generate_thumbnail_for_active_profile
@@ -138,8 +138,8 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="EDIT")
DecorationsHandler.install(context)
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
ProfileDecorator.install(context)
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
@@ -149,8 +149,8 @@ class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
if obj and obj.data and obj.data.BIMMeshProperties.is_profile:
DecorationsHandler.uninstall()
if obj and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
bpy.data.objects.remove(obj)
@@ -166,7 +166,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
DecorationsHandler.uninstall()
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
profile = tool.Model.export_profile(obj)
@@ -176,7 +176,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
self.layout.label(text="INVALID PROFILE: " + indices[1])
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
DecorationsHandler.install(context)
ProfileDecorator.install(context)
bpy.ops.object.mode_set(mode="EDIT")
return

Some files were not shown because too many files have changed in this diff Show More