Compare commits

...

397 Commits

Author SHA1 Message Date
Ryan Schultz 535e974cc3 bulk assignment of objects to an aggregate. 2023-05-22 22:06:08 -05:00
Andrej730 629aff1e50 Added a descriptive error on attempt of deleting last material layer 2023-05-22 16:37:31 +05:00
Andrej730 628b6830db Fixed error loading .ifc and activating view for drawing with underlay 2023-05-22 15:47:28 +05:00
Ryan Schultz 1b2ab4e3f0 partially addresses #3103. 2023-05-21 15:07:08 -05:00
Sigma Dimensions f97752c368 fix contracting/expanding schedule of rates 2023-05-21 13:25:15 +01:00
Sigma Dimensions d2d5db9bf6 Fix selecting unassigned work/cost schedule products 2023-05-21 12:59:54 +01:00
Sigma Dimensions a8554d9e56 small fix for work schedule derived start/end dates 2023-05-21 12:52:05 +01:00
Sigma Dimensions 11a38af901 BBIMD: enable deleting sequence relationships from list of predecessors/successors 2023-05-21 12:48:34 +01:00
Sigma Dimensions 27a4976b9d remove annoying print statement 2023-05-21 12:45:28 +01:00
Sigma Dimensions eb54f5a0cb ios api task fix: remove orphan IfcRelNests when deleting tasks 2023-05-21 12:45:04 +01:00
Sigma Dimensions a4a6dce6de BBIM D: Copy cost items, export Cost Schedules individually, start improving Cost Schedule UI 2023-05-21 12:42:50 +01:00
Sigma Dimensions 4184e25c68 Easier grouping of materials by allowing material attributes editing from the IfcMaterial subpanel in the scene properties tab 2023-05-21 12:33:41 +01:00
Bruno Postle d98b0766c2 Allow missing/broken GitPython
`import git` can fail badly with a broken xcode installation, so
we let blenderbim run anyway
2023-05-21 09:11:26 +01:00
Bruno Postle b8990dd0a4 Fix merging of remote branches (#3096)
You can now 'clone' a remote repository and 'push' to it. There is no 'pull'
functionality because this can fail badly, so there is a 'fetch' which
retrieves all remote branches without merging (and never fails). The
user can then select the remote branch in the branch pull-down and merge
it.
2023-05-20 09:25:33 +01:00
Andrej730 2198201e0b Detect BBIM railings at import to import as meshes not curves #3144
Sometimes railing could consist just of one IFCSWEPTDISKSOLID then it will be considered native swept disk solid and imported as a curve.

But when user create BBIM railing they edit it as a mesh and curve might look a bit different. Therefore we detect it now at import to make sure it's going to be reprsented the same way as it was saved.
2023-05-19 17:33:45 +05:00
Andrej730 53e58848f3 Reset active drawing id if we removed the active drawing
Previously there was an error if you remove all drawings:
```
Traceback (most recent call last):
  File "\addons\blenderbim\bim\module\drawing\ui.py", line 182, in draw
    DrawingsData.load()
  File "\addons\blenderbim\bim\module\drawing\data.py", line 106, in load
    "active_drawing_pset_data": cls.active_drawing_pset_data(),
  File "\addons\blenderbim\bim\module\drawing\data.py", line 134, in active_drawing_pset_data
    drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id)
  File "\addons\blenderbim\libs\site\packages\ifcopenshell\file.py", line 326, in by_id
    return self[id]
  File "\addons\blenderbim\libs\site\packages\ifcopenshell\file.py", line 314, in __getitem__
    return entity_instance(self.wrapped_data.by_id(key), self)
  File "\addons\blenderbim\libs\site\packages\ifcopenshell\ifcopenshell_wrapper.py", line 4517, in by_id
    return _ifcopenshell_wrapper.file_by_id(self, id)
RuntimeError: Instance #1 077 not found
```
2023-05-19 15:40:58 +05:00
Andrej730 9c367d1702 fixed neg values in format_distance & section/plan level #3175 2023-05-19 15:28:45 +05:00
Andrej730 db18a81035 Added default Model/Annotation/PLAN_VIEW to support e68fdb2db 2023-05-19 12:33:56 +05:00
Andrej730 d1d6b00f92 Test that drawing styles are loaded 2023-05-19 12:00:00 +05:00
Andrej730 32fc147a7c Fixed error on rebuilding sheet with underlays
Also added description and more clear name for `bim.open_sheet`

Error was:
```
Error: Python: Traceback (most recent call last):
  File "\addons\blenderbim\bim\module\drawing\operator.py", line 82, in execute
    IfcStore.execute_ifc_operator(self, context)
  File "\addons\blenderbim\bim\ifc.py", line 410, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
  File "\addons\blenderbim\bim\module\drawing\operator.py", line 1177, in _execute
    del raster_references[reference.Location]
TypeError: list indices must be integers or slices, not str
```
2023-05-19 11:48:35 +05:00
Andrej730 d98054234c Update drawing styles on changing Underlay checkbox #3172 2023-05-19 10:52:52 +05:00
Bruno Postle 88ba6514e8 Git remote fetch and push operations (#3096) 2023-05-19 00:27:37 +01:00
Andrej730 894a688bd8 Added test scenario to reproduce #3169
Can be easily called with `pytest .\test\bim\test_feature.py -k "issue"`

Commented some lines out to make sure it doesn't interrupt someone's test process.

Need to uncomment `When I press "bim.create_drawing"` if you want to see if it freezes for you.

You can also uncomment `And I save sample test files and open in blender` to see the freeze manually in Blender client.
2023-05-18 18:36:07 +05:00
Andrej730 c69ead4893 Test to cover drawing maintaining sheet position #3155
Updated to gitignore to include some new folders that are created during tests
2023-05-18 17:30:04 +05:00
Andrej730 e68fdb2db7 3d annotations for FALL, SECTION_LEVEL, PLAN_LEVEL #3145
Before that commit if you created those types of annotations and then reopened .ifc file (without .blend) then you would lose their z coordinate and therefore would lose their value (because they were created previously as 2d annotations).

Now those annotations created as 3d to avoid that problem. I've also added temporary fallback that will turn your existing annotations to 3d next time you edit them.
2023-05-18 14:19:36 +05:00
Andrej730 ae9372c3d3 Maintaining drawing's sheet position on raster x/y changes #3155
Example - https://i.imgur.com/uiNWhIq.png
2023-05-17 19:23:42 +05:00
Andrej730 0365c6d43b More consistent PlanLevel and SectionLevel in viewport and svg
Now it's using first vert instead of last to calculate height (same way svgwriter does it)
2023-05-17 14:50:52 +05:00
Andrej730 18b6969221 Showing decorations on activating view #3146 2023-05-17 12:35:53 +05:00
Andrej730 a1935acb3d AngleDecorator - changed edges / arc colors
Now angle annotation edges (not the arc itself) are greyed out when you're not editing them - that way they won't draw unnecessary attention. When you're in edit mode they'll appear white and the angle arc will appear blue.

Demo - https://imgur.com/a/lsgp9G4
2023-05-17 12:20:08 +05:00
Andrej730 9548c5e657 Positioning AngleDecorator text at the center of the arc
Before - https://i.imgur.com/0VxFkg9.png
After - https://i.imgur.com/ibDnlbE.png
2023-05-17 11:52:14 +05:00
Andrej730 762299124f More smoothed lines for annotations with angles and circles 2023-05-17 11:32:01 +05:00
Andrej730 f682bf5688 Added some tests for changing ifc text 2023-05-17 11:18:24 +05:00
Andrej730 f7ed3a8727 Fixed bug with annotations update #3159
toggleDecorations was interrupted because it was trying to update text for non text element
2023-05-17 10:44:26 +05:00
Andrej730 c124506e41 Fixing bug with changing text size #3159
Bug appeared after 046940f but it was there before - it was just kind of compensated by the other bug.
Because of that bug blender text size value was reupdated before saving it to pset therefore pset never received a new value.

Also added some refactor, planned to write some test to get this error covered.
2023-05-16 17:58:26 +05:00
Andrej730 273cdc6ea8 Fixed ifc text annotation preview not showing during edit #3159
Error was:

```
Traceback (most recent call last):
  File "\scripts\addons\blenderbim\bim\module\drawing\decoration.py", line 1899, in __call__
    decorator.decorate(context, obj)
  File "\scripts\addons\blenderbim\bim\module\drawing\decoration.py", line 1451, in decorate
    self.draw_text(context, obj)
  File "\scripts\addons\blenderbim\bim\module\drawing\decoration.py", line 540, in draw_text
    symbol = text_data["Symbol"]
KeyError: 'Symbol'
```
2023-05-16 16:53:38 +05:00
Andrej730 0ef6ea6bd4 Schedule Builder - some naive support for font size in text #3154 2023-05-16 16:43:39 +05:00
Andrej730 607d9a06ec Schedule Builder - support for bold and italic text in cells #3154 2023-05-16 16:31:26 +05:00
Andrej730 1b472ca5f2 Schedule builder - fixing cell alignment #3154
Noticed that it's actually not merging cell style with col style as I did before but just prioritizing cell style if it's defined
2023-05-16 15:24:39 +05:00
Andrej730 18568d6ba7 Schedule cells limit without print range to prevent system freeze
What I've found that sometimes you might get into situation (especially if .ods was created / processed in Excel) you might get a lot unnecessary columns and rows (in fact excel always seems to save 1048576x16384 table) and trying to print this kind of table results in investing a lot of RAM into it and possible system freeze.

Now there is a check if table has more than 10000 cells (number is completely made up 😁) then it would require user to define print range in the table to prevent the issue.
2023-05-16 15:16:48 +05:00
Andrej730 bfb59dd387 Scheduler builder enhancements #3154
Now it supports
1) print ranges - in svg you'll get only the part that's included in print-range.
2) children styles - sometimes cells inherit center alignment from column styles and before that commit text wouldn't be centered in svg in that case
3) repeating rows support
2023-05-16 14:44:45 +05:00
Andrej730 38cf88d5d2 Saving drawing shading styles to ifc #2991
Now shading styles are stored in ifc. It's actually stored in json file (by default it's in projects "assets/shading_styles.json") and the path to it saved in `EPset_Drawing.ShadingStyles`.
Current shading styles is stored in `EPset_Drawing.CurrentShadingStyle`.

This change could potentially cause problems but I've added some temporary fallback to make sure `EPset_Drawing.ShadingStyles` will be set to some default path and default shading styles will be copied to the project's folder.
2023-05-15 16:43:56 +05:00
Bruno Postle 7ca6f9d7a1 BlenderBIM Git clone repository operator (#3096) 2023-05-14 21:10:53 +01:00
Gorgious 5874508263 use tool module instead of local function to activate object 2023-05-13 19:57:52 +02:00
Bruno Postle 954de793dd BlenderBIM Git tagging documentation 2023-05-13 10:37:29 +01:00
Bruno Postle df74d5efbe BlenderBIM list and create git tags #3096 2023-05-13 09:57:10 +01:00
Andrej730 f7b9a7bfb5 Multiline in dimension description / suffix / prefix #3005
Also added some offset for dimension text in viewport to make it more similar to svg
2023-05-12 14:54:22 +05:00
Thomas Krijnen 63eaa88d3d #3139 Capture parsed selected simple type values 2023-05-12 10:19:15 +02:00
Andrej730 f518521ea1 Option to add prefix and suffix to dimension annotation #3005
Example - https://imgur.com/a/0NkStWm
2023-05-11 17:38:48 +05:00
Andrej730 5e1ee22bcd Ifc CSV Export template now also saves the expression #3034
Previously saved templates wouldn't work now, they need to be either recreated or modified to make them work.

Example of old template format:
`["Name", "class", "type.Name"]`

Example of new template format:
`{"expression": ".IfcDoor", "attributes": ["Name", "class", "type.Name"]}`

Note that this would work too:
`{"attributes": ["Name", "class", "type.Name"]}`
2023-05-11 14:18:51 +05:00
Andrej730 ca09ff2ef0 Not all curve annotation types were detected at the import #3090 2023-05-11 10:54:27 +05:00
Bruno Postle 8643aff800 Merge branch 'v0.7.0' of github.com:IfcOpenShell/IfcOpenShell into v0.7.0 2023-05-10 22:53:00 +01:00
Gorgious 2e88993654 Fix #3128 : RefLatitude and RefLongitude attributes now show their description on right click + add a snippet to tell the user how to format the values 2023-05-10 22:51:30 +01:00
Bruno Postle b77849d279 Report ifcmerge conflicts in pop-up #3096 2023-05-10 22:45:22 +01:00
Gorgious c7e288466f Fix #3128 : RefLatitude and RefLongitude attributes now show their description on right click + add a snippet to tell the user how to format the values 2023-05-10 21:31:19 +02:00
Kristoffer Andersen bb48d4aac1 Update meta.yaml
pin daily conda builds to occt 7.7.0
2023-05-10 20:43:22 +02:00
Gorgious56 2b99cc1e58 Fix typo in get_pset docstring 2023-05-10 16:41:19 +02:00
Andrej730 ca286f8d19 Migrating shaders to builtins to support M1 #2897
Migrated all shaders from geometry shaders to builtins (all geometry data now calculated in python before passing to shader) to make them more reliable and support Metal backend on Mac M1 (tested that it works - both annotations and gizmo).

The current downside is that there is no more custom frag shaders too - meaning we do not support dashed lines in the annotations (currently Hidden and Grid just use a bit less bright annotation color).
2023-05-10 18:19:00 +05:00
Bruno Perdigão 73a3cc2cc5 changed custom scale input 2023-05-10 09:11:09 +10:00
Bruno Postle 169709ed04 Selecting a git revision updates info box 2023-05-09 22:15:57 +01:00
Sigma Dimensions 2e746242ce Fix task json issue due to "Bad control character in string literal in JSON at position XXXX" 2023-05-09 15:43:12 +01:00
Sigma Dimensions 951b8e2b15 Convenient Pie Menu shortcut to aggregate selected objects to active object 2023-05-09 14:24:31 +01:00
Sigma Dimensions ede3edcf1e fix toggle_cost_item_parent 2023-05-09 13:25:21 +01:00
Sigma Dimensions 184c9f87b2 Fix baseline gantt chart bug when new tasks are added to the planned schedule 2023-05-09 13:03:57 +01:00
Dion Moult 0793a4602d Minor improvements to shape utility. 2023-05-09 20:51:08 +10:00
Dion Moult bb285c47dc See #2673. Bundle IfcTester for PyPI. 2023-05-09 20:49:08 +10:00
arun 4cb6fc05d6 corrected get_extrusions function 2023-05-08 20:18:57 +02:00
Kristoffer Andersen a302e5db64 add zlib to conda build dependency
zlib is now longer indirectly included from the vtk package now that we have a new occt variant novtk that does not depend on vtk
2023-05-08 14:12:03 +02:00
Bruno Postle ff21749da4 Merge branch 'v0.7.0' of github.com:IfcOpenShell/IfcOpenShell into v0.7.0 2023-05-07 13:12:22 +01:00
Bruno Postle 23b5da7670 BlenderBIM ifc file association (linux only) #2399 2023-05-07 13:08:28 +01:00
Bruno Postle c31e479f47 Rename blenderbim.sh, better initialisation #2399 2023-05-07 09:46:15 +01:00
Dion Moult 260eb31a69 Add shape utility to easily get profiles and extrusions 2023-05-07 17:32:46 +10:00
Dion Moult 1fb6dac6fb Fix failing tests in preparation for release. 2023-05-06 11:05:13 +10:00
Dion Moult 34d947c51f Fix #3084. Issue where drawings with no elements couldn't be generated. 2023-05-06 11:02:41 +10:00
Sigma Dimensions 23f7a6d5a6 fix test-related sequence bug
Fix baseline gantt chart bug
2023-05-06 00:21:57 +01:00
Sigma Dimensions b15926c20a Fix baseline gantt chart bug when new tasks are added to the planned schedule 2023-05-05 23:43:27 +01:00
Sigma Dimensions 0a7ef80ca7 BBIM4D: Enable filtering by active work schedule when retrieve tasks related to objects 2023-05-05 18:26:32 +01:00
Sigma Dimensions be08406cc1 Auto-refresh task sorting 2023-05-05 17:11:24 +01:00
Bruno Postle cc9777b1e8 Basic user documentation for BlenderBIM Git functionality (#3096) 2023-05-04 23:53:37 +01:00
Dion Moult 539b3a243b Fix fundamental bug where area and volume units were ignored on new projects. 2023-05-04 21:43:48 +10:00
Andrej730 41e58a3d46 Small addition to 8c302ed85 2023-05-04 16:00:12 +05:00
Andrej730 8c302ed85c Changed x_angle subtype to degrees
Now BIMModelProperties.x_angle stores data in radians internally but for user it still appears as degrees
2023-05-04 15:53:29 +05:00
Dion Moult dc4dc46b62 See #1676. Fix space boundary generation for IFC4 RV where openings are already subtracted from the body. 2023-05-04 20:22:29 +10:00
Dion Moult a07fa5a2c2 Accommodate invalid named units when loading IFCs. 2023-05-04 19:55:54 +10:00
Dion Moult ad9dbce003 See #3085. Don't bundle numpy as Blender already has it and it causes conflicts. 2023-05-04 19:45:04 +10:00
Andrej730 d0526d0ae5 Updating BIM Tools props on active object change #3102
Updating extrusion_depth, length and x_angle on active object change. Shouldn't be too expensive and that way it'll be more consistent with Blender UI.
2023-05-04 12:55:40 +05:00
Andrej730 75e9c3265d Fallback to abs path adding schedule rel path is not available #3106 2023-05-04 11:32:05 +05:00
Andrej730 b37705ab6e Updating drawing's size on sheet to match it's scale #2965 2023-05-04 11:25:16 +05:00
Massimo Fabbro d9d5f86a6e Now it's possible to regenerate a space that it was created automatically from walls 2023-05-04 11:37:42 +10:00
Sigma Dimensions 1f3ad95c00 remove annoying print statement, run black on cost 2023-05-03 19:46:15 +01:00
Sigma Dimensions 9e360f7efa remove annoying print statement, run black on cost 2023-05-03 19:45:01 +01:00
Sigma Dimensions 64fd6a1f52 fix copying relationships when duplicating tasks 2023-05-03 19:43:05 +01:00
Sigma Dimensions ba04df4ce3 BBIM 4D: implement human readable duration to read/edit durations.
new IOS Fuzzy Duration Parsing utility
2023-05-03 16:09:27 +01:00
Sigma Dimensions cd27a026fd fix editing null task duration values #3091 2023-05-03 16:01:58 +01:00
Dion Moult baedf71106 Minor documentation update to describe how add-ons with conflicting deps / hotkeys are incompatible. 2023-05-04 00:31:44 +10:00
Dion Moult 4f6c95b119 See #1676. Space boundary generation now accounts for voids and fills. 2023-05-04 00:31:24 +10:00
Dion Moult b1761ac393 See #1676. Space boundaries default to physical internal boundaries. 2023-05-03 21:21:55 +10:00
Dion Moult fcdf3e583f See #1676. You can now set the type of boundary class prior to generating boundaries. 2023-05-03 21:03:54 +10:00
Andrej730 2001119dff A bit more intuitive adding drawing/schedule to the sheet
Now it will detect currently active sheet even if you have sheet's drawing/titleblock/schedule selected.
2023-05-03 15:39:53 +05:00
Dion Moult 60a9d96ac8 See #1676. You can now duplicate space boundaries. 2023-05-03 20:38:09 +10:00
Andrej730 6bd177d250 Annotation Tool - always show "Add type" button
It wasn't showing if you had some objects selected which seems a bit counterintuitive - the need to deselect everything to create a new type.
2023-05-03 15:00:58 +05:00
Andrej730 1ece84e31d Fixed error during building schedule from ods
We were discarding some styles that didn't have any style properties and then we always needed to keep it in mind later on, now we just create empty dict for those.
2023-05-03 14:55:45 +05:00
Andrej730 40ea9457a8 Apply scale and rotation to ifc text symbols #3094
Before symbols attached to ifc text were not rotated / scaled if they didn't have "text-template" fields in them.
2023-05-03 14:39:43 +05:00
Andrej730 e95d91a91e Support multiline text in the schedule #3010
Before - https://i.imgur.com/gff5jo3.png
After - https://i.imgur.com/meyXBYb.png
2023-05-03 12:33:07 +05:00
Andrej730 c8fb92175f Added some (naive) text wrapping in the schedule #3010
Before - https://i.imgur.com/dJPQ788.png
After - https://i.imgur.com/T1FtI8H.png

For it to work make sure you have word wrapping in ods.

It's still very naive since it's taking constant font width which could vary depending on font type, font size, font lettering. The proper way would be is to either find some svg way to do it (couldn't find any stable one) or to render text to identify it's width (could be done with PIL but seems overkill, atleast for now).
2023-05-03 12:25:08 +05:00
Sigma Dimensions a6fb4cbfbf BBIM 4D: Enable displaying task tooltip in the gantt chart & add resource columns 2023-05-03 04:34:55 +01:00
Sigma Dimensions 30bb6d05c5 BBIM 4D: You can now visualize & print work schedule baselines with Gantt Charts 2023-05-03 04:33:16 +01:00
Sigma Dimensions 9422f804fb minor fix 2023-05-03 04:22:02 +01:00
Sigma Dimensions 415a7746be BBIM 4D Gantt: Support multiple languages, human readable durations, add convenient Print Function with different Sheet Formats,
start improving overall page style; refactor code to split js and css code away from mustache file
2023-05-02 17:27:47 +01:00
Andrej730 ee903208dc Small fix for text alignment in scheduler 2023-05-02 17:41:56 +05:00
Dion Moult f89e834fea Fix #3085. Prioritise shipped BlenderBIM Add-on packages instead of system packages to avoid dependency clashes. 2023-05-02 22:20:34 +10:00
Dion Moult e8b4335741 Minor UI cleanup. Remove align buttons from architectural tool panel. 2023-05-02 22:20:34 +10:00
Dion Moult 83010f0542 See #1676. You can now add basic space boundaries with the BIM Tool with Shift-B, either by selecting both a space and an element (e.g. wall) or selecting a space and placing the 3D cursor on a face. 2023-05-02 22:20:34 +10:00
Dion Moult 04e5f5036c See #1676. Deleting space boundaries now also removes connection geometry. 2023-05-02 22:20:34 +10:00
Andrej730 5d86d38a14 Support cells text alignment building schedule from ods #2885
Before - https://i.imgur.com/9QZ2YW2.png
After - https://i.imgur.com/M7ZHSJq.png
2023-05-02 17:04:48 +05:00
Andrej730 a82db1f622 Support merged columns building schedule from ods #2885
Before - https://i.imgur.com/p1l6ugn.png
After - https://i.imgur.com/9QZ2YW2.png
2023-05-02 16:24:30 +05:00
Andrej730 fb618d536b Keep rows height building svg schedule from ods #2885
Before - https://i.imgur.com/erMH4Ku.png
After - https://i.imgur.com/p1l6ugn.png
2023-05-02 14:56:05 +05:00
Andrej730 23f96779fa More safe adding roof modifier #3080 2023-05-02 10:38:25 +05:00
Dion Moult 7b67854654 Fix #3088. Prioritise body curves to be loaded before trying to load 2D annotations. 2023-05-02 13:11:01 +10:00
Sigma Dimensions 2e7f941465 BBIM 4D: Auto Update Resource Usage from resource tree 2023-05-02 02:47:22 +01:00
Sigma Dimensions 1c1706fde3 BBIM feature to create & view work schedule baselines 2023-05-02 02:43:54 +01:00
Sigma Dimensions ca77a895e0 IOS.API consider removing baseline tasks when deleting tasks 2023-05-02 02:31:48 +01:00
Sigma Dimensions 0f447f964e IOS.API usecase to create a work schedule baseline 2023-05-02 02:30:38 +01:00
Sigma Dimensions 2c31fc6b25 run black 2023-05-02 02:27:32 +01:00
Dion Moult 0ba79dc105 Accommodate space boundaries with no connection geometry, fetch spatial structure for IFC2X3, and fix scale bug when editing boundary profiles. 2023-05-02 10:23:54 +10:00
dependabot[bot] 52c686b656 Bump flask from 2.0.1 to 2.3.2 in /src/opencdeserver
Bumps [flask](https://github.com/pallets/flask) from 2.0.1 to 2.3.2.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.0.1...2.3.2)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-02 10:06:31 +10:00
Thomas Krijnen 64d98f1295 #2676 2023-05-01 20:16:39 +02:00
Andrej730 ceb2c4075c Option to scale symbol from ifc tag by scaling ifc text object #2946
Also added some text scaling in viewport if there is some symbol associated with text annotation to roughly visually indicate how big this thing is going to be in the svg.

Example - https://user-images.githubusercontent.com/9417531/235445393-447f3aba-8425-4da6-81d0-6bf9004fcf2b.mp4
2023-05-01 16:25:04 +05:00
Andrej730 166e7d52ec Keep text rotation for text annotation with symbols #2946 2023-05-01 16:25:04 +05:00
Dion Moult 644c2652ea See #1676. You can now toggle boundaries on or off in the BIM tool via Alt-B, with a special boundary decorator. 2023-05-01 21:19:29 +10:00
Andrej730 63b201fd95 Test command to load blend file during test scenario for debugging 2023-05-01 15:35:27 +05:00
Andrej730 d70616056c Moving decorators colors from scene to addon props #3033 2023-05-01 14:54:47 +05:00
Dion Moult a65e376886 See #1676. Tab now toggles space boundary surface profile editing. 2023-05-01 19:40:49 +10:00
Andrej730 a985c70f0c Fixed error on adding Roof modifier to non-footprint objects #3080 2023-05-01 14:03:28 +05:00
Andrej730 0d09e02e7e Fixed bug with double conversion of height/extrusion length #3073
Also fixed some tests
2023-05-01 13:46:24 +05:00
Thomas Krijnen 23e3551f27 Fix regex escape in rule_executor.py 2023-04-30 19:09:43 +02:00
Shohei Kunimatsu 885bca0865 Fix wrong attribute name usage (ValueComponent -> UnitComponent) in IfcFile::getUnit 2023-04-30 19:06:58 +02:00
Sigma Dimensions 0f874dad51 BBIM 4D features & fixes:
- feature to re-order tasks
- fixes for task sorting
- further workSchedule UI/UX improvements
- forgotten bits and bobs
2023-04-30 15:44:13 +01:00
Sigma Dimensions 107873cf6e - refactor IOS.API resource calculation to re-use shared utilities
- run black
2023-04-30 15:35:17 +01:00
Sigma Dimensions 4a8f5af7ba - BBIM 4D: features to review and edit Resource productivity data on the fly
- IOS: new resource utilitiy functions
2023-04-30 15:31:36 +01:00
Dion Moult 83084aeb7b Add profile-style (like slab editing) enable editing, edit, and disable editing operators for energy boundary surface geometry. 2023-04-30 20:57:51 +10:00
Dion Moult 51640ccc28 Fix #3030. Don't bother layerset slicing walls with a single layer. 2023-04-30 13:43:43 +10:00
Dion Moult aefbc134ea Cut merging now only applies to particular elements for the majority of cases. Drawing generation can be twice as fast due to this. 2023-04-30 13:43:43 +10:00
Massimo Fabbro 29e1d70334 Generate space from walls refactor and cleaning around 2023-04-29 22:33:19 +02:00
Massimo Fabbro 9118e529e6 Add generate space button in UI in N-Panel -> BlenderBim -> Architectural 2023-04-29 22:33:19 +02:00
Massimo Fabbro 8ee0a1611d Now it's possible to create spaces automatically from selected walls 2023-04-29 22:33:19 +02:00
Sigma Dimensions 2af4388f40 Fix duplicate task logic in IOS.API 2023-04-29 13:53:04 +01:00
Dion Moult 93a2d18ea6 Filter elements visible in the camera view. Drawing generation is now significantly faster on larger projects (e.g. 24s down to 10s for a portion of the Revit sample project) 2023-04-29 16:33:44 +10:00
Dion Moult fc49859ea4 Fallback to unmerged polygons if merging fails. More investigation needed. 2023-04-29 16:30:40 +10:00
Dion Moult c0ac9cb380 Viewport layerset slicing now uses a cache for speed. 2023-04-28 22:46:49 +10:00
Andrej730 d5cc6c3c8e Fixed double conversion in bim tool props after b4b6404
No more 1000000 and 3000000 mm values
2023-04-28 17:32:19 +05:00
Andrej730 9b22b5000f Added support for different railing cap types
Very similar to this - https://i.imgur.com/SNyvfgh.png
2023-04-28 17:24:41 +05:00
Dion Moult 3b69a4cc78 Fix bug where numerical tolerance caused certain objects to be unnecessarily updated. 2023-04-28 22:15:09 +10:00
Dion Moult adf54efcc5 Fix bug in cut decorator layersets where walls could be joined to odd non-layered things. 2023-04-28 22:14:26 +10:00
Dion Moult f6962eb58e Fix #3053. Bug where very thin meshes (<1mm) couldn't be reverse engineered into extrusions. 2023-04-28 21:05:13 +10:00
Andrej730 11601d6532 Fixed bug with wrong position of support in wall mounted railing
The problem was that that support's disk was not rotated according to the railing path.
2023-04-28 11:43:15 +05:00
Sigma Dimensions be2131d62c BBIM 4D: Auto-refresh task re-ordering 2023-04-27 13:58:08 +01:00
Dion Moult 3ffbf46ac2 Fix #3066. Regression crash when deleting openings. 2023-04-27 22:24:12 +10:00
Sigma Dimensions 3943107eec forgotten core functions & run black 2023-04-27 13:05:51 +01:00
Sigma Dimensions 94e3f75341 Sequence Tools clean-up 2023-04-27 13:03:32 +01:00
Sigma Dimensions 8bc8b950e7 BBIM 4D: Features to select a Work Schedule's assigned / unassigned products 2023-04-27 12:55:31 +01:00
Sigma Dimensions 05a95ac666 Work Schedule Panel UX improvement 2023-04-27 12:53:27 +01:00
Sigma Dimensions f016c803e4 Fix schedule task sorting 2023-04-27 12:49:27 +01:00
Dion Moult bac0dd37fa Fix #3052. Deep deletion of large geometries is now thousands of times faster. 2023-04-27 20:15:17 +10:00
Bruno Postle 75a37cf737 Fix crash when switching between git revisions 2023-04-26 23:50:36 +01:00
Bruno Postle bb7e9c4610 Fix check for invalid Git branch name 2023-04-26 22:25:27 +01:00
Andrej730 ee2f18349f WALL_MOUNTED_HANDRAIL type for Railing modifier
Added WALL_MOUNTED_HANDRAIL type for railing modifier. It's still a bit work in progress - still need to add support for different railing termination types.

Here's the short demonstration - https://user-images.githubusercontent.com/9417531/234582621-6c6f948b-9cdc-4be6-b380-17ddbf5803bf.mp4
2023-04-26 18:04:39 +05:00
Dion Moult 8829a3cbe9 See #1227. Minor fix. 2023-04-26 22:24:37 +10:00
Dion Moult b210e39c7e See #1227. Experimental viewport layerset visualisation. 2023-04-26 22:10:34 +10:00
Bruno Postle 4d4500f061 Fix some refresh glitches in IFC Git panel 2023-04-26 00:07:22 +01:00
Thomas Krijnen 79860a0c04 Fix iterator::get() invalid cast on nullptr 2023-04-25 21:00:01 +02:00
Bruno Postle d53fcdc616 Format ifcgit code with black 2023-04-25 07:30:35 +01:00
Dion Moult 5e6a094ac6 Fix #3046. Renaming drawings now updates all drawing references on sheets. 2023-04-24 22:41:38 +10:00
Andrej730 757bf4e916 Fixing error on using bim.override_mode_set_edit with no active object
Error was:
  File "\addons\blenderbim\bim\module\geometry\operator.py", line 775, in _execute
    obj.select_set(True)
AttributeError: 'NoneType' object has no attribute 'select_set'
2023-04-24 11:17:08 +05:00
Andrej730 7bb57b3ab9 Fixed blurred text with background after bfbced3
before commit - https://user-images.githubusercontent.com/9417531/233907917-f86ad10b-0227-4180-9488-a8b71a00f099.png
after commit https://user-images.githubusercontent.com/9417531/233907920-d100c73a-e206-42fd-bb41-f7257ea6f98c.png

Tested it on firefox too
2023-04-24 10:33:38 +05:00
Dion Moult cb22b5804d Fix #3055. Get paths using forward slashes for platform compatibility. 2023-04-24 13:11:42 +10:00
Dion Moult bfbced3859 Fix #3051. Text background filter now applied on <text> element to make it work on Firefox. 2023-04-24 12:58:36 +10:00
Dion Moult c090ee9b83 Fix #3040. View title template variables now have access to sheet attributes prefixed with "Sheet". 2023-04-23 22:04:49 +10:00
Dion Moult b6caf069d0 Fix #3001. User can now specify join criteria in drawings. Only elements where all the criteria have the same values are merged. 2023-04-23 21:32:00 +10:00
Dion Moult 5b4449eea0 Fix #3054. Edit sheet is now implemented for drawings, schedules, and titleblocks. 2023-04-23 17:49:51 +10:00
Bruno Postle 7dc17a3653 Include git, gitdb and smmp libs in blenderbim zip 2023-04-23 08:35:39 +01:00
Bruno Postle 598bf799de Install GitPython *before* removing the working folder 2023-04-23 08:12:49 +01:00
Bruno Postle 4774a37d48 Add GitPython dependency 2023-04-23 08:03:19 +01:00
Dion Moult d9a1e6f4b1 New default patterns for stack and stretcher bond tiles and floorboards. 2023-04-23 12:44:35 +10:00
Bruno Perdigão 72dae8e9ad applyed the patch by Bruno Postle 2023-04-23 12:05:57 +10:00
Bruno Perdigão b2d7fb9443 changed imports to integrate with blenderbim 2023-04-23 12:05:57 +10:00
Bruno Perdigão bf945e36bd initial commit 2023-04-23 12:05:57 +10:00
Dion Moult 2bcfe93dd2 See #3002. Spaces are now managed separately so that they don't conflict with projection polygon merging and raycasting. 2023-04-22 23:59:33 +10:00
Dion Moult 195ef743c6 See #3002. Experimental SVGFill approach to polygon merging. 2023-04-22 13:05:42 +10:00
Thomas Krijnen 8478eb9aa0 Update draw.py 2023-04-21 16:29:04 +02:00
Andrej730 47f57d20cd Fixed couple bugs in furniture library
1) last polyline point was failing on Consecutive rule causing validation errors
2) Fixed wrong ifc class for dishwasher (was used occurence class instead of type)
2023-04-21 18:24:32 +05:00
Dion Moult 8f8eb06d6b See #3002. Experimental shapely approach to polygon merging for semantic projection elements. 2023-04-21 22:33:08 +10:00
Andrej730 7d6803f325 Roof modifier - remove redundant context fill causing visual artifacts 2023-04-21 16:49:44 +05:00
Andrej730 f9b7a8b92b Roof modifier - removing bottom chord if angle = 90 degrees
This - https://i.imgur.com/75AmYGI.png
instead of https://community.osarch.org/uploads/editor/yp/zcaqxx9b33wu.png
2023-04-21 16:49:44 +05:00
Andrej730 7ee0c047e5 Roof Modifier - separating vertices on defining gable roof angle
Added option (which is activated by default) - when you set gable roof angle for some edge it will duplicate the top vertex instead of just moving which allows you to do shape like this - https://imgur.com/a/OEFZ4pw

Previously you would get something like this in that case - https://community.osarch.org/uploads/editor/ap/hlt70rbw5r1l.png
2023-04-21 16:49:44 +05:00
Andrej730 bfbfe17644 OverrideDelete - remove objects with X if they are not related to ifc
It was a bit frustraiting when you added a cube and can't just get rid of it with X hotkey 😅
2023-04-20 17:15:17 +05:00
Andrej730 046940f949 Fixed error in disabling text exiting after deleting text literal 2023-04-20 14:27:37 +05:00
Dion Moult 2950ddcf4f Fix validation error where IfcURIReference cannot be used as a property data type :( 2023-04-20 18:06:35 +10:00
Dion Moult 3d441b4ad3 Fix #3038. Fix #3039. Selector should default to none instead for throwing errors if the query doesn't have a result. 2023-04-20 17:41:52 +10:00
Dion Moult caa5e8983b Fix #3021. IfcSpatialElements are included in drawings by default. 2023-04-20 17:34:24 +10:00
Andrej 59b7944d8e Merge pull request #3032 from IfcOpenShell/quantities_project_units
Converting quantities to project units to fix #3025
2023-04-20 11:03:12 +05:00
Andrej730 648eb8fdd1 Converting to project units inside QtoCalculator.guess_quantity
Also added some error message if `calculate_quantity` failed to calculate a quantity
2023-04-20 11:01:38 +05:00
Andrej730 f160516772 tool.Qto.convert_to_project_units 2023-04-20 10:32:36 +05:00
Andrej730 4beb7780f8 bim.calculate_quantity - remove convertion part
now the convertion already happens in QtoCalculator.calculate_quantity
2023-04-20 10:32:24 +05:00
Kristoffer Andersen d44b6d40e9 pin lief version for conda build 2023-04-20 06:48:44 +02:00
Kristoffer Andersen 8fd9f5a8cb Add conda to conda daily build trigger path 2023-04-20 06:43:31 +02:00
Kristoffer Andersen 414af2f55e Change conda lief version 2023-04-20 06:40:44 +02:00
Kristoffer Andersen 8624be6bcf Fix conda env 2023-04-20 06:36:10 +02:00
Andrej730 4aa26b613b test_qto.py black format 2023-04-19 19:05:11 +05:00
Andrej730 57cb062d48 Tests for calculating quantities for prefix and imperial project units 2023-04-19 19:04:05 +05:00
Andrej730 b4b6404b65 Added DISTANCE subtype to BIM Tool Extrusion Depth and Length
In BIM Tool it looks like this right now - https://i.imgur.com/JWVPssg.png (note the units)
Previously it was displaying value like this - https://i.imgur.com/244aYTN.png

The benefit is that you can specify other units in that kind of property and it will automatically convert to current project's unit. 

For example if you have millimeters as project's unit you can still type "10m" instead of "5000" and it will automatically convert it to 5000mm. 
Or type in inches like 5" 11'.
2023-04-19 18:03:26 +05:00
Andrej730 989dd2be50 qto_calculator.py black format 2023-04-19 17:04:15 +05:00
Andrej730 50b8baddec Update QtoCalculator to convert to project units at the end
Before this commit it was calculating all values in meters not taking into account actual project units.
2023-04-19 17:03:32 +05:00
Andrej730 2e00c3db0b Update viewport on bim.activate_model
To see the changed context right away in the viewport
2023-04-19 12:12:11 +05:00
Andrej730 232260787e Fixed bug with wrong text value in viewport on canceling text editing
Actual text was replacing with "TEXT" default value if you cancel text editing instead of applying changes
2023-04-19 11:37:10 +05:00
Andrej730 54054c4aee Fixed bug in get_object_bounding_box #3023 2023-04-19 11:27:45 +05:00
arun 1a7843a6b5 Nesting added to util.get_decomposition function 2023-04-19 15:46:51 +10:00
Sigma Dimensions ed4ba4d0d3 fix "cost_item quantity updates" bug when editing a QTO 2023-04-19 01:01:44 +00:00
htlcnn 849d6118d0 fix EditBcfTopic and EditBcfTopicName 2023-04-19 09:23:50 +10:00
Andrej730 364138d408 CutDecorator color connected to preferences too #2987 2023-04-18 18:13:04 +05:00
Dion Moult 32c8da06ca New default triangle, hexagon, capsule, circle, and space tag symbols 2023-04-18 22:32:09 +10:00
Andrej730 2df4c5825f Added some decorator colors to addon preferences #2987
For ProfileDecorator (editing profiles, roof preview) and DecorationsHandler (openings).
Still need to add CutDecorator colors to preferences
2023-04-18 17:29:07 +05:00
Andrej730 527cbc3ae0 Some descriptions for EPset_Drawing 2023-04-18 16:59:07 +05:00
Andrej730 4fee8744d6 Fixed text annotation rotation in viewport #3017
Fixed text annotation rotation in viewport for the views rotated by multipel axis.
Changed text direction calculation to be the same in both viewport and svg.
2023-04-18 16:59:07 +05:00
Thomas Krijnen 8f8862d23d get_mappeditem_transformation() 2023-04-18 13:17:20 +02:00
Thomas Krijnen 836f36fd8e Add util.placement.get_cartesiantransformationoperator3d() 2023-04-18 13:15:13 +02:00
Thomas Krijnen a1326cb5ed Normalize vectors in a2p() 2023-04-18 13:13:29 +02:00
Vukas Pajic 4bfda8851c Update failure reason #3018 2023-04-18 13:12:24 +02:00
Andrej730 f02f15cbfb unsuppress zero inches in formatting distance #3011
Added option to suppress zero inches in the dimension annotation text through SuppressZeroInches property of BBIM_Dimension property set.
Previously by default suppressing zero inches was enabled. Now by default it is disabled.

SupressZeroInches On  - https://i.imgur.com/nrhELnA.png
SupprezZeroInches Off - https://i.imgur.com/gIuO2GW.png
2023-04-18 14:17:40 +05:00
Dion Moult 30182684d5 Fix bug where you could save geometry with no data (no vertices / splines) which is invalid. 2023-04-18 18:10:01 +10:00
Dion Moult 7df80af30b Fix #2913. Bug where properties with multiple inverses were not correctly purged. 2023-04-18 18:08:24 +10:00
Dion Moult 6b2e867107 Fix #2916. Bug where joining swept solids didn't merge styles correctly. 2023-04-18 18:07:44 +10:00
Vukas Pajic d333090b09 cleanup 2023-04-18 09:17:23 +10:00
Vukas Pajic 8ae7763ac5 added new tests for case where spec has no requirements 2023-04-18 09:17:23 +10:00
Andrej730 51ac86be79 Fixed bug in bim.select_assigned_product
It was trying to call method `Drawingcls.get_active_object()` that didn't exist
2023-04-17 14:47:11 +05:00
Andrej730 552078c8a2 TEXT_LEADERS BoxAlignment, FontSize, multiline in viewport preview #3008
Moved code for drawing text in viewport to a separate reusable method `draw_text` in BaseDecorator.
2023-04-17 11:21:07 +05:00
Dion Moult 4b4ede733f Add support for loading IfcProxy 2023-04-17 10:37:30 +10:00
Dion Moult d07e8bc747 Add support for editing ref latitude/longitude 2023-04-17 10:14:22 +10:00
Massimo Fabbro 8850d78e5a Small fix 2023-04-17 09:25:21 +10:00
Massimo Fabbro 4fbf4e7d42 Now it's possible to specify the digits number in a drawing. Fix #2993 2023-04-17 09:25:21 +10:00
Ryan Schultz d6e1a670b9 Library - changed to correct class 2023-04-17 09:24:35 +10:00
Dion Moult 1a32c8827e Fix #3003. 2023-04-17 09:11:33 +10:00
Dion Moult 784b5156df Fix #2975. Fix bug where imperial projects still created drawings with metric scales by default. 2023-04-16 23:18:22 +10:00
Dion Moult 6e3336afb1 See #2975. Fix bug where custom scales were not loaded correctly. 2023-04-16 23:17:56 +10:00
Dion Moult d0ddee3623 Fix bug where changing object visibility didn't first check for view layer existence. 2023-04-16 21:50:58 +10:00
Ryan Schultz 9cf55033d4 Fix #2989 2023-04-16 08:05:05 +10:00
Dion Moult 7371efde17 Fix #2980. Minor cleanup to polish array panel UI. 2023-04-15 16:01:50 +10:00
Dion Moult 156269a53f Fix #2988. Imperial dimensions in viewport are now split between feet and inches. 2023-04-15 15:33:01 +10:00
Ryan Schultz ed7e5fb482 Fix #2990 2023-04-14 15:57:36 -05:00
Andrej730 2e973a4591 Fixed bugs with furniture library toilets and basins #2976
Bug was introduced in cf6acd5 when `shape_builder.get_rectangle_coords` started returning coords in counter-clockwise order instead of clockwise.
2023-04-14 19:07:38 +05:00
Andrej730 f1bd82d301 Dimension description in viewport/option to hide the measurements #2918
same thing for diameter annotations since they are just reusing dimension annotations

+ some refactor along the way
2023-04-14 17:52:54 +05:00
Dion Moult f51bfa314d Fix #2986. Reimplement Metadata property of EPset_Drawing to include arbitrary metadata as CSS classes. 2023-04-14 20:24:02 +10:00
Dion Moult 7b3410c7b1 Fix #2985. Purge obsolete vector style feature. 2023-04-14 20:04:33 +10:00
Dion Moult 6b0e7d51b1 Fix #2979. Batch drawling unselected and selected cut lines. 2023-04-14 18:24:01 +10:00
Andrej730 4e5659efad Annotation Tool
Added Annotation Tool that allows you create annotation types and their occurences. It will also allow us to get rid of "Annotation" panel in N-panel.

I've recorded bunch of short videos to demonstrate the features:

- bulk tagging - currently works only for "Text" and "Stair arrow" type of annotations, adds annotations to all selected objects, automatically assigns products to the annotations
https://user-images.githubusercontent.com/9417531/231968358-28cbb762-3349-470a-beef-d1ab21882769.mp4

- creating annotation type with representation - all occurences will share the same representation which makes bulk tagging really powerful if you use variables like {{PredefinedType}}, {{GlobalId}}, {{Name}}, {{Description}}, {{ObjectType}}, {{Tag}}

https://user-images.githubusercontent.com/9417531/231970500-333514c6-6406-4ef5-a9e7-fddfcc9a2436.mp4

- creating annotation type without representation - it allows occurences to have unique text or line geometry but still share the same classes from inherited property set

Examples:
  creating text type without representation to create title case annotations
  https://user-images.githubusercontent.com/9417531/231969361-bda5f924-a8c8-4f5a-ba61-461aa7101f73.mp4

  creating type for oblique dimension annotations
  https://user-images.githubusercontent.com/9417531/231969902-38e8c3f4-45a6-4c98-bba4-2956288ce10a.mp4

- readjusting - if you created some annotations using bulk tagging but the products were changed / moved afterwards now you can select those annotations and "readjust" them according to new product's position / measurements
https://user-images.githubusercontent.com/9417531/231968975-ee8d6589-60ee-4575-9be2-b0ce68d43ffa.mp4

PS In this commit I've also moved all icons to 1 .blend file and added instructions how to regenerate them.
2023-04-14 12:40:53 +05:00
Dion Moult 66f10adde8 Fix #2980. Array deletion now uses delete tool. Delete moved into tool so it can be used by other functions. 2023-04-14 09:56:55 +10:00
Dion Moult 459c4d7fcc See #2978. Fix crash upon drawing deletion. 2023-04-14 09:36:51 +10:00
Dion Moult 03d9efb790 Fix #2955. Drawings can now set a precision. 2023-04-13 22:20:18 +10:00
Dion Moult 33bd6099ee Fix #2971. Only update placement if object actually moved. 2023-04-13 21:56:32 +10:00
Dion Moult 850a1d326b Purge old code from timer-based type browser system 2023-04-13 21:39:54 +10:00
Thomas Krijnen f0bb19d48d Fixes to validation of selected simple type / enum 2023-04-13 13:16:48 +02:00
Dion Moult a56eac0131 Minor fix 2023-04-13 18:48:20 +10:00
Dion Moult d3201809ab Fix #2968. Deletions are no longer postponed. Should fix or reveal a lot of sync errors. 2023-04-13 18:38:28 +10:00
Dion Moult 3628a0e241 Fix bug where switching representations didn't update the material checksum, leading to unnecessary material syncs at export time. 2023-04-13 18:15:28 +10:00
Dion Moult 07badd3df5 Styles are applied per mesh data now (potentially dangerous). Fix crash when switching to representation which has styles from an unstyled mesh. 2023-04-13 16:10:00 +10:00
Dion Moult f63091ce05 Fix #2974. Type property sets are now properly copied. 2023-04-13 14:08:02 +10:00
Dion Moult 617b2978c0 New activate model operator to switch back to a MODEL_VIEW target view subcontext 2023-04-12 21:27:43 +10:00
Dion Moult 364e96ad1e Activating a drawing now automatically switches to the preferred target view representation of all elements 2023-04-12 21:27:43 +10:00
Dion Moult 9170e5ca65 New "cut decorator" that draws cut lines in drawings. 2023-04-12 21:27:43 +10:00
Dion Moult cbe71f221c Only activate drawing styles if underlay enabled, and default to default Blender styles to prevent confusion 2023-04-12 21:27:43 +10:00
Dion Moult c039c01407 Regenerating fills now maintains the active representation 2023-04-12 21:27:43 +10:00
Dion Moult 629ba41f5f Only allow drawing / sheet / schedule creation once the IFC is saved to enforce correct relative paths. 2023-04-12 21:27:43 +10:00
Andrej730 198fa67cc1 Focus on text field on edit Ifc Text with BIM Tool hotkey (once again)
This option was here before but it was accidentally discarded after f7181aa
2023-04-12 11:51:50 +05:00
CyrilWaechter e7830e3d55 Fix #2964 2023-04-11 22:03:26 +02:00
Andrej730 90bfb25f41 restoring selection on bim.override_mode_set_edit if nothing worked
it was a bit confusing when you're trying to edit some object, you get error message but you also lose object selection
2023-04-11 19:17:03 +05:00
Andrej730 037cb00caa Fixed bug with roof created in projects with milimeters units
the problem was that operators were converting angle using SI and angle was set to 10000
2023-04-11 19:17:03 +05:00
Andrej730 f002124c38 Fixed error in PLAN_LEVEL elevation value in printed svgs #2961
Also added more precision to inPerFoot in helper.format_distance to avoid this a bit annoying precision problem where format function keeps adding 1/256"

Without precision - https://i.imgur.com/D9WYjvJ.png
With precision - https://i.imgur.com/3XgOFSH.png
2023-04-11 19:06:59 +05:00
Andrej730 ac42f9f599 Changed roof preview colors and it's now transparent
Changed roof preview colors to the same as openings
2023-04-11 19:06:58 +05:00
Andrej730 88d4640d3f fixed mistake in tool.Blender.apply_bmesh
was resulting in error when you apply roof path
2023-04-11 17:30:29 +05:00
Andrej730 d2da01041c Added checkboxes to drawing list to mark multiple selected drawings
Checkboxes affect the drawings that will be used when you shift click "Create drawing", "Open Drawing", "Remove Drawing". Previously those opeartors were always applying the action to all drawings.

Also added option to remove selected drawings with shift+click to "Remove Drawing" and added new operator "Select all drawings" to select drawings in one click (you can shift click it to delsect all drawings).

Example - https://i.imgur.com/GAGghu5.png
2023-04-11 17:14:10 +05:00
Dion Moult 75c17b820f Fix bug where invalidly mapped representations shouldn't be edited. 2023-04-11 21:50:28 +10:00
Thomas Krijnen 2dc4bbf8cd Support rule compilation and execution on latebound schema 2023-04-11 13:24:32 +02:00
Thomas Krijnen c9a5abc3be Some small fixes for rule compliance in template.py 2023-04-11 09:46:50 +02:00
Thomas Krijnen 7ea468a9a6 Update build-all.py 2023-04-11 08:47:46 +02:00
Dion Moult c6223520e0 Fix #2947. Embarrassing bug where slabs didn't consider the total thickness. 2023-04-11 16:24:39 +10:00
Dion Moult 176cbf9861 Fix #2948. Bug where IFC array tool didn't track undo properly causing crashes. 2023-04-11 15:21:04 +10:00
Dion Moult 00315a6f2d See #2516. Titleblocks dropdown now also shows project titleblocks. 2023-04-11 14:08:48 +10:00
Dion Moult 2145b2f8e2 Fix #2955. You can now store default paths in a project pset. 2023-04-11 14:06:31 +10:00
Dion Moult 31155e7bc9 See #2516. Fix issue where getting sheet layout URIs were not explicit. 2023-04-11 12:26:05 +10:00
Trashman247 2d07208d0f Reformat parametrized "Save" in IFC project (again) 2023-04-11 10:26:48 +10:00
Trashman247 35ad7e16c6 Reformat parametrized "Save" in IFC project 2023-04-11 10:26:48 +10:00
Trashman247 27c52fe6d5 Add dynamic tool tip for parametrized "Save" in IFC project 2023-04-11 10:26:48 +10:00
Andrej e153f63a23 Merge pull request #2962 from maxfb87/fix_bug
Fix minor bug
2023-04-11 01:23:58 +05:00
Andrej 2cbbc360c0 Merge branch 'v0.7.0' into fix_bug 2023-04-11 01:23:27 +05:00
Thomas Krijnen 8a4a42777f Property transfer ownership of schema from py to c++ upon register_schema() 2023-04-10 22:01:53 +02:00
Thomas Krijnen 352d6b5ef9 Attempt load schema from cwd (IS THIS DANGEROUS?) 2023-04-10 20:31:56 +02:00
Thomas Krijnen cc49b0efdc Don't fail on unmapped attribute types (IFC4.4) 2023-04-10 20:17:26 +02:00
Andrej730 e71ee988bf Fixed error on disabling BlenderBIM addon
Error was occuring because macros doesn't have `is_registered` attribute.

Error: Traceback (most recent call last):
  File "C:\Software\Steam\steamapps\common\Blender\3.5\scripts\modules\addon_utils.py", line 421, in disable
    mod.unregister()
  File "\scripts\addons\blenderbim\__init__.py", line 45, in unregister
    blenderbim.bim.unregister()
  File "\scripts\addons\blenderbim\bim\__init__.py", line 192, in unregister
    if cls.is_registered is not False:
AttributeError: type object 'OverrideDuplicateMoveMacro' has no attribute 'is_registered'

Still getting some other error on disabling the addon, seems to happen due ongoing drawing system restructure.
Error: Traceback (most recent call last):
  File "C:\Software\Steam\steamapps\common\Blender\3.5\scripts\modules\addon_utils.py", line 421, in disable
    mod.unregister()
  File "\addons\blenderbim\__init__.py", line 45, in unregister
    blenderbim.bim.unregister()
  File "\addons\blenderbim\bim\__init__.py", line 177, in unregister
    if bpy.data.scenes['Scene'].BIMProperties.module_visibility['drawing'].is_visible:
KeyError: 'bpy_prop_collection[key]: key "drawing" not found'
2023-04-10 18:05:40 +05:00
Dion Moult 937a5b2c2d Fix #2516. Add/build/remove/view/rename/copy sheet/drawing/schedule now all references and updates user configurable relative paths. 2023-04-10 22:30:33 +10:00
Dion Moult 92221e19c3 Fix bug where unassigning a document was too aggressive and deleted relationships where it shouldn't. 2023-04-10 22:30:33 +10:00
CyrilWaechter e5c920864c Add ifczip and ifcxml to x-ifc mime-type 2023-04-10 11:43:26 +02:00
Nathan Hild 6f63682bdf Correct tooltip messages for enabling and disabling edit buttons 2023-04-10 10:28:10 +02:00
Andrej730 7623eddcd7 Including/Excluding IfcAnnotations from drawing #2903
Including or excluding elements from the drawing using EPset_Drawing.Include/Exclude now also works for IfcAnnotation elements. Both in viewport (on "activate view") and on svg print.

Related change - annotation decorations are now hidden in viewport if their object is hidden.
2023-04-10 12:37:39 +05:00
Andrej730 d1dee1e908 Fixed errors from 9bca705 2023-04-10 12:30:51 +05:00
Andrej730 8b302f8d21 Make BIM Tool alignment buttons clickable and some hotkeys code cleanup 2023-04-10 11:22:17 +05:00
Dion Moult dfeebd6b6b Fix #2960. 2023-04-10 09:01:34 +10:00
Massimo Fabbro df53ce9f6e Better fix about cls without is_registered method 2023-04-09 22:33:37 +02:00
Massimo Fabbro 9bb2c00b6d Fix bug related to operators without is_registered method 2023-04-09 21:58:11 +02:00
Dion Moult 641253c4c6 Document list now shows description and locations instead of names as names are inherited. 2023-04-09 23:03:10 +10:00
Dion Moult 6ce49b7130 See #2516. References to titleblocks, layouts, and published sheets are now explicitly stored. Titleblock paths are now configurable. Generated filenames are sanitised. 2023-04-09 23:03:10 +10:00
Andrej730 804ad0cece Prevent flickering for SectionLevel and Grid decorators 2023-04-09 18:03:08 +05:00
Dion Moult 6c60d14c05 Fix #2959. Oversight when disabling mesh edit mode. Edit grid axes should be allowed. 2023-04-09 12:52:10 +10:00
Dion Moult def2d2bcc9 See #2516. WARNING: breaking changes! Restructure drawing system to use relative paths based on IfcDocumentReference Locations. 2023-04-08 21:59:53 +10:00
Andrej730 9bca705dc3 Solved producing orphan objects data on drawing deletion 2023-04-08 11:37:35 +05:00
Andrej730 2dcc271dff Updating sheet name from UI #2950 2023-04-07 19:06:41 +05:00
Thomas Krijnen 33415fa1af #2685 Fix AttributeError 2023-04-07 16:02:11 +02:00
Andrej730 10315e0e29 annotation and gizmo shaders rework for smoothed lines without bgl #2897
Reworked annotation and gizmo shaders to work without `bgl` and support smooth lines.

Moved LIB_GLSL and DEF_GLSL to shaders module since it's basically the same code and this way it'll be easier to keep track of it.
2023-04-07 17:36:58 +05:00
Dion Moult f321cc81c8 Minor improvement to add error message when mirror elements called without a mirror axis. 2023-04-07 20:07:38 +10:00
Bruno Perdigão 68b685f580 minor fix, sets a minimum to 'count' 2023-04-07 19:58:33 +10:00
Bruno Perdigão c87c79b937 Fix #2937. IFC Array now have the option to sync children 2023-04-07 19:58:33 +10:00
Sigma Dimensions de3dfcdc43 BBIM 4D Features to:
- Display nested task resources/task inputs of the active task
- Show list of tasks related to the current object selection
- Highlight, in the WorkSchedule panel, tasks  related to the current object selection
2023-04-07 04:33:56 +02:00
Kristoffer Andersen 657ddc0fa1 conda daily pin occt version 2023-04-06 21:38:42 +02:00
Dion Moult 1da01489f1 Fix #2935. Duplicating objects clear any parametric array relationships. 2023-04-06 21:18:36 +10:00
Massimo Fabbro 64a27614ff Fix bug #2825 2023-04-06 21:12:18 +10:00
Dion Moult 6c5acfc8bb Fix #2945. Selector should expand enumerated list properties so that you can match any of them. 2023-04-06 19:21:22 +10:00
Andrej730 884ee5b587 Warning when user loading sheets and some svgs are missing
Added this warning because missing svgs can lead to errors later on.
2023-04-06 13:51:12 +05:00
Andrej730 c2320a3c1e ChangeSheetTitleBlock operator
Added operator to change title block of currently selected sheet, it's located right after "Add sheet" button.
The way it works - you select the sheet, select the desired titleblock from dropdown menu and it overrides current titleblock from the sheet with the new one.
2023-04-06 13:51:07 +05:00
Bruno Perdigão ab23030051 ifc array modifier: get cursor position in relation to the object local axes 2023-04-06 10:45:47 +10:00
Bruno Perdigão 699c692f6c ifc array modifier: adds the option to get the dimensions from the 3d cursor 2023-04-06 10:45:47 +10:00
Bruno Perdigão 0c85e26950 ifc array modifier: bugfix, prevent divider from being zero. 2023-04-06 10:45:35 +10:00
Bruno Perdigão 2cd7e737cd ifc array modifier: adds the option to choose between "total" and "increment" as dimension input 2023-04-06 10:45:35 +10:00
Gorgious56 a1adbe2b34 You can now change the section line decorator width in the UI 2023-04-06 09:37:01 +10:00
Gorgious56 a144c2b7ea Add new shader to display the section as a thick line on the mesh 2023-04-06 09:37:01 +10:00
Gorgious56 201c056ffa Add new decorator utility to the Compare node group 2023-04-06 09:37:01 +10:00
Andrej730 2e95168610 Title case naming for operators 2023-04-05 17:49:58 +05:00
Andrej730 b18c05cbb9 Open all drawings at once with shift+click (similar to print all)
Also added filepath check to make sure all drawings were printed first.
2023-04-05 17:47:27 +05:00
Andrej730 102a4c6110 Title case naming for operators 2023-04-05 17:47:27 +05:00
Dion Moult b6c0ef3d19 Fix #2936. Unassigning a type now also unmaps representations. 2023-04-05 22:33:11 +10:00
Andrej730 ea3065dbc2 Sync viewport visibility based on EPset_Drawing/Include Exclude #2613
It's not real time, it's triggered on using "Activate view" button in "Drawings" section.
2023-04-05 16:34:43 +05:00
Gorgious56 534c3ad329 Fix temporary section cutaway socket inputs/outputs not initializing properly
Related to #2547
2023-04-05 13:30:35 +02:00
Dion Moult 90a7d807a1 Fix #2938. Unlinked objects should no longer synchronise edits after they've changed. 2023-04-05 21:07:44 +10:00
Dion Moult 4baafb5e2f See #2938. Fix issue where unlinking didn't consider other users of the object's data or material. 2023-04-05 20:58:24 +10:00
Andrej730 f911765cdb Support for SECTION_LEVEL annotations description in viewport
Example - https://i.imgur.com/ATF9Fp6.png
(similar to svg)
2023-04-05 14:44:27 +05:00
Andrej730 d0a36c79d5 Small fix 2023-04-05 13:10:41 +05:00
Andrej730 ffea5320a0 Background for Ifc Text annotations in SVG
Added option to fill background of Ifc Text annotation with white color.

To make it work need to add "fill-bg" class to EPset_Annotation - https://i.imgur.com/ig2EhkQ.png.

Result - https://i.imgur.com/mq4hzAM.png
2023-04-05 12:38:36 +05:00
Andrej730 0b54e41a40 Small editing assigned product UI fix
Previously checkmark didn't worked at all if the existing and new product were the same.
2023-04-04 19:02:07 +05:00
Andrej730 26c67f770a Sliding doors - more consistent 2d repr between door types #2919 2023-04-04 18:53:59 +05:00
Andrej730 f6623db0e6 Sliding doors - moved annotation arrow to Annotation/PLAN_VIEW #2919 2023-04-04 18:38:44 +05:00
Dion Moult d7844ee8e8 Fix #2114. Duplicate overrides are now defined as macros which means that repeating last function now works. 2023-04-04 22:01:45 +10:00
Andrej730 1c8970bbf2 removed redundant parts from generate_steel_profiles_library.py
removed never used methods and contexts
2023-04-04 14:23:21 +05:00
Dion Moult eb70849a30 Fix #2898. Explicitly check for changed active material in material UI panels. 2023-04-04 17:04:32 +10:00
Andrej730 77ce3c9067 Fixed ifc validation errors for generated openings #2925
Now ELEVATION_VIEW 3d curves converted to 2d to be extruded later on to create opening representation.
2023-04-04 11:18:29 +05:00
Dion Moult 56cd226a27 Deprecate mode subscriptiosn of profiles and walls 2023-04-04 13:59:10 +10:00
Dion Moult a1acdc0977 See #2881. Allow indices in the selector instead of relying on external evaluations. 2023-04-04 09:58:23 +10:00
Dion Moult 1a624c957b See #2916. Minor fix. 2023-04-04 09:43:58 +10:00
Andrej730 1c9eeb2aca Revert "Fixed ifc validation errors for generated openings #2925"
This reverts commit 66a53b486a.
2023-04-03 23:04:27 +05:00
Gorgious56 45f06eeea7 Interface now displays a hint when trying to edit a non-mesh-like object
& Fix case where the active object would go into edit mode even if not mesh-like when multiple objects were selected
2023-04-03 17:55:44 +02:00
Andrej730 66a53b486a Fixed ifc validation errors for generated openings #2925 2023-04-03 19:53:43 +05:00
Andrej730 fc45f5b48f Fixed ifc validation errors for furniture library #2925
Fixed bunch of validation errors with furniture library:

- IfcSpaceType PredefinedType wasn't optional
`<attribute PredefinedType: <enumeration IfcSpaceTypeEnum: (EXTERNAL, GFA, INTERNAL, NOTDEFINED, PARKING, SPACE, USERDEFINED)>> Not optional`

- some object had 0.0 depth extrusion
```
Rule IfcPositiveLengthMeasure_WR1:
    (self > 0.)
Violated by:
    (0.0 > 0.0)
```

- some orphan representations violating:
```
Rule IfcShapeModel_WR11:
    ((sizeof(self.OfProductRepresentation) == 1) ^ (sizeof(self.RepresentationMap) == 1) ^ (sizeof(ofshapeaspect) == 1))
Violated by:
    ((0 == 1 ^ 0 == 1) ^ 0 == 1)
     +  where 0 = sizeof(())
     +    where () = #9091=IfcShapeRepresentation(#15,'Body','SweptSolid',(#9090)).OfProductRepresentation
     +  and   0 = sizeof(())
     +    where () = #9091=IfcShapeRepresentation(#15,'Body','SweptSolid',(#9090)).RepresentationMap
     +  and   0 = sizeof(())
```
2023-04-03 19:53:43 +05:00
Andrej730 c7105a22e6 Fixed validation error for ELEVATION_VIEW curves #2925
Error occured because ShapeBuilder was assigning "Curve2D" representation type for elevation view curves when the correct type is "Curve3D".

It use to occur for both doors and windows created with ifc modifier.

```
Validation error text:
2023-04-03:18:21:28,879 ERROR   [rule_executor.py:154] On instance:
    #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134))
Rule IfcShapeRepresentation_CorrectItemsForType:
    (IfcShapeRepresentationTypes(self.RepresentationType,self.Items))
Violated by:
    False
     +  where False = IfcShapeRepresentationTypes('Curve2D', (#134=IfcIndexedPolyCurve(#133,(IfcLineIndex((1,2)),IfcLineIndex((2,3)),IfcLineIndex((3,4)),IfcLineIndex((4,1))),$),))
     +    where 'Curve2D' = #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134)).RepresentationType
     +    and   (#134=IfcIndexedPolyCurve(#133,(IfcLineIndex((1,2)),IfcLineIndex((2,3)),IfcLineIndex((3,4)),IfcLineIndex((4,1))),$),) = #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134)).Items
```
2023-04-03 19:53:43 +05:00
Andrej730 e1fc7cad44 Fixed all unit.add_si_unit uses after fd87747
Now they were causing `TypeError: Usecase.__init__() got an unexpected keyword argument 'name'`
2023-04-03 19:53:42 +05:00
Andrej730 afeeaeb984 Fixed some validation errors for doors and windows #2925
Fixed the ones caused by using 3d curves as inner/outer curves in profiles.
2023-04-03 19:53:42 +05:00
Andrej730 2bc60b87f2 Fixed issue with tool.Blender.apply_bmesh 2023-04-03 19:53:42 +05:00
Dion Moult 4ad9ecb7db FixRevitTINs patch recipe now also purges non up-facing polygons as Revit doesn't handle them very well. 2023-04-03 21:28:34 +10:00
Dion Moult 3ec7b82b85 Fix #2930. Do not require entity applicability in IfcTester. 2023-04-03 21:28:00 +10:00
Dion Moult 48666def6b Fix #2910. Bug where style colours were not synced prior to duplication. 2023-04-03 20:16:43 +10:00
Chun 5732921fb6 a simplifier workflow and a more consistent coding style 2023-04-03 19:50:53 +10:00
Chun b86c11e1dc Unify variable 2023-04-03 19:50:53 +10:00
Chun 1bae4b219a Update functions of get_connected_to and get_connected_from compatible with IFC4 and IFC2X3 2023-04-03 19:50:53 +10:00
Chun 70ff73f805 Add IFC4 support for functions get_connected_to and get_connected_from in util.system 2023-04-03 19:50:53 +10:00
Chun a9636c5038 Add IFC4 support to get_connected_to and get_connected_from functions 2023-04-03 19:50:53 +10:00
Dion Moult a406b9f7ef Fix #2928. 2023-04-03 18:12:32 +10:00
Dion Moult f291345111 Fix #2924. IfcElements default to a null predefined type so they can inherit where possible. 2023-04-03 13:54:25 +10:00
Dion Moult c947ccb8be Fix #2926. Connections removed when rotating 90 deg to prevent unintuitive behaviour. Layer2 are also now recalculated. 2023-04-03 13:43:42 +10:00
Dion Moult 28c6b0d4bb Link to IfcArchitect tutorial series for quickstart video tutorials 2023-04-03 12:35:10 +10:00
Dion Moult 19317a9cc2 See #2881. Add support for evaluated commands in smart text literals 2023-04-02 21:25:06 +10:00
Dion Moult d67da6425e Fix #2927. Continuing to make mesh editing stricter. 2023-04-02 15:02:43 +10:00
Dion Moult 901f41358f Shortlist which IFC geometries may be treated as a meshlike object. 2023-04-02 13:45:05 +10:00
Dion Moult 061922aea2 Minor fix 2023-04-01 22:19:29 +11:00
Massimo Fabbro 5f7b6a5498 fix qto ui bug where objects with no assigned base qto doesn't allow to show the qto panel 2023-04-01 21:59:53 +11:00
Dion Moult 2ef9d5eed9 Fix #2922. You can now edit extrusion profiles of swept solids. 2023-04-01 21:27:20 +11:00
Dion Moult 9fba57d5db Disable edit mode on non mesh-like representations as well to prevent risk of invalid geometry. 2023-04-01 21:25:45 +11:00
Dion Moult e9074e3b3f Fix bug where generated openings invalidly used 3 dimensions instead of 2 2023-04-01 18:14:54 +11:00
Andrej730 f053ba714f Fixed bug in tool.Blender.get_bmesh_for_mesh
It caused errors because it was expecting mesh object to be selected - when it's not necessary that active_object is the one that mesh belongs to and sometimes no objects can be selected at all.
2023-04-01 12:12:34 +05:00
Andrej730 b6598f09a3 Fixed error with switching from EDIT mode for annotation meshes
Error occured when you were editing annotation mesh and tried to switched from EDIT mode

Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\geometry\operator.py", line 838, in invoke
    elif element.HasOpenings:
  File "\blenderbim\libs\site\packages\ifcopenshell\entity_instance.py", line 153, in __getattr__
    raise AttributeError(
AttributeError: entity instance of type 'IFC4.IfcAnnotation' has no attribute 'HasOpenings'
2023-04-01 12:00:07 +05:00
Andrej730 b9d7b95304 Revert part of 3a3d96be0 #2897
Reverted part of the bgl to gpu transition (to not scare away people with annotations without antialiasing🙂) until I work out the complete solution.
2023-03-31 19:56:26 +05:00
Andrej730 3db540f786 Fixed another typo related to #2889... 2023-03-31 19:50:22 +05:00
Andrej730 071b99c6c6 Fixed crash in Blender 3.5 on creating fill area annotation
Crash was caused by some change in Blender Python API in 3.5.
The code below resulted in `ValueError: bpy_struct: item.attr = val: MeshPolygon.vertices: array length cannot be changed to 4 (expected 0)`

```
obj.data.polygons.add(1)
p1 = obj.data.polygons[-1]
p1.vertices = (v1.index, v2.index, v3.index, v4.index)
```
2023-03-31 17:52:07 +05:00
Andrej730 3a3d96be0a bgl to gpu: profiles and openings decorations #2897
Still need to fix shaders.py and decoration.py
2023-03-31 16:38:24 +05:00
Andrej730 a03fbab4d5 Added description to some annotation operators 2023-03-31 11:06:21 +05:00
Andrej730 cc3b119288 Automatically adjusted STAIR_ARROWs now stop at the top thread
Previously they were ending at the end of the stair object and problem was that the end of the object could be at the slab underneath the stair.

Example - https://i.imgur.com/Y43v0GM.png
Before - https://i.imgur.com/Ze9DoK2.png
2023-03-31 10:59:39 +05:00
Andrej730 1a750d1b85 STAIR_ARROW annotations attached to IfcStairFlight object now readjusted after changes in IFC Stair modifier
Later probably will also need some other way to readjust annotations - so they can be readjusted for custom stairs, not created with modifier.
2023-03-31 10:59:39 +05:00
Andrej730 5b3ae9b9fd Clever STAIR_ARROW annotation
Now adding STAIR_ARROW annotation is a bit more clever - if you have IfcStairFlight selected when adding this type of annotation it will automatically:
1) define stair as it's parent;
2) add the stair as it's product;
3) place itself along the stair (it's assumed that stair direction is X+)

Example - https://i.imgur.com/Ze9DoK2.png
2023-03-30 18:41:55 +05:00
Andrej730 81471d80cd Fixed bug causing errors in IFC4X3 projects
It was causing an error below on opening BIM Tool and probably in some other places.

```
Traceback (most recent call last):
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_view3d.py", line 35, in draw
    self.draw_tool_settings(context)
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_view3d.py", line 48, in draw_tool_settings
    tool = ToolSelectPanelHelper.draw_active_tool_header(
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_toolsystem_common.py", line 809, in draw_active_tool_header
    draw_settings(context, layout, tool)
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 67, in draw_settings
    BimToolUI.draw(context, layout)
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 89, in draw
    AuthoringData.load()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\data.py", line 52, in load
    cls.load_ifc_classes()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\data.py", line 109, in load_ifc_classes
    cls.data["ifc_classes"] = cls.ifc_classes()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\data.py", line 196, in ifc_classes
    + tool.Ifc.get().by_type("IfcDoorStyle")
  File "\Blender\3.5\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\file.py", line 364, in by_type
    return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)]
  File "\Blender\3.5\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\ifcopenshell_wrapper.py", line 4499, in by_type
    return _ifcopenshell_wrapper.file_by_type(self, *args)
RuntimeError: Entity with name 'IfcDoorStyle' not found in schema 'IFC4X3'
Traceback (most recent call last):
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_view3d.py", line 35, in draw
    self.draw_tool_settings(context)
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_view3d.py", line 48, in draw_tool_settings
    tool = ToolSelectPanelHelper.draw_active_tool_header(
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_toolsystem_common.py", line 809, in draw_active_tool_header
    draw_settings(context, layout, tool)
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 67, in draw_settings
    BimToolUI.draw(context, layout)
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 92, in draw
    cls.draw_header_interface()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 351, in draw_header_interface
    cls.draw_type_selection_interface()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 367, in draw_type_selection_interface
    if AuthoringData.data["ifc_classes"]:
KeyError: 'ifc_classes'
```
2023-03-30 17:12:00 +05:00
Andrej730 eb14db4deb Fixed typo in ifcopenshell.util.type 2023-03-30 16:42:25 +05:00
Andrej730 50d989363a Added arrow to 2d repr of sliding doors (#2919)
Example - https://i.imgur.com/eJnhDXU.png
2023-03-30 16:36:55 +05:00
Dion Moult e52a99bd71 Load all type products by default (in preparation for IfcAnnotationType support) 2023-03-30 21:26:16 +11:00
Andrej730 a7a7c86657 IFC Door Modifier - sliding doors
Added support for sliding doors in IFC Door Modifier.

The main differences are 2d representation (https://i.imgur.com/VyEH569.png) and the door panel being placed before the lining, not after
2023-03-30 12:41:34 +05:00
268 changed files with 38675 additions and 30304 deletions
@@ -4,6 +4,7 @@ on:
push:
paths:
- .github/workflows/ci-ifcopenshell-conda-daily.yml
- conda/**
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
@@ -53,11 +54,13 @@ jobs:
sudo mv -v MacOSX10.13.sdk /opt/ && \
ls /opt/
- uses: seanmiddleditch/gha-setup-ninja@master
- uses: conda-incubator/setup-miniconda@v2 # https://github.com/conda-incubator/setup-miniconda
- uses: mamba-org/provision-with-micromamba@main # https://github.com/mamba-org/provision-with-micromamba
with:
activate-environment: conda-build
python-version: ${{ matrix.pyver.distver }}
channels: conda-forge
channel-priority: strict
environment-file: conda/environment.yml
extra-specs: |
python=${{ matrix.pyver.distver }}
- name: build, test and upload ifcopenshell
if: ${{ matrix.platform.upload == 'true' }}
run: |
+57
View File
@@ -0,0 +1,57 @@
name: ci-ifctester-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
push:
paths:
- '.github/workflows/ci-ifctester-pypi.yml'
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
- name: Compile
run: |
pip install build
cd src/ifctester &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifctester/dist
+2
View File
@@ -86,6 +86,8 @@ src/blenderbim/blenderbim/libs
# blenderbim test temp files
src/blenderbim/test/files/temp
src/blenderbim/drawings
src/blenderbim/layouts
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
+1 -2
View File
@@ -10,5 +10,4 @@ dependencies:
- ripgrep
- pip
- pip:
- --extra-index-url https://lief.s3-website.fr-par.scw.cloud/latest
- lief==0.13.0.dev0
- lief==0.13
+4 -2
View File
@@ -24,7 +24,7 @@ requirements:
host:
- python
- boost-cpp
- occt
- occt ==7.7.0
- libxml2
- cgal-cpp
- hdf5
@@ -32,11 +32,12 @@ requirements:
- gmp # [unix]
- mpir # [win]
- nlohmann_json
- zlib
run:
- python
- boost-cpp
- occt
- occt ==7.7.0
- libxml2
- cgal-cpp
- hdf5
@@ -44,6 +45,7 @@ requirements:
- gmp # [unix]
- mpir # [win]
- nlohmann_json
- zlib
test:
imports:
+4
View File
@@ -59,6 +59,10 @@ import multiprocessing
import platform
import sysconfig
# @todo temporary for expired mpfr.org certificate on 2023-04-08
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
from urllib.request import urlretrieve
+12
View File
@@ -236,6 +236,11 @@ endif
cp -r dist/working/IfcOpenShell-0.7.0/src/ifc5d/ifc5d dist/blenderbim/libs/site/packages/
# Provides IFCCityJSON functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccityjson dist/blenderbim/libs/site/packages/
# Provides IfcGit functionality
cd dist/working && . env/bin/activate && $(PIP) install GitPython --target=./site-packages
cp -r dist/working/site-packages/smmap dist/blenderbim/libs/site/packages/
cp -r dist/working/site-packages/gitdb dist/blenderbim/libs/site/packages/
cp -r dist/working/site-packages/git dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides Mustache templating in construction documentation
@@ -659,6 +664,13 @@ endif
cp -r dist/working/bpypolyskel-master/bpypolyskel dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required for Desktop icon and file association
cp -r blenderbim/libs/desktop dist/blenderbim/libs/
# Remove dependencies also bundled with Blender
rm -rf dist/blenderbim/libs/site/packages/numpy
rm -rf dist/blenderbim/libs/site/packages/numpy.libs
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
rm -rf dist/blenderbim
+2 -1
View File
@@ -34,7 +34,8 @@ bl_info = {
if sys.modules.get("bpy", None):
# Process *.pth in /libs/site/packages to setup globally importable modules
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
import blenderbim.bim
+9 -1
View File
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim
import importlib
from . import handler, ui, prop, operator, helper
@@ -70,6 +71,7 @@ modules = {
"covetool": None,
"augin": None,
"debug": None,
"ifcgit": None,
# Uncomment this line to enable loading of the demo module. Happy hacking!
# The name "demo" must correlate to a folder name in `bim/module/`.
# "demo": None,
@@ -93,6 +95,8 @@ classes = [
operator.SelectIfcFile,
operator.ReloadSelectedIfcFile,
operator.SelectSchemaDir,
operator.FileAssociate,
operator.FileUnassociate,
operator.SelectURIAttribute,
operator.EditBlenderCollection,
operator.BIM_OT_open_webbrowser,
@@ -171,7 +175,11 @@ def register():
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
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_post.remove(handler.ensureIfcExported)
@@ -22,6 +22,7 @@
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; }
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
.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; }
@@ -96,4 +96,12 @@
<path d="M 7.5 10 L 10 0 L 15 10" class="annotation" style="stroke-width:1; fill: white;" />
</g>
</marker>
<filter x="0" y="0" width="1" height="1" id="fill-background">
<feFlood flood-color="white" result="bg" />
<feMerge>
<feMergeNode in="bg"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -17,16 +17,49 @@
<line x1="0" y1="0" x2="0" y2="3" style="stroke:black; stroke-width:0.25" />
</pattern>
<pattern id="square1" width="1" height="1" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="0" y2="1" style="stroke:black; stroke-width:0.25" />
<line x1="0" y1="0" x2="1" y2="0" style="stroke:black; stroke-width:0.25" />
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 1 0 1 1" />
</pattern>
<pattern id="square2" width="2" height="2" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="0" y2="2" style="stroke:black; stroke-width:0.25" />
<line x1="0" y1="0" x2="2" y2="0" style="stroke:black; stroke-width:0.25" />
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 2 0 2 2" />
</pattern>
<pattern id="square3" width="3" height="3" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="0" y2="3" style="stroke:black; stroke-width:0.25" />
<line x1="0" y1="0" x2="3" y2="0" style="stroke:black; stroke-width:0.25" />
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 3 0 3 3" />
</pattern>
<pattern id="recth1" width="2" height="1" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 2 0 2 1" />
</pattern>
<pattern id="recth2" width="4" height="2" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 4 0 4 2" />
</pattern>
<pattern id="recth3" width="6" height="3" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 6 0 6 3" />
</pattern>
<pattern id="rectv1" width="1" height="2" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 1 0 1 2" />
</pattern>
<pattern id="rectv2" width="2" height="4" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 2 0 2 4" />
</pattern>
<pattern id="rectv3" width="3" height="6" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 3 0 3 6" />
</pattern>
<pattern id="tile1" width="2" height="2" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 2 0 2 1 0 1 M 1 1 1 2" />
</pattern>
<pattern id="tile2" width="4" height="4" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 4 0 4 2 0 2 M 2 2 2 4" />
</pattern>
<pattern id="tile3" width="6" height="6" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 6 0 6 3 0 3 M 3 3 3 6" />
</pattern>
<pattern id="board1" width="4" height="2" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 4 0 4 1 0 1 M 2 1 2 2" />
</pattern>
<pattern id="board2" width="8" height="4" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 8 0 8 2 0 2 M 4 2 4 4" />
</pattern>
<pattern id="board3" width="12" height="6" patternUnits="userSpaceOnUse">
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 12 0 12 3 0 3 M 6 3 6 6" />
</pattern>
<pattern id="crosshatch1" width="1" height="1" patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="0" y2="1" style="stroke:black; stroke-width:0.25" />

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 69 KiB

@@ -0,0 +1,125 @@
{
"Technical": {
"raster_style": {
"bpy.data.worlds[0].color": [
1,
1,
1
],
"scene.render.engine": "BLENDER_WORKBENCH",
"scene.render.film_transparent": false,
"scene.display.shading.show_object_outline": true,
"scene.display.shading.show_cavity": false,
"scene.display.shading.cavity_type": "BOTH",
"scene.display.shading.curvature_ridge_factor": 1,
"scene.display.shading.curvature_valley_factor": 1,
"scene.view_settings.view_transform": "Standard",
"scene.display.shading.light": "FLAT",
"scene.display.shading.color_type": "SINGLE",
"scene.display.shading.single_color": [
1,
1,
1
],
"scene.display.shading.show_shadows": false,
"scene.display.shading.shadow_intensity": 0.5,
"scene.display.light_direction": [
0.5,
0.5,
0.5
],
"scene.view_settings.use_curve_mapping": false,
"space.overlay.show_wireframes": true,
"space.overlay.wireframe_threshold": 0,
"space.overlay.show_floor": false,
"space.overlay.show_axis_x": false,
"space.overlay.show_axis_y": false,
"space.overlay.show_axis_z": false,
"space.overlay.show_object_origins": false,
"space.overlay.show_relationship_lines": false
},
"render_type": "VIEWPORT"
},
"Shaded": {
"raster_style": {
"bpy.data.worlds[0].color": [
1,
1,
1
],
"scene.render.engine": "BLENDER_WORKBENCH",
"scene.render.film_transparent": false,
"scene.display.shading.show_object_outline": true,
"scene.display.shading.show_cavity": true,
"scene.display.shading.cavity_type": "BOTH",
"scene.display.shading.curvature_ridge_factor": 1,
"scene.display.shading.curvature_valley_factor": 1,
"scene.view_settings.view_transform": "Standard",
"scene.display.shading.light": "STUDIO",
"scene.display.shading.color_type": "MATERIAL",
"scene.display.shading.single_color": [
1,
1,
1
],
"scene.display.shading.show_shadows": true,
"scene.display.shading.shadow_intensity": 0.5,
"scene.display.light_direction": [
0.5,
0.5,
0.5
],
"scene.view_settings.use_curve_mapping": false,
"space.overlay.show_wireframes": false,
"space.overlay.wireframe_threshold": 0,
"space.overlay.show_floor": false,
"space.overlay.show_axis_x": false,
"space.overlay.show_axis_y": false,
"space.overlay.show_axis_z": false,
"space.overlay.show_object_origins": false,
"space.overlay.show_relationship_lines": false
},
"render_type": "VIEWPORT"
},
"Blender Default": {
"raster_style": {
"bpy.data.worlds[0].color": [
0.05087608844041824,
0.05087608844041824,
0.05087608844041824
],
"scene.render.engine": "BLENDER_EEVEE",
"scene.render.film_transparent": false,
"scene.view_settings.view_transform": "Filmic",
"scene.display.shading.show_object_outline": false,
"scene.display.shading.show_cavity": false,
"scene.display.shading.cavity_type": "WORLD",
"scene.display.shading.curvature_ridge_factor": 0.0,
"scene.display.shading.curvature_valley_factor": 0.0,
"scene.display.shading.light": "STUDIO",
"scene.display.shading.color_type": "MATERIAL",
"scene.display.shading.single_color": [
0.800000011920929,
0.800000011920929,
0.800000011920929
],
"scene.display.shading.show_shadows": false,
"scene.display.shading.shadow_intensity": 0.5,
"scene.display.light_direction": [
0.5773502588272095,
0.5773502588272095,
0.5773502588272095
],
"scene.view_settings.use_curve_mapping": false,
"space.overlay.show_wireframes": false,
"space.overlay.wireframe_threshold": 1.0,
"space.overlay.show_floor": true,
"space.overlay.show_axis_x": true,
"space.overlay.show_axis_y": true,
"space.overlay.show_axis_z": false,
"space.overlay.show_object_origins": true,
"space.overlay.show_relationship_lines": true
},
"render_type": "VIEWPORT"
}
}
@@ -0,0 +1,44 @@
<?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">
<g id="rectangle-tag">
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
</g>
<g id="triangle-tag">
<path style="fill: white; stroke: black; stroke-width: 0.25;" d="M 3.1409425e-7,-7.1069207 5.7650623,2.8784603 l -11.5301246,-6e-7 z" />
</g>
<g id="hexagon-tag">
<path style="fill: white; stroke: black; stroke-width: 0.25;" d="m 0.0161133,-6.0583333 5.2339915,3.8366023 -2.031433,6.1633977 c 0,0 -6.4894866,-0.027414 -6.4894866,-0.027414 l -1.97929,-6.1803395 5.2662181,-3.7922467" />
</g>
<g id="capsule-tag">
<rect x="-6" y="-2.5" width="12" height="5" rx="2.5" ry="2.5" fill="white" stroke="black" style="stroke-width: 0.25;" />
</g>
<g id="circle-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
</g>
<g id="door-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<text y="-1.75" class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<line x1="-5" x2="5" style="stroke: black; stroke-width: 0.25;" />
<text y="2.5" class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
</g>
<g id="space-tag">
<text y="-5" class="large" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<text class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<text y="5" class="regular" 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" />
</g>
<g id="elevation-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
</g>
<g id="section-arrow">
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
</g>
<g id="section-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -1,67 +1,55 @@
<link href="jsgantt.css" rel="stylesheet" type="text/css"/>
<link href="../../module/sequence/gantt/main.css" rel="stylesheet" type="text/css"/>
<script src="jsgantt.js" type="text/javascript"></script>
<style>
.gtaskcellwkend,
.gtaskcellcurrent,
.gminorheadingwkend {
background-color: #e1e1e1;
}
.gitemhighlight td {
background-color: #ffdaaa;
}
.gtaskblue {
background: #4281A4;
}
.gtaskred {
background: #C1666B;
}
.gtaskgreen {
background: #48A9A6;
}
.gtaskyellow {
background: #D4B483;
}
.gmainleft {
overflow: visible;
flex: 0 1 auto;
}
</style>
<a href="#print" id="print">Print Mode</a>
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
<script type="text/javascript">
var g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day');
g.setOptions({
vCaptionType: 'Caption', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
vQuarterColWidth: 36,
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
vWeekMinorDateDisplayFormat: 'dd mon', // Set format to display dates in the "Minor" header of the "Week" view
vLang: 'en',
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
vShowRes: false, // Disable the resource column.
vShowComp: false, // Disable the completion column.
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
vAdditionalHeaders: {ifcduration: {title: 'Duration'}},
vUseToolTip: false, // Disable tooltips.
vTotalHeight: 900,
});
document.getElementById('print').addEventListener('click', function() {
g.setTotalHeight("");
g.Draw();
});
var json_data = `
{{{json_data}}}
`;
JSGantt.parseJSONString(json_data, g);
g.Draw();
<script type="text/javascript" src="../../module/sequence/gantt/index.js"></script>
<script>
var json_data = `{{{json_data}}}`;
</script>
<div id="options" class="no-print">
<h4>Choose a language:
<select id="lang" class="sweet_button" onchange="set_language(event)">
<option value='cn'>Chinese (cn)</option>
<option value='cs'>Czech (cs)</option>
<option value='nl'>Dutch (Standard)</option>
<option value='en' selected>English (en)</option>
<option value='fr'>French (fr)</option>
<option value='de'>German (de)</option>
<option value='hu'>Hungarian (hu)</option>
<option value='id'>Indonesian (id)</option>
<option value='it'>Italian (it)</option>
<option value='ja'>Japanese (ja)</option>
<option value='pt'>Portuguese (pt)</option>
<option value='ru'>Russian (ru)</option>
<option value='es'>Spanish (es)</option>
<option value='sv'>Swedish (sv)</option>
<option value='tr'>Turkish (tr)</option>
</select>
</h4>
<br>
</div>
<div class="top-right no-print" id="print_options">
<select class="sweet_button" id="print_page_size">
<option value="210,297">A4 Portrait</option>
<option value="297,210">A4 Landscape</option>
<option value="297,420">A3 Portrait</option>
<option value="420,297">A3 Landscape</option>
<option value="420,594">A2 Portrait</option>
<option value="594,420">A2 Landscape</option>
<option value="594,841">A1 Portrait</option>
<option value="841,594">A1 Landscape</option>
<option value="841,1189">A0 Portrait</option>
<option value="1189,841">A0 Landscape</option>
</select>
</div>
<div id="schedule-data" class="no-print"></div>
<div id="schedule-header" style=""></div>
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
<script>
var data = `{{{data}}}`;
setupPage(data);
create_gantt_chart(json_data)
</script>
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,20 +5,26 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#6=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#14=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#15=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#16=IFCSIMPLEPROPERTYTEMPLATE('1$xfo9EVb26QLqmPll2_RK',$,'MetricPrecision','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
#17=IFCSIMPLEPROPERTYTEMPLATE('38uAtrp9nD_901NO42zd$7',$,'ImperialPrecision','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#18=IFCSIMPLEPROPERTYTEMPLATE('1MX0uffTL6TOvtvEJpxFmk',$,'DecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#19=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#20=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#21=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -5,18 +5,34 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#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));
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#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));
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#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.);
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#15=IFCSIMPLEPROPERTYTEMPLATE('0ulAhgk3v9qfGlDILauR6J',$,'SheetsDir','Default sheets directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#16=IFCSIMPLEPROPERTYTEMPLATE('2yvlVKiQXASucfH40deCvu',$,'LayoutsDir','Default layouts directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#17=IFCSIMPLEPROPERTYTEMPLATE('2kXZqXicL3jRwsOnLM_0ho',$,'TitleblocksDir','Default titleblocks directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#18=IFCSIMPLEPROPERTYTEMPLATE('1lmOfo9Bf9beMBJkCZKf5Y',$,'DrawingsDir','Default drawings directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#19=IFCSIMPLEPROPERTYTEMPLATE('30cOKpb4D9CADKM04$N9X3',$,'StylesheetPath','Default stylesheet CSS',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#20=IFCSIMPLEPROPERTYTEMPLATE('23tejOrxj859R_eCRp1AFP',$,'MarkersPath','Default markers SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#25,#26,#27,#28));
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -1,26 +0,0 @@
<?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">
<g id="rectangle-tag">
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
</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" />
</g>
<g id="elevation-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
</g>
<g id="section-arrow">
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
</g>
<g id="section-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

+1 -19
View File
@@ -45,7 +45,6 @@ class IfcExporter:
self.set_header()
IfcStore.update_cache()
if bpy.context.scene.BIMProjectProperties.is_authoring:
self.sync_deletions()
self.sync_all_objects()
self.sync_edited_objects()
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
@@ -87,19 +86,6 @@ class IfcExporter:
self.get_application_name(), self.get_application_version()
)
def sync_deletions(self):
results = []
for ifc_definition_id in IfcStore.deleted_ids:
try:
product = self.file.by_id(ifc_definition_id)
if hasattr(product, "GlobalId"):
results.append(product.GlobalId)
except:
continue
ifcopenshell.api.run("root.remove_product", self.file, **{"product": product})
IfcStore.deleted_ids.clear()
return results
def sync_all_objects(self):
results = []
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -126,7 +112,7 @@ class IfcExporter:
continue
try:
if isinstance(obj, bpy.types.Material):
if self.has_changed_shading(obj):
if tool.Ifc.has_changed_shading(obj):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj)
else:
element = tool.Ifc.get_entity(obj)
@@ -149,10 +135,6 @@ 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):
+54 -73
View File
@@ -27,6 +27,8 @@ from bpy.app.handlers import persistent
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation
from blenderbim.bim.module.model.data import AuthoringData
from mathutils import Vector
from math import cos, degrees
global_subscription_owner = object()
@@ -99,22 +101,69 @@ def color_callback(obj, data):
def active_object_callback():
refresh_ui_data()
update_bim_tool_props()
def update_bim_tool_props():
"""update BIM Tools props (such as extrusion_depth, length and x_angle) when active object changes"""
obj = bpy.context.active_object
# bunch of checks to see if we're in a valid state
if not obj:
return
mode = bpy.context.mode
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode).idname
if current_tool != "bim.bim_tool":
return
element = tool.Ifc.get_entity(obj)
if not element:
return
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
def get_x_angle(extrusion):
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
return x_angle
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props = bpy.context.scene.BIMModelProperties
if not AuthoringData.is_loaded:
AuthoringData.load()
if AuthoringData.data["active_material_usage"] == "LAYER2":
x_angle = get_x_angle(extrusion)
axis = tool.Model.get_wall_axis(obj)["reference"]
props.extrusion_depth = extrusion.Depth * si_conversion * cos(x_angle)
props.length = (axis[1] - axis[0]).length
props.x_angle = x_angle
elif AuthoringData.data["active_material_usage"] == "LAYER3":
x_angle = get_x_angle(extrusion)
props.x_angle = x_angle
elif AuthoringData.data["active_material_usage"] == "PROFILE":
props.extrusion_depth = extrusion.Depth * si_conversion
def active_material_index_callback(obj, data):
refresh_ui_data()
def subscribe_to(object, data_path, callback):
def subscribe_to(obj, data_path, callback):
try:
subscribe_to = object.path_resolve(data_path, False)
subscribe_to = obj.path_resolve(data_path, False)
except:
return
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=object,
owner=obj,
args=(
object,
obj,
data_path,
),
notify=callback,
@@ -233,72 +282,4 @@ def setDefaultProperties(scene):
)
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
ifcopenshell.api.owner.settings.get_application = get_application
# TODO: Move to drawing module
if len(bpy.context.scene.DocProperties.drawing_styles) == 0:
drawing_style = bpy.context.scene.DocProperties.drawing_styles.add()
drawing_style.name = "Technical"
drawing_style.render_type = "VIEWPORT"
drawing_style.raster_style = json.dumps(
{
RasterStyleProperty.WORLD_COLOR.value: (1, 1, 1),
RasterStyleProperty.RENDER_ENGINE.value: "BLENDER_WORKBENCH",
RasterStyleProperty.RENDER_TRANSPARENT.value: False,
RasterStyleProperty.SHADING_SHOW_OBJECT_OUTLINE.value: True,
RasterStyleProperty.SHADING_SHOW_CAVITY.value: False,
RasterStyleProperty.SHADING_CAVITY_TYPE.value: "BOTH",
RasterStyleProperty.SHADING_CURVATURE_RIDGE_FACTOR.value: 1,
RasterStyleProperty.SHADING_CURVATURE_VALLEY_FACTOR.value: 1,
RasterStyleProperty.VIEW_TRANSFORM.value: "Standard",
RasterStyleProperty.SHADING_LIGHT.value: "FLAT",
RasterStyleProperty.SHADING_COLOR_TYPE.value: "SINGLE",
RasterStyleProperty.SHADING_SINGLE_COLOR.value: (1, 1, 1),
RasterStyleProperty.SHADING_SHOW_SHADOWS.value: False,
RasterStyleProperty.SHADING_SHADOW_INTENSITY.value: 0.5,
RasterStyleProperty.DISPLAY_LIGHT_DIRECTION.value: (0.5, 0.5, 0.5),
RasterStyleProperty.VIEW_USE_CURVE_MAPPING.value: False,
RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: True,
RasterStyleProperty.OVERLAY_WIREFRAME_THRESHOLD.value: 0,
RasterStyleProperty.OVERLAY_SHOW_FLOOR.value: False,
RasterStyleProperty.OVERLAY_SHOW_AXIS_X.value: False,
RasterStyleProperty.OVERLAY_SHOW_AXIS_Y.value: False,
RasterStyleProperty.OVERLAY_SHOW_AXIS_Z.value: False,
RasterStyleProperty.OVERLAY_SHOW_OBJECT_ORIGINS.value: False,
RasterStyleProperty.OVERLAY_SHOW_RELATIONSHIP_LINES.value: False,
}
)
drawing_style = bpy.context.scene.DocProperties.drawing_styles.add()
drawing_style.name = "Shaded"
drawing_style.render_type = "VIEWPORT"
drawing_style.raster_style = json.dumps(
{
RasterStyleProperty.WORLD_COLOR.value: (1, 1, 1),
RasterStyleProperty.RENDER_ENGINE.value: "BLENDER_WORKBENCH",
RasterStyleProperty.RENDER_TRANSPARENT.value: False,
RasterStyleProperty.SHADING_SHOW_OBJECT_OUTLINE.value: True,
RasterStyleProperty.SHADING_SHOW_CAVITY.value: True,
RasterStyleProperty.SHADING_CAVITY_TYPE.value: "BOTH",
RasterStyleProperty.SHADING_CURVATURE_RIDGE_FACTOR.value: 1,
RasterStyleProperty.SHADING_CURVATURE_VALLEY_FACTOR.value: 1,
RasterStyleProperty.VIEW_TRANSFORM.value: "Standard",
RasterStyleProperty.SHADING_LIGHT.value: "STUDIO",
RasterStyleProperty.SHADING_COLOR_TYPE.value: "MATERIAL",
RasterStyleProperty.SHADING_SINGLE_COLOR.value: (1, 1, 1),
RasterStyleProperty.SHADING_SHOW_SHADOWS.value: True,
RasterStyleProperty.SHADING_SHADOW_INTENSITY.value: 0.5,
RasterStyleProperty.DISPLAY_LIGHT_DIRECTION.value: (0.5, 0.5, 0.5),
RasterStyleProperty.VIEW_USE_CURVE_MAPPING.value: False,
RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: False,
RasterStyleProperty.OVERLAY_WIREFRAME_THRESHOLD.value: 0,
RasterStyleProperty.OVERLAY_SHOW_FLOOR.value: False,
RasterStyleProperty.OVERLAY_SHOW_AXIS_X.value: False,
RasterStyleProperty.OVERLAY_SHOW_AXIS_Y.value: False,
RasterStyleProperty.OVERLAY_SHOW_AXIS_Z.value: False,
RasterStyleProperty.OVERLAY_SHOW_OBJECT_ORIGINS.value: False,
RasterStyleProperty.OVERLAY_SHOW_RELATIONSHIP_LINES.value: False,
}
)
drawing_style = bpy.context.scene.DocProperties.drawing_styles.add()
drawing_style.name = "Blender Default"
drawing_style.render_type = "DEFAULT"
bpy.ops.bim.save_drawing_style(index="2")
AuthoringData.type_thumbnails = {}
AuthoringData.type_thumbnails = {}
+2 -20
View File
@@ -24,6 +24,7 @@ import zipfile
import tempfile
import ifcopenshell
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
@@ -35,7 +36,6 @@ class IfcStore:
cache_path = None
id_map = {}
guid_map = {}
deleted_ids = set()
edited_objs = set()
pset_template_path = ""
pset_template_file = None
@@ -62,7 +62,6 @@ class IfcStore:
IfcStore.cache_path = None
IfcStore.id_map = {}
IfcStore.guid_map = {}
IfcStore.deleted_ids = set()
IfcStore.edited_objs = set()
IfcStore.pset_template_path = ""
IfcStore.pset_template_file = None
@@ -277,23 +276,6 @@ class IfcStore:
data = {"id": element.id(), "obj": obj.name}
IfcStore.commit_link_element(data)
@staticmethod
def delete_element(element):
IfcStore.deleted_ids.add(element.id())
if IfcStore.history:
data = {"id": element.id()}
IfcStore.history[-1]["operations"].append(
{"rollback": IfcStore.rollback_delete_element, "commit": IfcStore.commit_delete_element, "data": data}
)
@staticmethod
def rollback_delete_element(data):
IfcStore.deleted_ids.remove(data["id"])
@staticmethod
def commit_delete_element(data):
IfcStore.deleted_ids.add(data["id"])
@staticmethod
def link_element(element, obj):
existing_obj = IfcStore.id_map.get(element.id(), None)
@@ -371,7 +353,7 @@ class IfcStore:
def unlink_element(element=None, obj=None):
if element is None:
try:
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
element = tool.Ifc.get_entity(obj)
except:
pass
+52 -137
View File
@@ -36,7 +36,7 @@ import ifcopenshell.util.geolocation
import blenderbim.tool as tool
from itertools import chain, accumulate
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.drawing.prop import get_diagram_scales
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
class FileCopy(threading.Thread):
@@ -60,7 +60,6 @@ class MaterialCreator:
def create(self, element, obj, mesh):
self.mesh = mesh
self.obj = obj
if (hasattr(element, "Representation") and not element.Representation) or (
hasattr(element, "RepresentationMaps") and not element.RepresentationMaps
):
@@ -140,11 +139,11 @@ class MaterialCreator:
def assign_material_slots_to_faces(self):
if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]:
return
if len(self.obj.material_slots) == 1:
if len(self.mesh.materials) == 1:
return
material_to_slot = {}
for i, material in enumerate(self.mesh["ios_materials"]):
slot_index = self.obj.material_slots.find(self.styles[material].name)
slot_index = self.mesh.materials.find(self.styles[material].name)
material_to_slot[i] = slot_index
if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
@@ -179,6 +178,11 @@ class IfcImporter:
self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
self.settings.set(self.settings.STRICT_TOLERANCE, True)
self.settings_curve = ifcopenshell.geom.settings()
self.settings_curve.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
self.settings_curve.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
self.settings_curve.set(self.settings_curve.STRICT_TOLERANCE, True)
self.settings_curve.set(self.settings_curve.INCLUDE_CURVES, True)
self.settings_native = ifcopenshell.geom.settings()
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
self.settings_2d = ifcopenshell.geom.settings()
@@ -242,6 +246,8 @@ class IfcImporter:
self.profile_code("Create native elements")
self.create_elements()
self.profile_code("Create elements")
self.create_generic_elements(self.annotations)
self.profile_code("Create annotations")
self.create_grids()
self.profile_code("Create grids")
self.create_spatial_elements()
@@ -321,10 +327,13 @@ class IfcImporter:
if isinstance(self.elements, set):
self.elements = list(self.elements)
# TODO: enable filtering for annotations
self.annotations = set(self.file.by_type("IfcAnnotation"))
self.annotations = set([a for a in self.file.by_type("IfcAnnotation") if not a.HasAssignments])
else:
self.elements = self.file.by_type("IfcElement")
self.annotations = set(self.file.by_type("IfcAnnotation"))
if self.file.schema in ("IFC2X3", "IFC4"):
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
else:
self.elements = self.file.by_type("IfcElement")
self.annotations = set([a for a in self.file.by_type("IfcAnnotation") if not a.HasAssignments])
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
if self.ifc_import_settings.is_coordinating:
@@ -335,23 +344,7 @@ 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 == "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.file.by_type("IfcSpatialElementType")
)
self.element_types = set(self.file.by_type("IfcTypeProduct"))
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)
@@ -388,7 +381,7 @@ class IfcImporter:
representations = self.get_transformed_body_representations(element.Representation.Representations)
# Single swept disk solids (e.g. rebar) are better natively represented as beveled curves
if self.is_native_swept_disk_solid(representations):
if self.is_native_swept_disk_solid(element, representations):
self.native_data[element.GlobalId] = {
"representations": representations,
"representation": self.get_body_representation(element.Representation.Representations),
@@ -416,7 +409,11 @@ class IfcImporter:
}
return True
def is_native_swept_disk_solid(self, representations):
def is_native_swept_disk_solid(self, element, representations):
# detect BBIM Railings to represent them with meshes and not curves
if tool.Pset.get_element_pset(element, "BBIM_Railing"):
return False
for representation in representations:
items = representation["raw"].Items or [] # Be forgiving of invalid IFCs because Revit :(
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
@@ -695,28 +692,31 @@ class IfcImporter:
def create_generic_elements(self, elements):
# Based on my experience in viewing BIM models, representations are prioritised as follows:
# 1. 3D Body, 2. 2D Plans, 3. Point clouds, 4. No representation
# If an element has a representation that doesn't follow 1, 2, or 3, it will not show by default.
# 1. 3D Body, 2. 2D Body, 3. 2D Plans / annotations, 4. Point clouds, 5. No representation
# If an element has a representation that doesn't follow 1, 2, 3, or 4, it will not show by default.
# The user can load them later if they want to view them.
products = self.create_products(elements)
elements -= products
products = self.create_curve_products(elements)
products = self.create_products(elements, settings=self.settings_curve)
elements -= products
products = self.create_products(elements, settings=self.settings_2d)
elements -= products
products = self.create_pointclouds(elements)
elements -= products
for element in elements:
self.create_product(element)
def create_products(self, products):
def create_products(self, products, settings=None):
if settings is None:
settings = self.settings
results = set()
if not products:
return results
if self.ifc_import_settings.should_use_cpu_multiprocessing:
iterator = ifcopenshell.geom.iterator(
self.settings, self.file, multiprocessing.cpu_count(), include=products
)
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products)
else:
iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=products)
iterator = ifcopenshell.geom.iterator(settings, self.file, include=products)
if self.ifc_import_settings.should_cache:
cache = IfcStore.get_cache()
if cache:
@@ -783,10 +783,10 @@ class IfcImporter:
self.structural_collection.children.link(self.structural_connection_collection)
self.project["blender"].children.link(self.structural_collection)
self.create_curve_products(self.file.by_type("IfcStructuralCurveMember"))
self.create_curve_products(self.file.by_type("IfcStructuralCurveConnection"))
self.create_curve_products(self.file.by_type("IfcStructuralSurfaceMember"))
self.create_curve_products(self.file.by_type("IfcStructuralSurfaceConnection"))
self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_2d)
self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_2d)
self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_2d)
self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_2d)
self.create_structural_point_connections()
def create_structural_point_connections(self):
@@ -874,40 +874,6 @@ class IfcImporter:
self.link_element(product, obj)
return product
def create_curve_products(self, products):
results = set()
if not products:
return results
if self.ifc_import_settings.should_use_cpu_multiprocessing:
iterator = ifcopenshell.geom.iterator(
self.settings_2d, self.file, multiprocessing.cpu_count(), include=products
)
else:
iterator = ifcopenshell.geom.iterator(self.settings_2d, self.file, include=products)
if self.ifc_import_settings.should_cache:
cache = IfcStore.get_cache()
if cache:
iterator.set_cache(cache)
valid_file = iterator.initialize()
if not valid_file:
return results
checkpoint = time.time()
total = 0
while True:
total += 1
if total % 250 == 0:
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
checkpoint = time.time()
shape = iterator.get()
if shape:
product = self.file.by_id(shape.id)
self.create_product(product, shape)
results.add(product)
if not iterator.next():
break
print("Done creating geometry")
return results
def create_product(self, element, shape=None, mesh=None):
if element is None:
return
@@ -921,9 +887,6 @@ class IfcImporter:
if mesh:
pass
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
mesh = self.create_camera(element, shape)
tool.Loader.link_mesh(shape, mesh)
elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element) and shape:
mesh = self.create_curve(element, shape)
tool.Loader.link_mesh(shape, mesh)
@@ -1348,14 +1311,20 @@ class IfcImporter:
bpy.context.scene.unit_settings.length_unit = "FEET"
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
try:
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
try:
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
def create_project(self):
project = self.file.by_type("IfcProject")[0]
@@ -1746,14 +1715,8 @@ class IfcImporter:
bpy.context.scene.collection.objects.link(obj)
def is_curve_annotation(self, element):
return element.ObjectType in [
"DIMENSION",
"EQUAL_DIMENSION",
"PLAN_LEVEL",
"SECTION_LEVEL",
"STAIR_ARROW",
"TEXT_LEADER",
]
object_type = element.ObjectType
return object_type in ANNOTATION_TYPES_DATA and ANNOTATION_TYPES_DATA[object_type][3] == "curve"
def get_drawing_group(self, element):
for rel in element.HasAssignments or []:
@@ -1834,54 +1797,6 @@ class IfcImporter:
):
return representation.Items[0].MappingTarget
def create_camera(self, element, shape):
if hasattr(shape, "geometry"):
geometry = shape.geometry
else:
geometry = shape
v = geometry.verts
x = [v[i] for i in range(0, len(v), 3)]
y = [v[i + 1] for i in range(0, len(v), 3)]
z = [v[i + 2] for i in range(0, len(v), 3)]
width = max(x) - min(x)
height = max(y) - min(y)
depth = max(z) - min(z)
camera = bpy.data.cameras.new(tool.Loader.get_mesh_name(geometry))
camera.type = "ORTHO"
camera.ortho_scale = width if width > height else height
camera.clip_end = depth
if width > height:
camera.BIMCameraProperties.raster_x = 1000
camera.BIMCameraProperties.raster_y = round(1000 * (height / width))
else:
camera.BIMCameraProperties.raster_x = round(1000 * (width / height))
camera.BIMCameraProperties.raster_y = 1000
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("EPset_Drawing")
if pset:
if "TargetView" in pset:
camera.BIMCameraProperties.target_view = pset["TargetView"]
if "Scale" in pset:
valid_scales = [
i[0] for i in get_diagram_scales(None, bpy.context) if pset["Scale"] == i[0].split("|")[-1]
]
if valid_scales:
camera.BIMCameraProperties.diagram_scale = valid_scales[0]
else:
camera.BIMCameraProperties.diagram_scale = "CUSTOM"
camera.BIMCameraProperties.custom_diagram_scale = pset["Scale"]
if "HasUnderlay" in pset:
camera.BIMCameraProperties.has_underlay = pset["HasUnderlay"]
if "HasLinework" in pset:
camera.BIMCameraProperties.has_linework = pset["HasLinework"]
if "HasAnnotation" in pset:
camera.BIMCameraProperties.has_annotation = pset["HasAnnotation"]
return camera
def create_curve(self, element, shape):
if hasattr(shape, "geometry"):
geometry = shape.geometry
@@ -43,13 +43,17 @@ class BIM_OT_assign_object(bpy.types.Operator, Operator):
related_object: bpy.props.IntProperty()
def _execute(self, context):
core.assign_object(
tool.Ifc,
tool.Aggregate,
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
core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object)),
related_obj=obj,
)
class BIM_OT_unassign_object(bpy.types.Operator, Operator):
@@ -59,9 +59,13 @@ class MaterialAttributesData:
@classmethod
def load(cls):
cls.data = {"attributes": cls.attributes()}
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def ifc_definition_id(cls):
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
@classmethod
def attributes(cls):
results = []
@@ -50,7 +50,22 @@ class EnableEditingAttributes(bpy.types.Operator):
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
props.attributes.clear()
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes)
def callback(name, prop, data):
if name in ("RefLatitude", "RefLongitude"):
new = props.attributes.add()
new.name = name
new.is_null = data[name] is None
new.is_optional = True
new.data_type = "string"
new.ifc_class = data["type"]
new.string_value = "" if new.is_null else json.dumps(data[name])
blenderbim.bim.helper.add_attribute_description(new)
new.description += " The degrees, minutes and seconds should follow this format : [12,34,56]"
blenderbim.bim.helper.import_attributes2(
tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback
)
props.is_editing_attributes = True
return {"FINISHED"}
@@ -87,7 +102,19 @@ class EditAttributes(bpy.types.Operator, Operator):
obj = bpy.data.materials.get(self.obj)
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
attributes = blenderbim.bim.helper.export_attributes(props.attributes)
def callback(attributes, prop):
if prop.name in ("RefLatitude", "RefLongitude"):
if prop.is_null:
attributes[prop.name] = None
else:
try:
attributes[prop.name] = json.loads(prop.string_value)
except:
attributes[prop.name] = None
return True
attributes = blenderbim.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
return {"FINISHED"}
@@ -94,4 +94,10 @@ class BIM_PT_material_attributes(Panel):
def draw(self, context):
if not MaterialAttributesData.is_loaded:
MaterialAttributesData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialAttributesData.data["ifc_definition_id"]
):
MaterialAttributesData.load()
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
@@ -218,7 +218,7 @@ class EditBcfTopicName(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = bcfxml.topics[blender_topic.name]
topic = bcfxml.topics[blender_topic.name].topic
topic.title = blender_topic.title
return {"FINISHED"}
@@ -233,7 +233,7 @@ class EditBcfTopic(bpy.types.Operator):
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = bcfxml.topics[blender_topic.name]
topic = bcfxml.topics[blender_topic.name].topic
topic.title = blender_topic.title or None
topic.priority = blender_topic.priority or None
topic.due_date = blender_topic.due_date or None
@@ -940,7 +940,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
context.area.type = old
for global_id in exception_global_ids:
obj = IfcStore.get_element(global_id)
if obj:
if obj and bpy.context.view_layer.objects.get(obj.name):
obj.hide_set(True)
else:
objs = []
@@ -20,28 +20,37 @@ import bpy
from . import ui, operator, prop
classes = (
operator.AddBoundary,
operator.ColourByRelatedBuildingElement,
operator.DisableEditingBoundary,
operator.DisableEditingBoundaryGeometry,
operator.EditBoundaryAttributes,
operator.EditBoundaryGeometry,
operator.EnableEditingBoundary,
operator.EnableEditingBoundaryGeometry,
operator.HideBoundaries,
operator.LoadBoundary,
operator.LoadProjectSpaceBoundaries,
operator.LoadSpaceBoundaries,
operator.LoadBoundary,
operator.SelectRelatedElementBoundaries,
operator.SelectProjectBoundaries,
operator.SelectRelatedElementBoundaries,
operator.SelectRelatedElementTypeBoundaries,
operator.SelectSpaceBoundaries,
operator.ShowBoundaries,
operator.UpdateBoundaryGeometry,
ui.BIM_PT_Boundary,
ui.BIM_PT_SceneBoundaries,
ui.BIM_PT_SpaceBoundaries,
prop.BIMBoundaryProperties,
prop.BIMObjectBoundaryProperties,
)
def register():
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
bpy.types.Scene.BIMBoundaryProperties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMObjectBoundaryProperties)
def unregister():
del bpy.types.Scene.BIMBoundaryProperties
del bpy.types.Object.bim_boundary_properties
@@ -0,0 +1,112 @@
# 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 gpu
import bmesh
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
class BoundaryDecorator:
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 draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
selected_vertices = []
selected_edges = []
selected_tris = []
unselected_vertices = []
unselected_edges = []
unselected_tris = []
for boundary in context.scene.BIMBoundaryProperties.boundaries:
obj = boundary.obj
if not obj or not obj.data: # A boundary may not have data if it has no connection geometry
continue
if obj.mode == "EDIT":
continue # A profile decorator or something else is used here.
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
obj.data.calc_loop_triangles()
if obj.select_get():
offset = len(selected_vertices)
selected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
selected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
else:
offset = len(unselected_vertices)
unselected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
unselected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
if obj.mode != "EDIT":
bm.free()
if unselected_edges:
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
@@ -18,14 +18,23 @@
import bpy
import bmesh
import mathutils
import logging
import shapely
import mathutils
import numpy as np
import ifcopenshell.api
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import ifcopenshell.util.shape
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
import blenderbim.bim.import_ifc as import_ifc
from math import pi, inf
from mathutils import Vector, Matrix
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.boundary.decorator import BoundaryDecorator
def get_boundaries_collection(blender_space):
@@ -38,6 +47,20 @@ def get_boundaries_collection(blender_space):
return boundaries_collection
def disable_editing_boundary_geometry(context):
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
old_mesh = obj.data
loader = Loader()
obj.data = loader.create_mesh(element)
tool.Geometry.delete_data(old_mesh)
return {"FINISHED"}
class Loader:
def __init__(self):
self.ifc_file = None
@@ -119,7 +142,7 @@ class Loader:
class LoadProjectSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.load_project_space_boundaries"
bl_label = "Load all project space boundaries"
bl_label = "Load All Project Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -131,7 +154,7 @@ class LoadProjectSpaceBoundaries(bpy.types.Operator):
class LoadBoundary(bpy.types.Operator):
bl_idname = "bim.load_boundary"
bl_label = "Load boundary"
bl_label = "Load Boundary"
bl_options = {"REGISTER", "UNDO"}
boundary_id: bpy.props.IntProperty()
@@ -147,7 +170,7 @@ class LoadBoundary(bpy.types.Operator):
class LoadSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.load_space_boundaries"
bl_label = "Load selected space boundaries"
bl_label = "Load Selected Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -168,7 +191,7 @@ def get_element_boundaries(element):
class SelectRelatedElementBoundaries(bpy.types.Operator):
bl_idname = "bim.select_related_element_boundaries"
bl_label = "Select related element space boundaries"
bl_label = "Select Related Element Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
related_element: bpy.props.IntProperty()
@@ -185,7 +208,7 @@ class SelectRelatedElementBoundaries(bpy.types.Operator):
class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
bl_idname = "bim.select_related_element_type_boundaries"
bl_label = "Select related element type space boundaries"
bl_label = "Select Related Element Type Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
related_element: bpy.props.IntProperty()
@@ -208,7 +231,7 @@ class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
class SelectSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.select_space_boundaries"
bl_label = "Select all space boundaries"
bl_label = "Select All Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -224,7 +247,7 @@ class SelectSpaceBoundaries(bpy.types.Operator):
class SelectProjectBoundaries(bpy.types.Operator):
bl_idname = "bim.select_project_space_boundaries"
bl_label = "Select all project space boundaries"
bl_label = "Select All Project Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -260,7 +283,7 @@ def get_colour(ifc_boundary):
class ColourByRelatedBuildingElement(bpy.types.Operator):
bl_idname = "bim.colour_by_related_building_element"
bl_label = "Apply colour based on related building elements"
bl_label = "Apply Colour Based on Related Building Elements"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -308,7 +331,7 @@ EDITABLE_ATTRIBUTES = {
class EnableEditingBoundary(bpy.types.Operator):
bl_idname = "bim.enable_editing_boundary"
bl_label = "Edit boundary relations"
bl_label = "Edit Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -327,7 +350,7 @@ class EnableEditingBoundary(bpy.types.Operator):
class DisableEditingBoundary(bpy.types.Operator):
bl_idname = "bim.disable_editing_boundary"
bl_label = "Disable editing boundary relations"
bl_label = "Disable Editing Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -340,7 +363,7 @@ class DisableEditingBoundary(bpy.types.Operator):
class EditBoundaryAttributes(bpy.types.Operator):
bl_idname = "bim.edit_boundary_attributes"
bl_label = "Disable editing boundary relations"
bl_label = "Disable Editing Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -361,7 +384,7 @@ class EditBoundaryAttributes(bpy.types.Operator):
class UpdateBoundaryGeometry(bpy.types.Operator):
bl_idname = "bim.update_boundary_geometry"
bl_label = "Update boundary geometry"
bl_label = "Update Boundary Geometry"
bl_description = """
Update boundary connection geometry from mesh.
Mesh must lie on a single plane. It should look like a face or a face with holes.
@@ -376,3 +399,357 @@ class UpdateBoundaryGeometry(bpy.types.Operator):
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
return {"FINISHED"}
class EnableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_boundary_geometry"
bl_label = "Enable Editing Boundary Geometry"
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)
if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"):
surface = element.ConnectionGeometry.SurfaceOnRelatingElement
tool.Model.import_surface(surface, obj)
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(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 EditBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_boundary_geometry"
bl_label = "Edit Boundary Geometry"
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)
if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"):
surface = tool.Model.export_surface(obj)
if not surface:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
ProfileDecorator.install(
context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context)
)
bpy.ops.object.mode_set(mode="EDIT")
return
old_surface = element.ConnectionGeometry.SurfaceOnRelatingElement
for inverse in tool.Ifc.get().get_inverse(old_surface):
ifcopenshell.util.element.replace_attribute(inverse, old_surface, surface)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_surface)
old_mesh = obj.data
loader = Loader()
obj.data = loader.create_mesh(element)
tool.Geometry.delete_data(old_mesh)
class DisableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_boundary_geometry"
bl_label = "Disable Editing Boundary Geometry"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
return disable_editing_boundary_geometry(context)
class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.show_boundaries"
bl_label = "Show Boundaries"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMBoundaryProperties
loader = Loader()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not getattr(element, "BoundedBy", None):
continue
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
element = tool.Ifc.get_entity(obj)
for rel in element.BoundedBy or []:
boundary_obj = loader.load_boundary(rel, obj)
tool.Boundary.decorate_boundary(boundary_obj)
BoundaryDecorator.install(bpy.context)
return {"FINISHED"}
class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.hide_boundaries"
bl_label = "Hide Boundaries"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
to_delete = set()
spaces = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcSpace"):
spaces.add(element)
elif element.is_a("IfcRelSpaceBoundary"):
spaces.add(element.RelatingSpace)
for element in spaces:
for boundary in element.BoundedBy or []:
boundary_obj = tool.Ifc.get_object(boundary)
if boundary_obj:
to_delete.add(boundary_obj)
for boundary_obj in to_delete:
tool.Ifc.unlink(obj=boundary_obj)
bpy.data.objects.remove(boundary_obj)
context.scene.BIMBoundaryProperties.boundaries.clear()
return {"FINISHED"}
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boundary"
bl_label = "Add Boundary"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
objs = context.selected_objects
if len(objs) == 2:
# The user may select two objects, a space and its related building element
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = obj
else:
related_building_element = element
related_building_element_obj = obj
elif len(objs) == 1:
# Optionally the user may select just the space, and the building element shall be auto-detected
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
element = tool.Ifc.get_entity(objs[0])
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = objs[0]
target = bpy.context.scene.cursor.location
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
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
if not (subelement.is_a("IfcWall") or subelement.is_a("IfcSlab")):
continue
obj = tool.Ifc.get_object(subelement)
if obj:
raycast = obj.closest_point_on_mesh(obj.matrix_world.inverted() @ target, distance=0.1)
if raycast[0]:
related_building_element = subelement
related_building_element_obj = obj
break
if not relating_space or not related_building_element:
return
bm = bmesh.new()
bm.from_mesh(relating_space_obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
target_distance = inf
target_face = None
for face in bm.faces:
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
raycast = related_building_element_obj.closest_point_on_mesh(
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
)
if raycast[0]:
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
if distance < target_distance:
target_face = face
target_distance = distance
if not target_face:
return
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
# Is this right? Or should I use loop?
target_face_verts = [v.co.copy() for v in target_face.verts]
target_face_matrix = self.get_face_matrix(*[v.copy() for v in target_face_verts[0:3]])
target_face_matrix_i = target_face_matrix.inverted()
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
related_building_element_polygon = self.get_flattened_polygon(
related_building_element, relating_space_obj, target_face_matrix_i
)
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
if type(gross_boundary_polygon) == shapely.GeometryCollection:
for geom in gross_boundary_polygon.geoms:
if type(geom) == shapely.Polygon:
gross_boundary_polygon = geom
break
# The gross boundary polygon may not be a true gross boundary since it
# may have openings already removed, such as in IFC4 Reference View. So
# we cheat by using the exterior boundary to mean "gross". Later, we
# can use this to check whether or not the opening is relevant to our
# space.
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
net_boundary_polygon = shapely.Polygon(gross_boundary_polygon)
inner_boundaries = []
for rel in getattr(related_building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
filling = None
if opening.HasFillings:
filling = opening.HasFillings[0].RelatedBuildingElement
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
net_boundary_polygon = net_boundary_polygon.difference(opening_polygon)
# Only openings that are projected onto our exterior boundary are relevant.
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
continue
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
boundary.RelatingSpace = relating_space
boundary.RelatedBuildingElement = filling or related_building_element
boundary.ConnectionGeometry = connection_geometry
boundary.PhysicalOrVirtualBoundary = "PHYSICAL" if filling else "VIRTUAL"
boundary.InternalOrExternalBoundary = "INTERNAL"
if boundary.is_a("IfcRelSpaceBoundary2ndLevel"):
boundary.ParentBoundary = parent_boundary
connection_geometry = self.create_connection_geometry_from_polygon(net_boundary_polygon, target_face_matrix)
parent_boundary.RelatingSpace = relating_space
parent_boundary.RelatedBuildingElement = related_building_element
parent_boundary.ConnectionGeometry = connection_geometry
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
bpy.ops.bim.show_boundaries()
obj = tool.Ifc.get_object(parent_boundary)
obj.select_set(True)
def get_face_matrix(self, p1, p2, p3):
edge1 = p2 - p1
edge2 = p3 - p1
normal = edge1.cross(edge2)
z_axis = normal.normalized()
x_axis = p2 - p1
x_axis.normalize()
y_axis = z_axis.cross(x_axis)
mat = Matrix()
mat.col[0][:3] = x_axis
mat.col[1][:3] = y_axis
mat.col[2][:3] = z_axis
mat.col[3][:3] = p1
return mat
def get_flattened_polygon(self, element, relating_space_obj, target_face_matrix_i):
obj = tool.Ifc.get_object(element)
if obj and tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
space_matrix_i = relating_space_obj.matrix_world.inverted()
settings = ifcopenshell.geom.settings()
if not element.is_a("IfcOpeningElement"):
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
settings.set(settings.STRICT_TOLERANCE, True)
# geometry = ifcopenshell.geom.create_shape(settings, body)
shape = ifcopenshell.geom.create_shape(settings, element)
m = shape.transformation.matrix.data
mat = Matrix(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
verts = [space_matrix_i @ mat @ Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
polygons = []
for face in faces:
polygon = shapely.Polygon([tuple((target_face_matrix_i @ verts[vi]).xy) for vi in face])
polygons.append(polygon)
return shapely.ops.unary_union(polygons)
def create_connection_geometry_from_polygon(self, polygon, target_face_matrix):
surface = self.export_surface(polygon, target_face_matrix)
return tool.Ifc.get().createIfcConnectionSurfaceGeometry(surface)
def export_surface(self, polygon, target_face_matrix):
x_axis = target_face_matrix.col[0][:3]
z_axis = target_face_matrix.col[2][:3]
p1 = target_face_matrix.col[3][:3]
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
tool.Model.unit_scale = self.unit_scale
surface = tool.Ifc.get().createIfcCurveBoundedPlane()
surface.BasisSurface = tool.Ifc.get().createIfcPlane(tool.Ifc.get().createIfcAxis2Placement3D(
tool.Ifc.get().createIfcCartesianPoint([o / self.unit_scale for o in p1]),
tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]),
tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]),
))
if tool.Ifc.get().schema != "IFC2X3":
points = [tool.Model.convert_si_to_unit(list(co)) for co in polygon.exterior.coords]
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
outer_boundary = tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False)
inner_boundaries = []
for interior in polygon.interiors:
points = [tool.Model.convert_si_to_unit(list(co)) for co in interior.coords]
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
inner_boundaries.append(tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False))
else:
pass # TODO
surface.OuterBoundary = outer_boundary
surface.InnerBoundaries = inner_boundaries
return surface
@@ -18,6 +18,7 @@
import bpy
from bpy.types import PropertyGroup
from blenderbim.bim.prop import ObjProperty
from bpy.props import (
PointerProperty,
StringProperty,
@@ -52,9 +53,13 @@ def element_filter(self, object):
return False
class BIMBoundaryProperties(PropertyGroup):
class BIMObjectBoundaryProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
class BIMBoundaryProperties(PropertyGroup):
boundaries: bpy.props.CollectionProperty(type=ObjProperty)
@@ -18,7 +18,6 @@
import bpy
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
from bpy.types import PropertyGroup
from math import pi
@@ -32,3 +31,4 @@ class BIMCadProperties(PropertyGroup):
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
gable_roof_separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
@@ -46,7 +46,7 @@ class CadTool(WorkSpaceTool):
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
)
def draw_settings(context, layout, tool):
def draw_settings(context, layout, workspace_tool):
obj = context.active_object
if not obj or not obj.data:
return
@@ -54,14 +54,19 @@ class CadTool(WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
if obj.BIMObjectProperties.ifc_definition_id:
row.operator("bim.edit_extrusion_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
else:
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
element = tool.Ifc.get_entity(obj)
if element:
if element.is_a("IfcProfileDef"):
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
elif element.is_a("IfcRelSpaceBoundary"):
row.operator("bim.edit_boundary_geometry", text="Save Profile")
row.operator("bim.disable_editing_boundary_geometry", text="", icon="CANCEL")
else:
row.operator("bim.edit_extrusion_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -156,7 +161,7 @@ class CadTool(WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Set gable roof angle").hotkey = "S_R"
row.operator("bim.cad_hotkey", text="Set Gable Roof Angle").hotkey = "S_R"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -229,6 +234,7 @@ class CadHotkey(bpy.types.Operator):
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
self.layout.row().prop(props, "gable_roof_separate_verts")
elif self.hotkey == "S_V":
if not self.is_profile():
@@ -254,28 +260,30 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_offset(distance=self.props.distance)
def hotkey_S_Q(self):
if tool.Ifc.get_entity(bpy.context.active_object):
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
element = tool.Ifc.get_entity(bpy.context.active_object)
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
if element.is_a("IfcProfileDef"):
bpy.ops.bim.edit_arbitrary_profile()
elif element.is_a("IfcRelSpaceBoundary"):
bpy.ops.bim.edit_boundary_geometry()
else:
bpy.ops.bim.edit_extrusion_profile()
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
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 (
(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()
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()
def hotkey_S_R(self):
if self.is_profile():
@@ -285,7 +293,9 @@ class CadHotkey(bpy.types.Operator):
and RoofData.data["parameters"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
bpy.ops.bim.set_gable_roof_edge_angle(
angle=self.props.gable_roof_edge_angle, separate_verts=self.props.gable_roof_separate_verts
)
def hotkey_S_T(self):
bpy.ops.bim.cad_mitre()
@@ -76,6 +76,7 @@ classes = (
operator.LoadCostItemTaskQuantities,
operator.LoadCostItemResourceQuantities,
operator.ChangeParentCostItem,
operator.CopyCostItem,
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
@@ -97,10 +97,9 @@ class AddSummaryCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Add Cost Item"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add a summary cost item"
cost_schedule: bpy.props.IntProperty()
def _execute(self, context):
core.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
core.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=tool.Cost.get_active_cost_schedule())
class AddCostItem(bpy.types.Operator, tool.Ifc.Operator):
@@ -114,6 +113,16 @@ class AddCostItem(bpy.types.Operator, tool.Ifc.Operator):
core.add_cost_item(tool.Ifc, tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
class CopyCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.copy_cost_item"
bl_label = "Copy Cost Item"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy a cost item"
def _execute(self, context):
core.copy_cost_item(tool.Ifc, tool.Cost)
class ExpandCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.expand_cost_item"
bl_label = "Expand Cost Item"
@@ -149,7 +158,7 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator):
class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_cost_item"
bl_label = "Remove Cost item"
bl_label = "Remove Cost Item"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
@@ -214,8 +223,9 @@ class UnassignCostItemType(bpy.types.Operator, tool.Ifc.Operator):
core.unassign_cost_item_type(
tool.Ifc,
tool.Cost,
self.cost_item,
products=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [],
tool.Spatial,
cost_item=tool.Ifc.get().by_id(self.cost_item),
product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [],
)
return {"FINISHED"}
@@ -491,7 +501,7 @@ class AddCostColumn(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
def _execute(self, context):
def execute(self, context):
core.add_cost_column(tool.Cost, self.name)
return {"FINISHED"}
@@ -619,10 +629,12 @@ class ExportCostSchedules(bpy.types.Operator):
bl_label = "Export Cost Schedule"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
cost_schedule: bpy.props.IntProperty()
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)
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
core.export_cost_schedules(tool.Cost, format=self.format, cost_schedule=cost_schedule)
return {"FINISHED"}
def invoke(self, context, event):
@@ -667,7 +679,9 @@ class LoadProductCostItems(bpy.types.Operator):
return True
def execute(self, context):
core.load_product_cost_items(tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id))
core.load_product_cost_items(
tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
return {"FINISHED"}
@@ -137,7 +137,7 @@ class CostItemType(PropertyGroup):
def update_cost_item_parent(self, context):
cost_item = tool.Cost.get_highlighted_cost_item()
tool.Cost.toggle_cost_item_parent(cost_item=cost_item)
tool.Cost.toggle_cost_item_parent_change(cost_item=cost_item)
def update_active_cost_item_elements(self, context):
@@ -213,3 +213,4 @@ class BIMCostProperties(PropertyGroup):
name="Show Nested Tasks", default=False, update=update_active_cost_item_resources
)
change_cost_item_parent: BoolProperty(name="Change Cost Item Parent", default=False, update=update_cost_item_parent)
show_cost_item_operators: BoolProperty(name="Show Cost Item Operators", default=False)
+70 -45
View File
@@ -43,43 +43,54 @@ class BIM_PT_cost_schedules(Panel):
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.prop(self.props, "cost_schedule_predefined_types")
row.operator("bim.add_cost_schedule", icon="ADD", text="Add new")
if not self.props.active_cost_schedule_id:
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.prop(self.props, "cost_schedule_predefined_types")
row.operator("bim.add_cost_schedule", icon="ADD", text="Add")
for schedule in CostSchedulesData.data["schedules"]:
self.draw_cost_schedule_ui(schedule)
def draw_cost_schedule_ui(self, cost_schedule):
row = self.layout.row(align=True)
row.label(text=cost_schedule["name"], icon="LINENUMBERS_ON")
if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule["id"]:
op = row.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
row.label(text="Currently editing: {}".format(cost_schedule["name"]), icon="LINENUMBERS_ON")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col = grid.column()
row1 = col.row(align=True)
row1.alignment = "LEFT"
row1.label(text="Schedule tools")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.operator("bim.export_cost_schedules", text="Export", icon="EXPORT").cost_schedule = cost_schedule["id"]
row2 = col.row(align=True)
row2.alignment = "RIGHT"
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
op.cost_schedule = cost_schedule["id"]
row.operator("bim.select_unassigned_products", icon="RESTRICT_SELECT_OFF", text="Unassigned")
row2.operator("bim.select_unassigned_products", icon="RESTRICT_SELECT_OFF", text="Unassigned")
row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY")
col = grid.column()
row1 = col.row(align=True)
row1.alignment = "LEFT"
row1.label(text="Settings")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY")
if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES":
row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK")
elif self.props.is_editing == "COST_ITEMS":
row.operator("bim.add_summary_cost_item", text="", icon="ADD").cost_schedule = cost_schedule["id"]
row.operator("bim.disable_editing_cost_schedule", text="", icon="CANCEL")
elif self.props.active_cost_schedule_id:
row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule["id"]
row.operator("bim.disable_editing_cost_schedule", text="Disable Editing", icon="CANCEL")
else:
row.label(text=cost_schedule["name"], icon="LINENUMBERS_ON")
row.operator("bim.enable_editing_cost_items", text="", icon="OUTLINER").cost_schedule = cost_schedule["id"]
row.operator(
"bim.enable_editing_cost_schedule_attributes", text="", icon="GREASEPENCIL"
).cost_schedule = cost_schedule["id"]
row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule["id"]
if self.props.active_cost_schedule_id == cost_schedule["id"]:
if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES":
self.draw_editable_cost_schedule_ui()
@@ -101,30 +112,39 @@ class BIM_PT_cost_schedules(Panel):
row = self.layout.row(align=True)
row.alignment = "RIGHT"
ifc_definition_id = None
row = self.layout.row(align=True)
row.label(text="Cost Item Tools")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.add_summary_cost_item", text="Add Summary Cost", icon="ADD")
row.operator("bim.expand_all_tasks", text="Expand All")
row.operator("bim.contract_all_tasks", text="Contract All")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
if self.props.cost_items and self.props.active_cost_item_index < len(self.props.cost_items):
ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id
if ifc_definition_id:
row.prop(self.props, "change_cost_item_parent", text="", icon="LINKED")
row.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
if not CostSchedulesData.data["is_editing_rates"]:
op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES")
row.prop(self.props, "show_cost_item_operators", text="Edit", icon="DOWNARROW_HLT")
row.operator("bim.add_cost_item", text="Add", icon="ADD").cost_item = ifc_definition_id
row.operator("bim.copy_cost_item", text="Copy", icon="ADD")
row.operator("bim.remove_cost_item", text="Delete", icon="X").cost_item = ifc_definition_id
if self.props.show_cost_item_operators:
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.props, "change_cost_item_parent", text="", icon="LINKED")
row.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
if not CostSchedulesData.data["is_editing_rates"]:
op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES")
op.cost_item = ifc_definition_id
op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC")
op.cost_item = ifc_definition_id
op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC")
op.cost_item = ifc_definition_id
row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = ifc_definition_id
if self.props.active_cost_item_id == ifc_definition_id:
if self.props.cost_item_editing_type == "ATTRIBUTES":
row.operator("bim.edit_cost_item", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL")
else:
op = row.operator("bim.enable_editing_cost_item_attributes", text="", icon="GREASEPENCIL")
op.cost_item = ifc_definition_id
row.operator("bim.remove_cost_item", text="", icon="X").cost_item = ifc_definition_id
if self.props.active_cost_item_id == ifc_definition_id:
if self.props.cost_item_editing_type == "ATTRIBUTES":
row.operator("bim.edit_cost_item", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL")
else:
op = row.operator("bim.enable_editing_cost_item_attributes", text="", icon="GREASEPENCIL")
op.cost_item = ifc_definition_id
self.layout.template_list(
"BIM_UL_cost_items",
"",
@@ -594,7 +614,7 @@ class BIM_UL_cost_items_trait:
layout.label(text=text)
def draw_uom_column(self, layout, cost_item):
layout.label(text=cost_item["UnitBasisUnitSymbol"] or "?" if cost_item["UnitBasisValueComponent"] else "-")
layout.label(text=cost_item["UnitBasisUnitSymbol"] or "-" if cost_item["UnitBasisValueComponent"] else "-")
def draw_order_operator(self, row, ifc_definition_id, cost_item):
if cost_item["NestingIndex"] is not None:
@@ -608,7 +628,14 @@ class BIM_UL_cost_items_trait:
op.new_index = cost_item["NestingIndex"] - 1
def draw_total_quantity_column(self, layout, cost_item):
layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '?'}")
layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}")
# if cost_item["DerivedTotalCostQuantity"] not in [None, 0]:
# layout.label(text="{0:.2f}".format(cost_item["DerivedTotalCostQuantity"]) + f" {cost_item['DerivedUnitSymbol'] or '-'}")
# else:
# if cost_item["TotalCostQuantity"] == 0:
# layout.label(text="-")
# else:
# layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}")
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
@@ -691,9 +718,7 @@ class BIM_PT_Costing_Tools(Panel):
def draw(self, context):
self.props = context.scene.BIMCostProperties
row = self.layout.row()
row.operator(
"bim.load_product_cost_items", icon="FILE_REFRESH"
)
row.operator("bim.load_product_cost_items", icon="FILE_REFRESH")
row = self.layout.row()
row.template_list(
"BIM_UL_product_cost_items",
@@ -31,6 +31,7 @@ from blenderbim.bim.handler import purge_module_data
class AddCsvAttribute(bpy.types.Operator):
bl_idname = "bim.add_csv_attribute"
bl_label = "Add CSV Attribute"
bl_description = "Add a new IFC Attribute to the CSV export"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -52,6 +53,7 @@ class RemoveCsvAttribute(bpy.types.Operator):
class RemoveAllCsvAttributes(bpy.types.Operator):
bl_idname = "bim.remove_all_csv_attributes"
bl_label = "Remove all CSV Attributes"
bl_description = "Remove all IFC Attributes from the CSV export"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -61,20 +63,25 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
class ImportCsvAttributes(bpy.types.Operator):
bl_idname = "bim.import_csv_attributes"
bl_label = "Import CSV Attributes"
bl_label = "Import CSV Template"
bl_description = "Import a json template for CSV export"
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
csv_attributes = context.scene.CsvProperties.csv_attributes
csv_attributes.clear()
csv_props = context.scene.CsvProperties
csv_json = json.load(open(self.filepath))
i = 0
for attribute in csv_json:
csv_attributes.add()
csv_attributes[i].name = attribute
i += 1
expression = csv_json.get("expression", "")
if expression:
csv_props.ifc_selector = expression
attributes = csv_json.get("attributes", [])
if attributes:
csv_props.csv_attributes.clear()
for attribute in attributes:
csv_props.csv_attributes.add().name = attribute
return {"FINISHED"}
@@ -85,19 +92,30 @@ class ImportCsvAttributes(bpy.types.Operator):
class ExportCsvAttributes(bpy.types.Operator):
bl_idname = "bim.export_csv_attributes"
bl_label = "Export CSV Attributes"
bl_label = "Export CSV Template"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save a json template for CSV export"
filename_ext = ".json"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
csv_attributes = []
with open(self.filepath, "w") as outfile:
for attribute in context.scene.CsvProperties.csv_attributes:
csv_attributes.append(attribute.name)
csv_props = context.scene.CsvProperties
json.dump(csv_attributes, outfile)
csv_template = {}
expression = csv_props.ifc_selector
if expression:
csv_template["expression"] = expression
csv_attributes = []
for attribute in csv_props.csv_attributes:
attribute_name = attribute.name
csv_attributes.append(attribute_name)
if csv_attributes:
csv_template["attributes"] = csv_attributes
with open(self.filepath, "w") as outfile:
json.dump(csv_template, outfile)
return {"FINISHED"}
+7 -16
View File
@@ -49,27 +49,18 @@ class BIM_PT_ifccsv(Panel):
row.operator("bim.eyedrop_ifccsv", icon="EYEDROPPER", text="")
layout.separator()
row = layout.row()
split = row.split(factor=0.7)
c = split.column()
c.operator("bim.add_csv_attribute")
c = split.column()
c.operator("bim.import_csv_attributes", icon="IMPORT", text="Load Template")
row = layout.row(align=True)
row.operator("bim.add_csv_attribute", icon="ADD")
row.operator("bim.remove_all_csv_attributes", icon="CANCEL")
row = layout.row(align=True)
row.operator("bim.import_csv_attributes", icon="IMPORT")
row.operator("bim.export_csv_attributes", icon="EXPORT")
for index, attribute in enumerate(props.csv_attributes):
row = layout.row(align=True)
row.prop(attribute, "name", text="")
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
if props.csv_attributes:
row = layout.row()
row.label()
row.operator("bim.remove_all_csv_attributes", icon="CANCEL", text="")
row = layout.row()
row.operator("bim.export_csv_attributes", icon="EXPORT", text="Create Template")
layout.separator()
row = layout.row(align=True)
row.prop(props, "csv_delimiter")
@@ -78,7 +69,7 @@ class BIM_PT_ifccsv(Panel):
row.prop(props, "csv_custom_delimiter")
row = layout.row()
split = row.split(factor=0.7)
split = row.split(factor=0.5)
c = split.column()
c.operator("bim.export_ifccsv", icon="EXPORT")
c = split.column()
@@ -17,11 +17,12 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from . import ui, prop, operator, handler, gizmos
from . import ui, prop, operator, handler, gizmos, workspace
classes = (
operator.ActivateDrawing,
operator.ActivateDrawingStyle,
operator.ActivateView,
operator.ActivateModel,
operator.AddAnnotation,
operator.AddDrawing,
operator.AddDrawingStyle,
@@ -30,6 +31,7 @@ classes = (
operator.AddSchedule,
operator.AddScheduleToSheet,
operator.AddSheet,
operator.AddTextLiteral,
operator.BuildSchedule,
operator.CleanWireframes,
operator.ContractSheet,
@@ -40,32 +42,34 @@ classes = (
operator.DisableEditingSchedules,
operator.DisableEditingSheets,
operator.DisableEditingText,
operator.AddTextLiteral,
operator.RemoveTextLiteral,
operator.DuplicateDrawing,
operator.EditAssignedProduct,
operator.EditSheet,
operator.EditText,
operator.EditTextPopup,
operator.EditVectorStyle,
operator.EnableEditingAssignedProduct,
operator.EnableEditingText,
operator.ExpandSheet,
operator.LoadDrawings,
operator.LoadSchedules,
operator.LoadSheets,
operator.OpenDrawing,
operator.OpenSchedule,
operator.OpenSheet,
operator.OpenView,
operator.RemoveDrawing,
operator.RemoveDrawingFromSheet,
operator.RemoveDrawingStyle,
operator.RemoveDrawingStyleAttribute,
operator.RemoveSchedule,
operator.ReloadDrawingStyles,
operator.RemoveSheet,
operator.RemoveTextLiteral,
operator.SelectAllDrawings,
operator.ResizeText,
operator.SaveDrawingStyle,
operator.SelectDocIfcFile,
operator.SaveDrawingStylesData,
operator.SelectAssignedProduct,
operator.SelectDocIfcFile,
prop.Variable,
prop.Drawing,
prop.Schedule,
@@ -76,6 +80,7 @@ classes = (
prop.Literal,
prop.BIMTextProperties,
prop.BIMAssignedProductProperties,
prop.BIMAnnotationProperties,
ui.BIM_PT_camera,
ui.BIM_PT_drawing_underlay,
ui.BIM_PT_annotation_utilities,
@@ -91,11 +96,15 @@ classes = (
gizmos.DimensionLabelGizmo,
gizmos.ExtrusionGuidesGizmo,
gizmos.ExtrusionWidget,
workspace.Hotkey,
)
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=True)
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
bpy.types.Scene.BIMAnnotationProperties = bpy.props.PointerProperty(type=prop.BIMAnnotationProperties)
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
@@ -105,7 +114,10 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.AnnotationTool)
del bpy.types.Scene.DocProperties
del bpy.types.Scene.BIMAnnotationProperties
del bpy.types.Camera.BIMCameraProperties
del bpy.types.Object.BIMAssignedProductProperties
del bpy.types.Object.BIMTextProperties
@@ -77,66 +77,35 @@ class Annotator:
co1, co2, _, _ = Annotator.get_placeholder_coords()
co1 = obj.matrix_world.inverted() @ co1
co2 = obj.matrix_world.inverted() @ co2
if isinstance(obj.data, bpy.types.Mesh):
obj.data.vertices.add(2)
obj.data.vertices[-2].co = co1
obj.data.vertices[-1].co = co2
obj.data.edges.add(1)
obj.data.edges[-1].vertices = (obj.data.vertices[-2].index, obj.data.vertices[-1].index)
if isinstance(obj.data, bpy.types.Curve):
polyline = obj.data.splines.new("POLY")
polyline.points.add(1)
polyline.points[-2].co = list(co1) + [1]
polyline.points[-1].co = list(co2) + [1]
return obj
@staticmethod
def add_plane_to_annotation(obj):
co1, co2, co3, co4 = Annotator.get_placeholder_coords()
co1 = obj.matrix_world.inverted() @ co1 # bot left
co2 = obj.matrix_world.inverted() @ co2 # top left
co3 = obj.matrix_world.inverted() @ co3 # bot right
co4 = obj.matrix_world.inverted() @ co4 # top right
# default order = bot left, top left, bot right, top right
# therefore we redefine the order
face_verts = [0, 2, 3, 1]
obj.data.vertices.add(4)
v1 = obj.data.vertices[-4]
v2 = obj.data.vertices[-3]
v3 = obj.data.vertices[-2]
v4 = obj.data.vertices[-1]
v1.co = co4
v2.co = co2
v3.co = co1
v4.co = co3
verts_world_space = Annotator.get_placeholder_coords()
verts_local = [obj.matrix_world.inverted() @ v for v in verts_world_space]
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
new_verts = [bm.verts.new(v) for v in verts_local]
obj.data.edges.add(4)
e1 = obj.data.edges[-4]
e2 = obj.data.edges[-3]
e3 = obj.data.edges[-2]
e4 = obj.data.edges[-1]
e1.vertices = (v1.index, v2.index)
e2.vertices = (v2.index, v3.index)
e3.vertices = (v3.index, v4.index)
e4.vertices = (v4.index, v1.index)
obj.data.loops.add(4)
l1 = obj.data.loops[-4]
l2 = obj.data.loops[-3]
l3 = obj.data.loops[-2]
l4 = obj.data.loops[-1]
l1.vertex_index = v1.index
l1.edge_index = e1.index
l2.vertex_index = v2.index
l2.edge_index = e2.index
l3.vertex_index = v3.index
l3.edge_index = e3.index
l4.vertex_index = v4.index
l4.edge_index = e4.index
obj.data.polygons.add(1)
p1 = obj.data.polygons[-1]
p1.vertices = (v1.index, v2.index, v3.index, v4.index)
p1.loop_start = l1.index
p1.loop_total = 4
bm.faces.new([new_verts[i] for i in face_verts])
tool.Blender.apply_bmesh(obj.data, bm, obj)
return obj
@staticmethod
@@ -144,15 +113,15 @@ class Annotator:
camera = tool.Ifc.get_object(drawing)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = camera.matrix_world.copy()
matrix_world[0][3] = co1.x
matrix_world[1][3] = co1.y
matrix_world[2][3] = co1.z
matrix_world.translation = co1
collection = camera.users_collection[0]
if object_type == "TEXT":
obj = bpy.data.objects.new(object_type, None)
obj.matrix_world = matrix_world
collection.objects.link(obj)
return obj
elif object_type in ("TEXT_LEADER", "SECTION_LEVEL"):
data = bpy.data.curves.new(object_type, type="CURVE")
data.dimensions = "3D"
@@ -161,17 +130,20 @@ class Annotator:
obj.matrix_world = matrix_world
collection.objects.link(obj)
return obj
if object_type != "ANGLE":
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if element and element.ObjectType == object_type and obj.type == object_type.upper():
return obj
if data_type == "mesh":
data = bpy.data.meshes.new(object_type)
elif data_type == "curve":
data = bpy.data.curves.new(object_type, type="CURVE")
data.dimensions = "3D"
data.resolution_u = 2
obj = bpy.data.objects.new(object_type, data)
obj.matrix_world = matrix_world
collection.objects.link(obj)
@@ -182,14 +154,13 @@ class Annotator:
if not camera:
camera = bpy.context.scene.camera
z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
y = (
camera.data.ortho_scale
* (bpy.context.scene.render.resolution_y / bpy.context.scene.render.resolution_x)
/ 4
)
else:
y = camera.data.ortho_scale / 4
y = camera.data.ortho_scale / 4
res_x = bpy.context.scene.render.resolution_x
res_y = bpy.context.scene.render.resolution_y
if res_x > res_y:
y *= res_y / res_x
y_offset = camera.matrix_world.to_quaternion() @ Vector((0, y, 0))
x_offset = camera.matrix_world.to_quaternion() @ Vector((y / 2, 0, 0))
return (
@@ -16,10 +16,12 @@
# 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 bpy
import ifcopenshell.util.element
import ifcopenshell.util.representation
import blenderbim.tool as tool
from pathlib import Path
def refresh():
@@ -27,7 +29,10 @@ def refresh():
SheetsData.is_loaded = False
SchedulesData.is_loaded = False
DrawingsData.is_loaded = False
AnnotationData.is_loaded = False
DecoratorData.data = {}
DecoratorData.cut_cache = {}
DecoratorData.layerset_cache = {}
class ProductAssignmentsData:
@@ -56,12 +61,36 @@ class SheetsData:
@classmethod
def load(cls):
cls.data = {"total_sheets": cls.total_sheets()}
cls.data = {
"has_saved_ifc": cls.has_saved_ifc(),
"total_sheets": cls.total_sheets(),
"titleblocks": cls.titleblocks(),
}
cls.is_loaded = True
@classmethod
def has_saved_ifc(cls):
return os.path.isfile(tool.Ifc.get_path())
@classmethod
def total_sheets(cls):
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"])
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"])
@classmethod
def titleblocks(cls):
files = Path(os.path.join(bpy.context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
files = [str(f.stem) for f in files]
if tool.Ifc.get():
project = tool.Ifc.get().by_type("IfcProject")[0]
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
if not titleblocks_dir:
titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
if os.path.exists(titleblocks_dir):
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
return [(f, f, "") for f in sorted(list(set(files)))]
class DrawingsData:
@@ -70,9 +99,18 @@ class DrawingsData:
@classmethod
def load(cls):
cls.data = {"total_drawings": cls.total_drawings(), "location_hint": cls.location_hint()}
cls.data = {
"has_saved_ifc": cls.has_saved_ifc(),
"total_drawings": cls.total_drawings(),
"location_hint": cls.location_hint(),
"active_drawing_pset_data": cls.active_drawing_pset_data(),
}
cls.is_loaded = True
@classmethod
def has_saved_ifc(cls):
return os.path.isfile(tool.Ifc.get_path())
@classmethod
def total_drawings(cls):
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
@@ -87,6 +125,15 @@ class DrawingsData:
return results
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
@classmethod
def active_drawing_pset_data(cls):
ifc_file = tool.Ifc.get()
drawing_id = bpy.context.scene.DocProperties.active_drawing_id
if drawing_id == 0:
return {}
drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id)
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
class SchedulesData:
data = {}
@@ -94,9 +141,13 @@ class SchedulesData:
@classmethod
def load(cls):
cls.data = {"total_schedules": cls.total_schedules()}
cls.data = {"has_saved_ifc": cls.has_saved_ifc(), "total_schedules": cls.total_schedules()}
cls.is_loaded = True
@classmethod
def has_saved_ifc(cls):
return os.path.isfile(tool.Ifc.get_path())
@classmethod
def total_schedules(cls):
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SCHEDULE"])
@@ -114,6 +165,8 @@ FONT_SIZES = {
class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
layerset_cache = {}
# used by Ifc Annotations with ObjectType = "BATTING"
@classmethod
@@ -171,7 +224,7 @@ class DecoratorData:
cls.data[obj.name] = display_data
return display_data
# used by Ifc Annotations with ObjectType = "TEXT"
# used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"
@classmethod
def get_ifc_text_data(cls, obj):
"""returns font size in mm for current ifc text object"""
@@ -180,23 +233,26 @@ class DecoratorData:
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"]:
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
return None
props = obj.BIMTextProperties
# getting font size
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
# 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"
# get font size
classes = pset_data.get("Classes", None) or "regular"
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"
)
font_size = FONT_SIZES[font_size_type]
# get symbol
symbol = pset_data.get("Symbol", None)
# other attributes
props_literals = props.literals
props_literals_n = len(props.literals)
@@ -214,19 +270,20 @@ class DecoratorData:
literals_data.append(literal_data)
text_data = {"Literals": literals_data, "FontSize": font_size}
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol}
cls.data[obj.name] = text_data
return text_data
# used by Ifc Annotations with ObjectType = "DIMENSION"
# used by Ifc Annotations with ObjectType = "DIMENSION" / "DIAMETER"
@classmethod
def get_dimension_style(cls, obj):
def get_dimension_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 or not element.is_a("IfcAnnotation") or element.ObjectType != "DIMENSION":
supported_object_types = ("DIMENSION", "DIAMETER")
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types:
return None
dimension_style = "arrow"
@@ -234,5 +291,43 @@ class DecoratorData:
if classes and "oblique" in classes.lower().split():
dimension_style = "oblique"
cls.data[obj.name] = dimension_style
return dimension_style
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension") or {}
show_description_only = pset_data.get("ShowDescriptionOnly", False)
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
text_prefix = pset_data.get("TextPrefix", "")
text_suffix = pset_data.get("TextSuffix", "")
dimension_data = {
"dimension_style": dimension_style,
"show_description_only": show_description_only,
"suppress_zero_inches": suppress_zero_inches,
"text_prefix": text_prefix,
"text_suffix": text_suffix,
}
cls.data[obj.name] = dimension_data
return dimension_data
class AnnotationData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.props = bpy.context.scene.BIMAnnotationProperties
cls.data["relating_types"] = cls.get_relating_types()
@classmethod
def get_relating_types(cls):
object_type = cls.props.object_type
relating_types = []
for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"):
if tool.Drawing.is_annotation_object_type(relating_type, object_type):
relating_types.append(relating_type)
enum_items = [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in relating_types]
# item to create anootations without relating types
enum_items.insert(0, ("0", "-", ""))
return enum_items
File diff suppressed because it is too large Load Diff
@@ -18,13 +18,12 @@
import bpy
import blf
import math
import gpu, bgl
import gpu
from bpy import types
from mathutils import Vector, Matrix
from mathutils import Vector
from mathutils import geometry
from bpy_extras import view3d_utils
from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader, BaseLinesShader
from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader
from ifcopenshell.util.unit import si_conversions
@@ -252,11 +251,12 @@ X3DISC = (
class CustomGizmo:
# FIXME: highliting/selection doesnt work
def draw_very_custom_shape(self, ctx, custom_shape, select_id=None):
# similar to draw_custom_shape
shape, batch, shader = custom_shape
# create shader and batch
shader_wrapper, batch = custom_shape
shader = shader_wrapper.get_shader()
# setup params
shader.bind()
if select_id is not None:
gpu.select.load_id(select_id)
else:
@@ -265,15 +265,19 @@ class CustomGizmo:
else:
color = (*self.color, self.alpha)
shader.uniform_float("color", color)
shape.glenable()
shader_wrapper.glenable()
shader_wrapper.uniform_region(ctx)
shape.uniform_region(ctx)
# shader.uniform_float('modelMatrix', self.matrix_world)
# using `with` block to make sure matrix multiplication
# won't affect other shaders
with gpu.matrix.push_pop():
gpu.matrix.multiply_matrix(self.matrix_world)
batch.draw()
# using matrix_world seems to be unaffected by matrix_offset
# therefore we use basis @ offset
matrix = self.matrix_basis @ self.matrix_offset
gpu.matrix.multiply_matrix(matrix)
batch.draw(shader)
bgl.glDisable(bgl.GL_BLEND)
gpu.state.blend_set("NONE")
class OffsetHandle:
@@ -361,6 +365,7 @@ class UglyDotGizmo(OffsetHandle, types.Gizmo):
self.draw_custom_shape(self.custom_shape, select_id=select_id)
# TODO: dead code?
class DotGizmo(CustomGizmo, OffsetHandle, types.Gizmo):
"""Single dot viewport-aligned"""
@@ -390,10 +395,6 @@ class DotGizmo(CustomGizmo, OffsetHandle, types.Gizmo):
self.refresh()
self.draw_very_custom_shape(ctx, self.custom_shape, select_id=select_id)
# doesn't get called
# def test_select(self, ctx, location):
# pass
class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
"""Extrusion guides
@@ -408,19 +409,25 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
__slots__ = ("scale_value", "custom_shape")
def setup(self):
shader = ExtrusionGuidesShader()
self.custom_shape = shader, shader.batch(pos=((0, 0, 0), (0, 0, 1))), shader.prog
self.use_draw_scale = False
def refresh(self):
depth = self.target_get_value("depth") / self.scale_value
self.matrix_offset.col[2][2] = depth # z-scaled
"""setup `custom_shape`"""
shader_wrapper = ExtrusionGuidesShader()
verts = [Vector((0, 0, 0)), Vector((0, 0, 1))]
verts, edges = shader_wrapper.process_geometry(verts)
self.custom_shape = shader_wrapper, shader_wrapper.batch(
pos=verts,
indices=edges,
)
def draw(self, ctx):
self.refresh()
self.draw_very_custom_shape(ctx, self.custom_shape)
def refresh(self):
depth = self.target_get_value("depth") / self.scale_value
self.matrix_offset.col[2][2] = depth # z-scaled
# TODO: dead code?
class DimensionLabelGizmo(types.Gizmo):
"""Text label for a dimension"""
@@ -487,6 +494,7 @@ class ExtrusionWidget(types.GizmoGroup):
theme = ctx.preferences.themes[0].user_interface
scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit)
# setup handle
gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d")
gz.matrix_basis = basis
gz.scale_basis = 0.1
@@ -497,10 +505,11 @@ class ExtrusionWidget(types.GizmoGroup):
gz.target_set_prop("offset", prop, "value")
gz.scale_value = scale_value
# setup guides
gz = self.guides = self.gizmos.new("BIM_GT_extrusion_guides")
gz.matrix_basis = basis
gz.color = gz.color_highlight = tuple(theme.gizmo_secondary)
gz.alpha = gz.alpha_highlight = 0.5
gz.alpha = gz.alpha_highlight = 0.75
gz.use_draw_modal = True
gz.target_set_prop("depth", prop, "value")
gz.scale_value = scale_value
@@ -23,8 +23,7 @@ from bpy.app.handlers import persistent
@persistent
def toggleDecorationsOnLoad(*args):
toggle = bpy.context.scene.DocProperties.should_draw_decorations
if toggle:
if bpy.context.scene.DocProperties.should_draw_decorations:
decoration.DecorationsHandler.install(bpy.context)
else:
decoration.DecorationsHandler.uninstall()
@@ -120,9 +120,9 @@ class BoundingBox:
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
# MeasureIt-ARCH is GPL-v3
# In the future I will need to rewrite this to allow the user to have custom
# settings for each annotation object, not read from Blender.
def format_distance(value, isArea=False, hide_units=True):
def format_distance(
value, isArea=False, hide_units=True, precision=None, decimal_places=None, suppress_zero_inches=False
):
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
# Get Scene Unit Settings
@@ -131,7 +131,7 @@ def format_distance(value, isArea=False, hide_units=True):
unit_length = bpy.context.scene.unit_settings.length_unit
toInches = 39.3700787401574887
inPerFoot = 11.999
inPerFoot = 11.9999
if isArea:
toInches = 1550
@@ -141,8 +141,7 @@ def format_distance(value, isArea=False, hide_units=True):
# Imperial Formatting
if unit_system == "IMPERIAL":
precision = bpy.context.scene.BIMProperties.imperial_precision
if precision == "NONE":
if not precision:
precision = 256
elif precision == "1":
precision = 1
@@ -155,13 +154,14 @@ def format_distance(value, isArea=False, hide_units=True):
# Separate ft and inches
# Unless Inches are the specified Length Unit
if unit_length != "INCHES":
feet = math.floor(decInches / inPerFoot)
feet = int(decInches / inPerFoot) # remove decimal
decInches -= feet * inPerFoot
else:
feet = 0
# Separate Fractional Inches
inches = math.floor(decInches)
decInches = abs(decInches) # ignore the sign for inches
inches = math.floor(decInches) # remove decimal
if inches != 0:
frac = round(base * (decInches - inches))
else:
@@ -186,44 +186,50 @@ def format_distance(value, isArea=False, hide_units=True):
inches = 0
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
if feet:
tx_dist += str(feet) + "'"
if feet and inches:
if feet and add_inches:
tx_dist += " - "
if inches:
if add_inches:
tx_dist += str(inches)
if inches and frac:
if add_inches and frac:
tx_dist += " "
if frac:
tx_dist += str(frac) + "/" + str(base)
if inches or frac:
if add_inches or frac:
tx_dist += '"'
else:
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
# METRIC FORMATTING
elif unit_system == "METRIC":
precision = bpy.context.scene.BIMProperties.metric_precision
if precision != 0:
if precision:
value = precision * round(float(value) / precision)
if decimal_places:
fmt = "%1." + str(decimal_places) + "f"
# Meters
if unit_length == "METERS":
fmt = "%1.3f"
if not decimal_places:
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
# Centimeters
elif unit_length == "CENTIMETERS":
fmt = "%1.1f"
if not decimal_places:
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
# Millimeters
elif unit_length == "MILLIMETERS":
fmt = "%1.0f"
if not decimal_places:
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
d_mm = value * (1000)
@@ -231,20 +237,21 @@ def format_distance(value, isArea=False, hide_units=True):
# Otherwise Use Adaptive Units
else:
if round(value, 2) >= 1.0:
if round(value, 2) >= 1.0 and not decimal_places:
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
else:
if round(value, 2) >= 0.01:
if round(value, 2) >= 0.01 and not decimal_places:
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
else:
fmt = "%1.0f"
if not decimal_places:
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
d_mm = value * (1000)
File diff suppressed because it is too large Load Diff
@@ -25,7 +25,7 @@ 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, DecoratorData
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
from blenderbim.bim.module.drawing.data import refresh as refresh_drawing_data
from pathlib import Path
from blenderbim.bim.prop import Attribute, StrProperty
@@ -44,20 +44,14 @@ from bpy.props import (
diagram_scales_enum = []
titleblocks_enum = []
sheets_enum = []
vector_styles_enum = []
def purge():
global diagram_scales_enum
global titleblocks_enum
global sheets_enum
global vector_styles_enum
diagram_scales_enum = []
titleblocks_enum = []
sheets_enum = []
vector_styles_enum = []
def update_target_view(self, context):
@@ -86,7 +80,7 @@ def update_diagram_scale(self, context):
)
except:
return
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
@@ -162,6 +156,21 @@ def update_drawing_name(self, context):
core.update_drawing_name(tool.Ifc, tool.Drawing, drawing=drawing, name=self.name)
def get_drawing_style_name(self):
"""needed to make `set_drawing_style_name` work"""
return self.get("name", "")
def set_drawing_style_name(self, new_value):
"""ensure the name is unique"""
scene = bpy.context.scene
drawing_styles = [s.name for s in scene.DocProperties.drawing_styles if s.name != self.name]
new_value = tool.Blender.ensure_unique_name(new_value, drawing_styles)
old_value = self.name
self["name"] = new_value
bpy.ops.bim.save_drawing_styles_data(rename_style=True, rename_style_from=old_value, rename_style_to=new_value)
def update_schedule_name(self, context):
schedule = tool.Ifc.get().by_id(self.ifc_definition_id)
core.update_schedule_name(tool.Ifc, tool.Drawing, schedule=schedule, name=self.name)
@@ -169,6 +178,10 @@ def update_schedule_name(self, context):
def update_has_underlay(self, context):
update_layer(self, context, "HasUnderlay", self.has_underlay)
# making sure that camera is active
if self.has_underlay and (context.active_object and context.active_object.data == self.id_data):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
def update_has_linework(self, context):
@@ -191,20 +204,14 @@ def update_layer(self, context, name, value):
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={name: value})
def getTitleblocks(self, context):
global titleblocks_enum
if len(titleblocks_enum) < 1:
titleblocks_enum.clear()
files = Path(os.path.join(context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
files = sorted([str(f.stem) for f in files])
titleblocks_enum.extend([(f, f, "") for f in files])
return titleblocks_enum
def get_titleblocks(self, context):
if not SheetsData.is_loaded:
SheetsData.load()
return SheetsData.data["titleblocks"]
def refreshTitleblocks(self, context):
global titleblocks_enum
titleblocks_enum.clear()
getTitleblocks(self, context)
def update_titleblocks(self, context):
SheetsData.data["titleblocks"] = SheetsData.titleblocks()
def toggleDecorations(self, context):
@@ -213,6 +220,9 @@ def toggleDecorations(self, context):
# TODO: design a proper text variable templating renderer
collection = context.scene.camera.users_collection[0]
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
continue
tool.Drawing.update_text_value(obj)
refresh_drawing_data()
decoration.DecorationsHandler.install(context)
@@ -220,16 +230,6 @@ def toggleDecorations(self, context):
decoration.DecorationsHandler.uninstall()
def getVectorStyles(self, context):
global vector_styles_enum
if len(vector_styles_enum) < 1:
sheets_enum.clear()
for filename in Path(os.path.join(context.scene.BIMProperties.data_dir, "styles")).glob("*.css"):
f = str(filename.stem)
vector_styles_enum.append((f, f, ""))
return vector_styles_enum
class Variable(PropertyGroup):
name: StringProperty(name="Name")
prop_key: StringProperty(name="Property Key")
@@ -239,6 +239,7 @@ class Drawing(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
name: StringProperty(name="Name", update=update_drawing_name)
target_view: StringProperty(name="Target View")
is_selected: BoolProperty(name="Is Selected", default=True)
class Schedule(PropertyGroup):
@@ -248,27 +249,17 @@ class Schedule(PropertyGroup):
class Sheet(PropertyGroup):
def set_name(self, new):
old = self.get("name")
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets")
if old and os.path.isfile(os.path.join(path, old + ".svg")):
os.rename(os.path.join(path, old + ".svg"), os.path.join(path, new + ".svg"))
self["name"] = new
def get_name(self):
return self.get("name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
identification: StringProperty(name="Identification")
name: StringProperty(name="Name", get=get_name, set=set_name)
name: StringProperty(name="Name")
is_sheet: BoolProperty(name="Is Sheet", default=False)
reference_type: StringProperty(name="Reference Type")
is_expanded: BoolProperty(name="Is Expanded", default=False)
class DrawingStyle(PropertyGroup):
name: StringProperty(name="Name")
raster_style: StringProperty(name="Raster Style")
name: StringProperty(name="Name", get=get_drawing_style_name, set=set_drawing_style_name)
raster_style: StringProperty(name="Raster Style", default="{}")
render_type: EnumProperty(
items=[
("NONE", "None", ""),
@@ -278,7 +269,6 @@ class DrawingStyle(PropertyGroup):
name="Render Type",
default="VIEWPORT",
)
vector_style: EnumProperty(items=getVectorStyles, name="Vector Style")
include_query: StringProperty(name="Include Query")
exclude_query: StringProperty(name="Exclude Query")
attributes: CollectionProperty(name="Attributes", type=StrProperty)
@@ -337,19 +327,34 @@ class DocProperties(PropertyGroup):
current_drawing_index: IntProperty(name="Current Drawing Index")
schedules: CollectionProperty(name="Schedules", type=Schedule)
active_schedule_index: IntProperty(name="Active Schedule Index")
titleblock: EnumProperty(items=getTitleblocks, name="Titleblock", update=refreshTitleblocks)
titleblock: EnumProperty(items=get_titleblocks, name="Titleblock", update=update_titleblocks)
is_editing_sheets: BoolProperty(name="Is Editing Sheets", default=False)
sheets: CollectionProperty(name="Sheets", type=Sheet)
active_sheet_index: IntProperty(name="Active Sheet Index")
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
decorations_colour: FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
titleblocks_dir: StringProperty(
default=os.path.join("layouts", "titleblocks") + os.path.sep, name="Default Titleblocks Directory"
)
drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory")
stylesheet_path: StringProperty(
default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet"
)
markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers")
symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols")
patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns")
shadingstyles_path: StringProperty(
default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles"
)
shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style")
class BIMCameraProperties(PropertyGroup):
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
@@ -357,6 +362,8 @@ class BIMCameraProperties(PropertyGroup):
view_name: StringProperty(name="View Name")
diagram_scale: EnumProperty(items=get_diagram_scales, name="Drawing Scale", update=update_diagram_scale)
custom_diagram_scale: StringProperty(name="Custom Scale", update=update_diagram_scale)
custom_diagram_scale_input1: StringProperty(name="Custom Scale Input 1", update=update_diagram_scale)
custom_diagram_scale_input2: StringProperty(name="Custom Scale Input 2", update=update_diagram_scale)
raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000)
is_nts: BoolProperty(name="Is NTS")
@@ -471,3 +478,57 @@ class BIMTextProperties(PropertyGroup):
class BIMAssignedProductProperties(PropertyGroup):
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object)
# ObjectType: annotation_name, description, icon, data_type
# fmt: off
ANNOTATION_TYPES_DATA = {
"DIMENSION": ("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"),
"ANGLE": ("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"),
"RADIUS": ("Radius", "", "FORWARD", "curve"),
"DIAMETER": ("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"),
"TEXT": ("Text", "", "SMALL_CAPS", "empty"),
"TEXT_LEADER": ("Leader", "", "TRACKING_BACKWARDS", "curve"),
"STAIR_ARROW": ("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"),
"HIDDEN_LINE": ("Hidden", "", "CON_TRACKTO", "mesh"),
"PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"),
"SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"),
"BREAKLINE": ("Breakline", "", "FCURVE", "mesh"),
"LINEWORK": ("Line", "", "MESH_MONKEY", "mesh"),
"BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"),
"FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"),
"FALL": ("Fall", "", "SORT_ASC", "curve"),
}
# fmt: on
annotation_classes = [(x, *ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(ANNOTATION_TYPES_DATA)]
def get_annotation_data_prop(prop_name):
def function(self, context):
if not AnnotationData.is_loaded:
AnnotationData.load()
return AnnotationData.data[prop_name]
return function
def update_annotation_object_type(self, context):
self.relating_type_id = "0"
# changing enum doesn't trigger refresh by itself
AnnotationData.is_loaded = False
class BIMAnnotationProperties(PropertyGroup):
object_type: bpy.props.EnumProperty(
name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type
)
relating_type_id: bpy.props.EnumProperty(
name="Relating Annotation Type", items=get_annotation_data_prop("relating_types")
)
create_representation_for_type: bpy.props.BoolProperty(
name="Create Representation For Type",
default=False,
description='Whether "Add type" should define a representation for the type \n'
"or allow occurences to have their own",
)
@@ -16,12 +16,41 @@
# 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/>.
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
import svgwrite
from odf.opendocument import load
from odf.opendocument import load as load_ods
from odf.table import Table, TableRow, TableColumn, TableCell
from odf.text import P
from odf.style import Style
from textwrap import wrap
from pathlib import Path
import string
FONT_SIZE = 4.13
FONT_WIDTH = lambda size: size * 0.45
FONT_SIZE_PT = 12
FONT_FAMILY = "OpenGost Type B TT"
DEBUG = False
def col2num(col):
"""convert letter column index to number:
`"A" -> 1`, `"AA" -> 27``
"""
num = 0
for c in col:
if c in string.ascii_letters:
num = num * 26 + (ord(c.upper()) - ord("A")) + 1
return num
def a1_to_rc(cell):
"""convert cell index from A1 format to RC: `"A1" -> (0,0)`"""
column_letter = cell.strip(string.digits)
col_number = col2num(column_letter) - 1
row_number = int(cell[len(column_letter) :]) - 1
return row_number, col_number
class Scheduler:
@@ -33,69 +62,328 @@ class Scheduler:
)
self.padding = 1
self.margin = 1
doc = load(infile)
doc = load_ods(infile)
# useful for debugging ods
if DEBUG:
import xml.dom.minidom
path = Path(infile)
dom = xml.dom.minidom.parseString(doc.xml())
pretty_xml = dom.toprettyxml()
with open(path.with_suffix(".xml"), "w") as fo:
fo.write(pretty_xml)
styles = {}
for style in doc.getElementsByType(Style):
name = style.getAttribute("name")
if not style.firstChild:
for cell_style in doc.getElementsByType(Style):
name = cell_style.getAttribute("name")
styles[name] = {}
# NOTE: there are also styles that inherit from parent styles that we do not process atm
if not cell_style.firstChild:
continue
styles[name] = {key[1]: value for key, value in style.firstChild.attributes.items()}
if cell_style.firstChild.tagName in ["style:table-column-properties", "style:table-row-properties"]:
style_children = [cell_style.firstChild]
else:
# for style:table-cell-properties we need to collect also text and paragraph properties
style_children = cell_style.childNodes
for child in style_children:
child_params = {key[1]: value for key, value in child.attributes.items()}
styles[name].update(child_params)
table = doc.getElementsByType(Table)[0]
# related styles stored as a list of tuples:
# [(child, parent), ...]
related_styles = []
# collect columns width
column_widths = []
column_styles = []
for col in table.getElementsByType(TableColumn):
style_name = col.getAttribute("stylename")
repeat = col.getAttribute("numbercolumnsrepeated")
repeat = int(repeat) if repeat else 1
for i in range(0, repeat):
col_repeat = col.getAttribute("numbercolumnsrepeated")
col_repeat = int(col_repeat) if col_repeat else 1
for i in range(col_repeat):
if not style_name or "column-width" not in styles[style_name]:
column_widths.append(50)
column_width = 50
else:
column_widths.append(self.convert_to_mm(styles[style_name]["column-width"]))
column_width = self.convert_to_mm(styles[style_name]["column-width"])
column_styles.append(style_name)
column_widths.append(column_width)
cell_style = col.getAttribute("defaultcellstylename")
if cell_style:
related_styles.append((style_name, cell_style))
# collect rows height
row_heights = []
# TODO: never used yet because unsure about priority for row styles
# over column styles or vice versa
row_styles = []
for col in table.getElementsByType(TableRow):
style_name = col.getAttribute("stylename")
row_repeat = col.getAttribute("numberrowsrepeated")
row_repeat = int(row_repeat) if row_repeat else 1
for i in range(row_repeat):
if not style_name or "row-height" not in styles[style_name]:
row_height = 6
else:
row_height = self.convert_to_mm(styles[style_name]["row-height"])
row_styles.append(style_name)
row_heights.append(row_height)
cell_style = col.getAttribute("defaultcellstylename")
if cell_style:
related_styles.append((style_name, cell_style))
while len(related_styles) > 0:
# unzip related styles to children and parents
children, parents = zip(*related_styles)
independent_styles = set(parents) - set(children)
for relation in related_styles[:]:
child, parent = relation
if parent in independent_styles:
child_style = styles[child]
styles[child] = styles[parent] | child_style
related_styles.remove(relation)
# TODO: multiple print ranges? 😔
print_range = table.getAttribute("printranges")
if print_range:
min_rc, max_rc = [a1_to_rc(cell.rsplit(".", 1)[1]) for cell in print_range.split(":")]
else:
# fallback if print range is not defined
n_rows = len(row_heights)
n_cols = len(column_widths)
n_cells = len(row_heights) * len(column_widths)
cells_limit = 10000
if n_cells >= cells_limit:
raise Exception(
f"You were about to build a very big table with number of cells more than {cells_limit}.\n"
f"In fact it is {n_rows} rows x {n_cols} cols = {n_cells} cells \n"
"and the operation was stopped to prevent system freeze.\n"
"Please define print range in .ods file to proceede\n"
"(needed to make sure printed table will have reasonable size)."
)
min_rc, max_rc = (0, 0), (1048576, 16384)
min_row, min_col = min_rc
max_row, max_col = max_rc
# draw table
y = self.margin
for tri, tr in enumerate(table.getElementsByType(TableRow)):
x = self.margin
height = 6
tdi = 0
for td in tr.getElementsByType(TableCell):
repeat = td.getAttribute("numbercolumnsrepeated")
repeat = int(repeat) if repeat else 1
for i in range(0, repeat):
width = column_widths[tdi]
self.svg.add(
self.svg.rect(
insert=(x, y),
size=(width, height),
style="fill: #ffffff; stroke-width:.125; stroke: #000000;",
tri = 0
stop_iterating_over_rows = False
# TODO: row spans support?
for tr in table.getElementsByType(TableRow):
if stop_iterating_over_rows:
break
row_repeat = tr.getAttribute("numberrowsrepeated")
row_repeat = int(row_repeat) if row_repeat else 1
for i_row_repeat in range(row_repeat):
if tri < min_row:
tri += 1
continue
elif tri > max_row:
stop_iterating_over_rows = True
break
x = self.margin
height = row_heights[tri]
tdi = 0
stop_iterating_over_columns = False
for td in tr.getElementsByType(TableCell):
if stop_iterating_over_columns:
break
column_span = td.getAttribute("numbercolumnsspanned")
column_span = int(column_span) if column_span else 1
col_repeat = td.getAttribute("numbercolumnsrepeated")
col_repeat = int(col_repeat) if col_repeat else 1
# figuring text alignment
cell_style = self.get_style(td.getAttribute("stylename"), styles)
# drawing cells and text
for i_col_repeat in range(col_repeat):
start_tdi = tdi
end_tdi = tdi + int(column_span) - 1
# if the entire span is beyond print range => continue
# if only part then keeping that part
if start_tdi < min_col:
if end_tdi < min_col:
tdi += column_span
continue
else:
start_tdi = min_col
# stop if start column is beyond print range
if start_tdi > max_col:
stop_iterating_over_columns = True
break
# making sure last column won't go beyond the print range
if end_tdi > max_col:
end_tdi = max_col
width = sum(column_widths[start_tdi : end_tdi + 1])
self.svg.add(
self.svg.rect(
insert=(x, y),
size=(width, height),
style="fill: #ffffff; stroke-width:.125; stroke: #000000;",
)
)
)
value = td.getElementsByType(P)
if value:
self.add_text(value[0], x + self.padding, y + self.padding)
x += width
tdi += 1
y += height
total_width = sum(column_widths) + (self.margin * 2)
p_tags = td.getElementsByType(P)
col_style = self.get_style(column_styles[tdi], styles)
final_cell_style = cell_style or col_style
box_alignment = self.get_box_alignment(final_cell_style)
wrap_text = final_cell_style.get("wrap-option", None) == "wrap"
bold_text = final_cell_style.get("font-weight", None) == "bold"
italic_text = final_cell_style.get("font-style", None) == "italic"
# NOTE: very naive since we're scaling text proportionally
font_size = (
float(final_cell_style.get("font-size", f"{FONT_SIZE_PT}pt")[:-2])
/ FONT_SIZE_PT
* FONT_SIZE
)
if p_tags:
# figuring text position based on alignment
text_position = [0.0, 0.0]
if box_alignment.endswith("left"):
text_position[0] = x + self.padding
elif box_alignment.endswith("middle") or box_alignment == "center":
text_position[0] = x + width / 2
elif box_alignment.endswith("right"):
text_position[0] = x + width - self.padding
if box_alignment.startswith("top"):
text_position[1] = y + self.padding
elif box_alignment.startswith("middle") or box_alignment == "center":
text_position[1] = y + height / 2
elif box_alignment.startswith("bottom"):
text_position[1] = y + height - self.padding
self.add_text(
p_tags,
*text_position,
font_size=font_size,
box_alignment=box_alignment,
wrap_text=wrap_text,
cell_width=width,
bold=bold_text,
italic=italic_text,
)
x += width
tdi += column_span
tri += 1
y += height
total_width = x + self.margin
total_height = y + self.margin
self.svg["width"] = "{}mm".format(total_width)
self.svg["height"] = "{}mm".format(y)
self.svg["viewBox"] = "0 0 {} {}".format(total_width, y)
self.svg["height"] = "{}mm".format(total_height)
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
self.svg.save(pretty=True)
def add_text(self, text, x, y):
self.svg.add(
self.svg.text(
str(text).upper(),
insert=tuple((x, y)),
**{
"font-size": 4.13,
"font-family": "OpenGost Type B TT",
"text-anchor": "start",
"alignment-baseline": "baseline",
"dominant-baseline": "hanging",
}
)
)
def get_style(self, style_name, styles):
style = styles[style_name] if style_name else {}
return style
def get_box_alignment(self, style):
if style and "vertical-align" in style and style["vertical-align"] != "automatic":
vertical_align = style["vertical-align"]
else:
vertical_align = "bottom"
alignment_translation = {
"center": "middle",
"end": "right",
"start": "left",
}
if style and "text-align" in style and style["text-align"] != "automatic":
horizontal_align = style["text-align"]
horizontal_align = alignment_translation.get(horizontal_align, horizontal_align)
else:
horizontal_align = "left"
if vertical_align == "middle" and horizontal_align == "middle":
box_alignment = "center"
else:
box_alignment = f"{vertical_align}-{horizontal_align}"
return box_alignment
def add_text(
self,
p_tags,
x,
y,
font_size,
box_alignment="bottom-left",
wrap_text=False,
cell_width=100,
bold=False,
italic=False,
):
"""
Adds text to svg.
Args:
p_tags: list of cell's P tags from odt file
box_alignment: alignment of text in box
wrap_text: if True, text will be wrapped to fit in cell
cell_width: width of cell, used for wrapping text
"""
text_lines = [str(p).upper() for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
text_params = {
"font-size": font_size,
"font-family": FONT_FAMILY,
}
if bold:
text_params["font-weight"] = "bold"
if italic:
text_params["font-style"] = "italic"
if len(text_lines) == 1 and not wrap_text:
text_params.update(box_alignment_params)
text_tag = self.svg.text(text_lines[0], insert=(x, y), **(text_params))
self.svg.add(text_tag)
return
text_tag = self.svg.text("", **(text_params | {"font-size": "0"} | box_alignment_params))
# TODO: should be done in less naive way
# without using magic number for FONT_WIDTH
# currently it might not work for all fonts and font sizes
if wrap_text:
wrapped_lines = []
for line in text_lines:
wrapped_line = wrap(line, width=int(cell_width // FONT_WIDTH(font_size)), break_long_words=False)
wrapped_lines.extend(wrapped_line)
else:
wrapped_lines = text_lines
for line_number, text_line in enumerate(wrapped_lines[::-1]):
# position has to be inserted at tspan to avoid x offset between tspans
tspan = self.svg.tspan(text_line, insert=(x, y), **text_params)
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
tspan.update({"dy": f"-{line_number}em"})
text_tag.add(tspan)
self.svg.add(text_tag)
def convert_to_mm(self, value):
# XSL is what defines the units of measurements in ODF
@@ -16,10 +16,200 @@
# 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 bgl
from mathutils import Matrix
from gpu.types import GPUShader
from gpu_extras.batch import batch_for_shader
import gpu
from mathutils import Vector
# NOTES:
# Since Metal doesn't support geometry shaders we stick to builtin shaders
# and generate all geometry data in python before passing it to the shader.
# This way was considered to be the most reliable atm.
# More: https://blender.stackexchange.com/questions/291674/migrating-geometry-shaders-to-metal
#
# BGL deprecation:
# since `bgl` is deprecated, creating smoothing lines became tricky
# Notes for creating shaders with smoothed lines:
# in geom shader - use triangle_strip, DEFAULT_SETUP, do_edge_verts or do_vertex to emit vertices
# in frag shader - use lineWidth uniform, smoothline flaot in, smoothing shader code from base shader
# mind the vertex limit since emitting vertices for smoothed lines produces twice as much vertices
BASE_DEF_GLSL = """
#define PI 3.141592653589793
#define MAX_POINTS 64
#define CIRCLE_SEGS 12
#define SMOOTH_WIDTH 1.0
#define lineSmooth true
"""
BASE_LIB_GLSL = """
// TODO: redefine as macor instead
uniform vec2 winsize;
uniform float lineWidth;
#define half_winsize (winsize / 2)
// convert camera to window
#define C2W(v) vec4(v.x * half_winsize.x / v.w, v.y * half_winsize.y / v.w, v.z / v.w, 1)
// convert window to camera
#define W2C(v) vec4(v.x * v.w / half_winsize.x, v.y * v.w / half_winsize.y, v.z * v.w, 1)
void emitSegment(vec4 p0, vec4 p1) {
gl_Position = p0;
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
}
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
gl_Position = p1;
EmitVertex();
gl_Position = p2;
EmitVertex();
gl_Position = p3;
EmitVertex();
EndPrimitive();
}
#define matCLIP2WIN() vec4(winsize.x/2, winsize.y/2, 1, 1)
#define CLIP2WIN(v) (clip2win * (v) / (v).w)
#define matWIN2CLIP() vec4(2/winsize.x, 2/winsize.y, 1, 1)
#define WIN2CLIP(v) (win2clip * (v) * (v).w)
#define DEFAULT_SETUP() vec4 clip2win = matCLIP2WIN(), win2clip = matWIN2CLIP(); vec2 EDGE_DIR
out float smoothline;
void arrow_head(in vec4 dir, in float size, in float angle, out vec4 head[3]) {
vec4 nose = dir * size;
float c = cos(angle), s = sin(angle);
head[0] = nose;
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
}
void circle_head(in float size, out vec4 head[CIRCLE_SEGS]) {
float angle_d = PI * 2 / CIRCLE_SEGS;
for(int i = 0; i<CIRCLE_SEGS; i++) {
float angle = angle_d * i;
head[i] = vec4(cos(angle), sin(angle), 0, 0) * size;
}
}
bool check_counterclockwise(in vec4 A, in vec4 B, in vec4 C) {
return (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x);
}
void angle_circle_head(
in vec4 circle_start, in float circle_angle,
in bool counterclockwise,
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
// 1 added to CIRCLE_SEGS because we're number of vertices
// for n segments is n+1
float angle_d;
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
angle_segs = max(1, ceil(circle_angle / angle_d));
angle_d = circle_angle / angle_segs;
for(int i = 0; i < (angle_segs + 1); i++) {
float angle = angle_d * i;
if (counterclockwise) {
head[i] = vec4(
circle_start.x * cos(-angle) + circle_start.y * sin(-angle),
circle_start.x * -sin(-angle) + circle_start.y * cos(-angle),
0, 0
);
} else {
head[i] = vec4(
circle_start.x * cos(angle) + circle_start.y * sin(angle),
circle_start.x * -sin(angle) + circle_start.y * cos(angle),
0, 0
);
}
}
}
void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
vec4 nose = dir * size;
float c = cos(PI/2), s = sin(PI/2);
head[0] = nose;
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
}
// do_vertex_util is custom EmitVertex method to support line smoothing
// `e` - edge xy direction in win/clip space (directions are invariant in both spaces)
#define do_vertex(pos, e) (do_vertex_util(pos, vec2(-(e).y, (e).x) / winsize.xy))
#define do_vertex_win(pos, e) ( do_vertex( WIN2CLIP( pos ), e ) )
// if vertex is shared by two segments of the line still need to emit it twice
// to avoid smoothing artifacts
// don't forget to initialize `vec2 EDGE_DIR` for macro to work
// `pos0` / `pos1` - vertex position in clip space
#define do_edge_verts(pos0, pos1) ( EDGE_DIR = normalize( ( (pos1) - (pos0) ).xy ), do_vertex( pos0, EDGE_DIR ), do_vertex( pos1, EDGE_DIR) )
#define do_edge_verts_win(pos0, pos1) ( do_edge_verts( WIN2CLIP(pos0), WIN2CLIP(pos1) ) )
void do_vertex_util(vec4 pos, vec2 ofs)
{
float final_line_width = lineWidth + SMOOTH_WIDTH * float(lineSmooth);
ofs *= final_line_width;
smoothline = final_line_width * 0.5;
gl_Position = pos;
gl_Position.xy += ofs * pos.w;
EmitVertex();
smoothline = -final_line_width * 0.5;
gl_Position = pos;
gl_Position.xy -= ofs * pos.w;
EmitVertex();
}
// geometry utils
void triangle_head(in vec4 side, in vec4 dir, in float length, in float width, in float radius, out vec4 head[5]) {
// TODO: radius is unnecessary?
head[0] = side * -radius;
head[1] = side * length * -.5;
head[2] = dir * width;
head[3] = side * length * .5;
head[4] = side * radius;
}
void do_triangle_head(vec4 pos_w, vec4 head[5]) {
DEFAULT_SETUP();
do_edge_verts_win(pos_w + head[0], pos_w + head[1]);
do_edge_verts_win(pos_w + head[1], pos_w + head[2]);
do_edge_verts_win(pos_w + head[2], pos_w + head[3]);
do_edge_verts_win(pos_w + head[3], pos_w + head[4]);
}
void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) {
DEFAULT_SETUP();
for(int i=0; i<CIRCLE_SEGS-1; i++) {
do_edge_verts_win(pos_w + head[i], pos_w + head[i+1]);
EmitVertex();
}
do_edge_verts_win( pos_w + head[CIRCLE_SEGS-1], pos_w + head[0] );
}
"""
def add_verts_sequence(verts, start_i, output_verts, output_edges, closed=False):
"""Add sequence of verts to output lists, returns next vertex index"""
for i, v in enumerate(verts[:-1], start_i):
output_verts.append(v)
output_edges.append((i, i + 1))
output_verts.append(verts[-1])
if closed:
output_edges.append((i + 1, start_i))
return i + 2
def add_offsets(v, offsets):
"""returns list of verts with offsets added"""
return [v + offset for offset in offsets]
class BaseShader:
@@ -28,7 +218,7 @@ class BaseShader:
The Geometry shader works in clipping coords (aftre projecting before division and window scaling).
To make window-scale geometry, vectors should be calculated in window space and than back-projected to clipping spae.
Provide `winSize` uniform vector with halfsize of window, and use W2C and C2W macros.
Provide `half_winsize` uniform vector with halfsize of window, and use W2C and C2W macros.
Replace glsl code in derived classes.
Beware of unused attributes and uniforms: glsl compiler will optimize them out and blender fail with an exception.
@@ -43,9 +233,7 @@ class BaseShader:
TYPE = None # should be LINES|POINTS|etc
DEF_GLSL = """
#define PI 3.141592653589793
"""
DEF_GLSL = BASE_DEF_GLSL
VERT_GLSL = """
uniform mat4 viewMatrix;
@@ -63,84 +251,86 @@ class BaseShader:
"""
# prepended to geom_glsl
LIB_GLSL = """
uniform vec2 winSize;
// convert camera to window
#define C2W(v) vec4(v.x * winSize.x / v.w, v.y * winSize.y / v.w, v.z / v.w, 1)
// convert window to camera
#define W2C(v) vec4(v.x * v.w / winSize.x, v.y * v.w / winSize.y, v.z * v.w, 1)
void emitSegment(vec4 p0, vec4 p1) {
gl_Position = p0;
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
}
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
gl_Position = p1;
EmitVertex();
gl_Position = p2;
EmitVertex();
gl_Position = p3;
EmitVertex();
EndPrimitive();
}
"""
LIB_GLSL = BASE_LIB_GLSL
GEOM_GLSL = """
uniform float viewportDrawingScale;
layout(lines) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
// default setup for macro to work
vec4 clip2win = matCLIP2WIN();
vec4 win2clip = matWIN2CLIP();
vec2 EDGE_DIR;
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
do_edge_verts(p0, p1);
EndPrimitive();
}
"""
FRAG_GLSL = """
uniform vec4 color;
uniform float lineWidth;
in float smoothline;
out vec4 fragColor;
void main() {
vec2 co = gl_FragCoord.xy;
fragColor = color;
if (lineSmooth) {
fragColor.a *= clamp((lineWidth + SMOOTH_WIDTH) * 0.5 - abs(smoothline), 0.0, 1.0);
}
}
"""
def __init__(self):
# NB: libcode arg doesn't work
self.prog = GPUShader(
vertexcode=self.VERT_GLSL,
fragcode=self.FRAG_GLSL,
geocode=self.LIB_GLSL + self.GEOM_GLSL,
defines=self.DEF_GLSL,
)
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.base_shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
def get_shader(self):
"""Returns shader for this type"""
return self.line_shader if self.TYPE == "LINES" else self.base_shader
def batch(self, indices=None, **data):
"""Returns automatic GPUBatch filled with provided parameters"""
batch = batch_for_shader(self.prog, self.TYPE, data, indices=indices)
batch.program_set(self.prog)
shader = self.get_shader()
batch = batch_for_shader(shader, self.TYPE, data, indices=indices)
return batch
def bind(self):
self.prog.bind()
"""need to bind shader before changing it's uniforms"""
shader = self.get_shader()
shader.bind()
return shader
def glenable(self):
bgl.glEnable(bgl.GL_BLEND)
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
bgl.glBlendEquation(bgl.GL_FUNC_ADD)
# bgl.glEnable(bgl.GL_DEPTH_TEST)
# bgl.glDepthFunc(bgl.GL_LEQUAL)
# bgl.glDepthMask(True)
gpu.state.blend_set("ALPHA")
gpu.state.depth_test_set("LESS_EQUAL")
def uniform_region(self, ctx):
shader = self.bind()
region = ctx.region
region3d = ctx.region_data
try:
self.prog.uniform_float("viewMatrix", region3d.perspective_matrix)
except ValueError: # unused uniform
pass
try:
self.prog.uniform_float("winSize", (region.width / 2, region.height / 2))
except ValueError: # unused uniform
pass
uniform_floats = {
"ModelViewProjectionMatrix": region3d.perspective_matrix,
# POLYLINE_UNIFORM_COLOR specific uniforms
"viewportSize": (region.width, region.height),
"lineWidth": 2.5,
}
for name, value in uniform_floats.items():
shader.uniform_float(name, value)
# TODO: add smoothing if this shader is going to be used
# TODO: dead code?
class BaseLinesShader(BaseShader):
"""Draws line segments with gaps around vertices at endpoints"""
@@ -185,7 +375,6 @@ class BaseLinesShader(BaseShader):
def glenable(self):
super().glenable()
bgl.glEnable(bgl.GL_LINE_SMOOTH)
class GizmoShader(BaseShader):
@@ -207,6 +396,7 @@ class GizmoShader(BaseShader):
"""
# TODO: add smoothing if this shader is going to be used
class DotsGizmoShader(GizmoShader):
"""Draws circles of radius 1 around points"""
@@ -263,35 +453,24 @@ class ExtrusionGuidesShader(GizmoShader):
TYPE = "LINES"
DEF_GLSL = (
BaseShader.DEF_GLSL
+ """
#define CROSS_SIZE .5
"""
)
def process_geometry(self, verts):
CROSS_SIZE = 0.5
GEOM_GLSL = """
uniform mat4 ModelViewProjectionMatrix;
p0, p1 = verts
bx = Vector((1, 0, 0)) * CROSS_SIZE
by = Vector((0, 1, 0)) * CROSS_SIZE
layout(lines) in;
layout(line_strip, max_vertices=10) out;
output_verts = []
output_edges = []
out_kwargs = {
"output_verts": output_verts,
"output_edges": output_edges,
}
void main() {
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
vec4 p0w = C2W(p0), p1w = C2W(p1);
start_i = 0
start_i = add_verts_sequence(add_offsets(p0, [-bx, bx]), start_i, **out_kwargs)
start_i = add_verts_sequence(add_offsets(p0, [-by, by]), start_i, **out_kwargs)
start_i = add_verts_sequence(add_offsets(p1, [-bx, bx]), start_i, **out_kwargs)
start_i = add_verts_sequence(add_offsets(p1, [-by, by]), start_i, **out_kwargs)
emitSegment(p0, p1);
vec4 bx = ModelViewProjectionMatrix[0] * CROSS_SIZE;
vec4 by = ModelViewProjectionMatrix[1] * CROSS_SIZE;
emitSegment(p0 - bx, p0 + bx);
emitSegment(p0 - by, p0 + by);
emitSegment(p1 - bx, p1 + bx);
emitSegment(p1 - by, p1 + by);
}
"""
def glenable(self):
super().glenable()
bgl.glEnable(bgl.GL_LINE_SMOOTH)
return output_verts, output_edges
@@ -17,15 +17,20 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import uuid
import shutil
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
from mathutils import Vector
VIEW_TITLE_OFFSET_Y = 5
DEFAULT_POSITION = Vector((30, 30))
class SheetBuilder:
@@ -33,22 +38,29 @@ class SheetBuilder:
self.data_dir = None
self.scale = "NTS"
def create(self, sheet_path, titleblock_name):
def create(self, layout_path, titleblock_name):
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
root.attrib["id"] = "root"
root.attrib["version"] = "1.1"
view_root = ET.parse(
os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
).getroot()
sheet_dir = os.path.dirname(layout_path)
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
os.makedirs(sheet_dir, exist_ok=True)
os.makedirs(os.path.dirname(titleblock_path), exist_ok=True)
if not os.path.exists(titleblock_path):
shutil.copy(ootb_titleblock_path, titleblock_path)
view_root = ET.parse(titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
view = ET.SubElement(root, "g")
view.attrib["data-type"] = "titleblock"
titleblock = ET.SubElement(view, "image")
titleblock.attrib["xlink:href"] = "../templates/titleblocks/" + titleblock_name + ".svg"
titleblock.attrib["xlink:href"] = os.path.relpath(titleblock_path, sheet_dir)
titleblock.attrib["x"] = "0"
titleblock.attrib["y"] = "0"
titleblock.attrib["width"] = str(view_width)
@@ -58,26 +70,25 @@ class SheetBuilder:
root.attrib["height"] = "{}mm".format(view_height)
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
with open(sheet_path, "w") as f:
with open(layout_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
def add_drawing(self, reference, drawing, sheet):
filename = drawing.Name
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_dir = os.path.join(self.data_dir, "sheets")
drawing_dir = os.path.join(self.data_dir, "diagrams")
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
drawing_path = os.path.join(drawing_dir, filename + ".svg")
underlay_path = os.path.join(drawing_dir, filename + "-underlay.png")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
if not os.path.isfile(sheet_path):
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
if not os.path.exists(layout_path) or not os.path.exists(drawing_path):
raise FileNotFoundError
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
sheet_tree = ET.parse(sheet_path)
sheet_root = sheet_tree.getroot()
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
view_tree = ET.parse(drawing_path)
view_root = view_tree.getroot()
@@ -85,103 +96,161 @@ class SheetBuilder:
# The view is placed into a group with a background image element.
# Although the foreground SVG already has a background, it is duplicated
# here to accommodate browsers which do not nest images.
view = ET.SubElement(sheet_root, "g")
view = ET.SubElement(layout_root, "g")
view.attrib["data-type"] = "drawing"
view.attrib["data-id"] = str(reference.id())
view.attrib["data-drawing"] = drawing.GlobalId
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
# add background
if os.path.isfile(underlay_path):
background = ET.SubElement(view, "image")
background.attrib["data-type"] = "background"
background.attrib["xlink:href"] = os.path.relpath(underlay_path, sheet_dir)
background.attrib["x"] = "30"
background.attrib["y"] = "30"
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
background.attrib["x"] = str(DEFAULT_POSITION.x)
background.attrib["y"] = str(DEFAULT_POSITION.y)
background.attrib["width"] = str(view_width)
background.attrib["height"] = str(view_height)
# add foreground
if os.path.isfile(drawing_path):
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "foreground"
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, sheet_dir)
foreground.attrib["x"] = "30"
foreground.attrib["y"] = "30"
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(30, view_height + 35, view)
sheet_tree.write(sheet_path)
self.add_view_title(
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
)
layout_tree.write(layout_path)
def update_sheet_drawing_sizes(self, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
SVG = "{http://www.w3.org/2000/svg}"
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
ifc_file = tool.Ifc.get()
# iterate over all drawings in the sheet
drawings_views = layout_root.findall(f'{SVG}g[@data-type="drawing"]')
for drawing_view in drawings_views:
# find drawing in ifc file to get the drawing dimensions
drawing = ifc_file.by_guid(drawing_view.attrib.get("data-drawing"))
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
drawing_tree = ET.parse(drawing_path)
drawing_root = drawing_tree.getroot()
view_width = self.convert_to_mm(drawing_root.attrib.get("width"))
view_height = self.convert_to_mm(drawing_root.attrib.get("height"))
foreground = drawing_view.find(f'.//{SVG}image[@data-type="foreground"]')
height = float(foreground.attrib["height"])
width = float(foreground.attrib["width"])
readjust = Vector((width - view_width, height - view_height)) / 2
for image in drawing_view.findall(f"{SVG}image"):
x = float(image.attrib["x"])
y = float(image.attrib["y"])
if image.attrib["data-type"] == "view-title":
image.attrib["x"] = str(x - readjust.x)
image.attrib["y"] = str(y - readjust.y)
else:
image.attrib["x"] = str(x + readjust.x)
image.attrib["y"] = str(y + readjust.y)
image.attrib["width"] = str(view_width)
image.attrib["height"] = str(view_height)
layout_tree.write(layout_path)
def remove_drawing(self, reference, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
sheet_path = tool.Drawing.get_document_uri(sheet)
sheet_tree = ET.parse(sheet_path)
sheet_root = sheet_tree.getroot()
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
for g in sheet_root.findall("{http://www.w3.org/2000/svg}g"):
for g in layout_root.findall("{http://www.w3.org/2000/svg}g"):
if g.attrib.get("data-id") == str(reference.id()):
sheet_root.remove(g)
layout_root.remove(g)
break
sheet_tree.write(sheet_path)
layout_tree.write(layout_path)
def add_schedule(self, reference, schedule, sheet):
view_path = tool.Drawing.get_document_uri(schedule)
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_schedule(schedule)
schedule_name = os.path.splitext(os.path.basename(view_path))[0]
sheet_path = tool.Drawing.get_document_uri(sheet)
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
sheet_tree = ET.parse(sheet_path)
sheet_root = sheet_tree.getroot()
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
view_tree = ET.parse(view_path)
view_root = view_tree.getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
view = ET.SubElement(sheet_root, "g")
view = ET.SubElement(layout_root, "g")
view.attrib["data-type"] = "schedule"
view.attrib["data-id"] = str(reference.id())
view.attrib["data-schedule"] = str(schedule.id())
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "table"
foreground.attrib["xlink:href"] = "../schedules/{}.svg".format(schedule_name)
foreground.attrib["x"] = "30"
foreground.attrib["y"] = "30"
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(30, view_height + 35, view)
sheet_tree.write(sheet_path)
self.add_view_title(
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
)
layout_tree.write(layout_path)
def add_view_title(self, x, y, parent):
title_tree = ET.parse(os.path.join(self.data_dir, "templates", "view-title.svg"))
def add_view_title(self, x, y, parent, layout_dir):
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
ootb_title = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "view-title.svg")
shutil.copy(ootb_title, title_path)
title_tree = ET.parse(title_path)
title_root = title_tree.getroot()
title = ET.SubElement(parent, "image")
title.attrib["data-type"] = "view-title"
title.attrib["xlink:href"] = "../templates/view-title.svg"
title.attrib["xlink:href"] = os.path.relpath(title_path, layout_dir)
title.attrib["x"] = str(x)
title.attrib["y"] = str(y)
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet):
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
self.references = {"SHEET": None, "RASTER": []}
sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
self.layout_dir = os.path.dirname(layout_path)
sheet_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_sheet_path(sheet[0], sheet.Name))
self.sheets_dir = os.path.dirname(sheet_path)
os.makedirs(self.sheets_dir, exist_ok=True)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
tree = ET.parse(sheet_path)
tree = ET.parse(layout_path)
root = tree.getroot()
self.defs = ET.Element("defs")
@@ -189,11 +258,15 @@ class SheetBuilder:
self.build_titleblock(root, sheet)
self.build_drawings(root, sheet)
self.build_schedules(root)
self.build_schedules(root, sheet)
with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
with open(sheet_path, "wb") as output:
tree.write(output)
self.references["SHEET"] = sheet_path
return self.references
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]
@@ -208,8 +281,6 @@ class SheetBuilder:
titleblock.remove(image)
def build_drawings(self, root, sheet):
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
@@ -232,12 +303,15 @@ class SheetBuilder:
view.append(self.parse_embedded_svg(foreground, {}))
if background is not None:
background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
background_path = os.path.join(self.layout_dir, self.get_href(background))
raster_path = os.path.join(self.sheets_dir, os.path.basename(background_path))
shutil.copy(background_path, raster_path)
self.references["RASTER"].append(raster_path)
if view_title is not None:
foreground_path = self.get_href(foreground)
data = reference.get_info()
data.update({"Sheet" + k: v for k, v in sheet.get_info().items()})
if not data["Name"]:
data["Name"] = ntpath.basename(foreground_path)[0:-4]
data["Scale"] = tool.Drawing.get_drawing_human_scale(drawing)
@@ -246,7 +320,7 @@ class SheetBuilder:
for image in images:
view.remove(image)
def build_schedules(self, root):
def build_schedules(self, root, sheet):
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'):
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"]))
@@ -268,6 +342,7 @@ class SheetBuilder:
if view_title is not None:
path = self.get_href(table)
data = reference.get_info()
data.update({"Sheet" + k: v for k, v in sheet.get_info().items()})
if not data["Name"]:
data["Name"] = schedule.Name or "Unnamed"
view.append(self.parse_embedded_svg(view_title, data))
@@ -298,7 +373,7 @@ class SheetBuilder:
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:
with open(os.path.join(self.layout_dir, svg_path), "r") as template:
embedded = ET.fromstring(pystache.render(template.read(), data))
# viewBox should not be nested
embedded.attrib["viewBox"] = ""
@@ -314,6 +389,39 @@ class SheetBuilder:
group.append(child)
return group
def change_titleblock(self, sheet, titleblock_name):
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
sheet_dir = os.path.dirname(sheet_path)
os.makedirs(sheet_dir, exist_ok=True)
os.makedirs(os.path.dirname(titleblock_path), exist_ok=True)
if not os.path.exists(titleblock_path):
shutil.copy(ootb_titleblock_path, titleblock_path)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
view_root = ET.parse(ootb_titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
sheet_tree = ET.parse(sheet_path)
root = sheet_tree.getroot()
titleblock = sheet_tree.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image[@{http://www.w3.org/1999/xlink}href]")[0]
image.attrib["{http://www.w3.org/1999/xlink}href"] = os.path.relpath(titleblock_path, sheet_dir)
image.attrib["width"] = str(view_width)
image.attrib["height"] = str(view_height)
root.attrib["width"] = "{}mm".format(view_width)
root.attrib["height"] = "{}mm".format(view_height)
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
sheet_tree.write(sheet_path)
def convert_to_mm(self, value):
# CSS is what defines these possibilities
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
@@ -21,6 +21,7 @@ import re
import bpy
import math
import bmesh
import shutil
import pystache
import mathutils
import xml.etree.ElementTree as ET
@@ -65,10 +66,9 @@ class SvgWriter:
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
self.related_paths = None
self.resource_paths = {}
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,
@@ -85,35 +85,24 @@ class SvgWriter:
self.svg.save(pretty=True)
def draw_underlay(self, image):
self.svg.add(
self.svg.image(
os.path.join("..", "diagrams", os.path.basename(image)),
width=self.width,
height=self.height,
)
)
self.svg.add(self.svg.image(os.path.basename(image), width=self.width, height=self.height))
return self
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 setup_drawing_resource_paths(self, element):
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"):
resource_path = pset.get(resource)
if not resource_path:
self.resource_paths[resource] = None
continue
resource_path = tool.Ifc.resolve_uri(resource_path)
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
if not os.path.exists(resource_path):
resource_basename = os.path.basename(resource_path)
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
if os.path.exists(ootb_resource):
shutil.copy(ootb_resource, resource_path)
self.resource_paths[resource] = resource_path
def define_boilerplate(self):
self.add_stylesheet()
@@ -132,33 +121,43 @@ class SvgWriter:
self.height = self.raw_height * self.svg_scale
def add_stylesheet(self):
with open(self.related_paths["Stylesheet"], "r") as stylesheet:
if not self.resource_paths["Stylesheet"] or not os.path.exists(self.resource_paths["Stylesheet"]):
return
with open(self.resource_paths["Stylesheet"], "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self):
tree = ET.parse(self.related_paths["Markers"])
if not self.resource_paths["Markers"] or not os.path.exists(self.resource_paths["Markers"]):
return
tree = ET.parse(self.resource_paths["Markers"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_symbols(self):
tree = ET.parse(self.related_paths["Symbols"])
if not self.resource_paths["Symbols"] or not os.path.exists(self.resource_paths["Symbols"]):
return
tree = ET.parse(self.resource_paths["Symbols"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def find_xml_symbol_by_id(self, id):
tree = ET.parse(self.related_paths["Symbols"])
tree = ET.parse(self.resource_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"])
if not self.resource_paths["Patterns"] or not os.path.exists(self.resource_paths["Patterns"]):
return
tree = ET.parse(self.resource_paths["Patterns"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def draw_annotations(self, annotations):
def draw_annotations(self, annotations, precision, decimal_places):
self.precision = precision
self.decimal_places = decimal_places
for element in annotations:
obj = tool.Ifc.get_object(element)
if not obj or element.ObjectType == "DRAWING":
@@ -226,21 +225,24 @@ class SvgWriter:
)
)
# TODO: allow metric to be configurable
rl = (matrix_world @ points[0].co.xyz).z
rl_value = (matrix_world @ points[0].co.xyz).z
if bpy.context.scene.unit_settings.system == "IMPERIAL":
rl = helper.format_distance(rl)
rl = helper.format_distance(rl_value, precision=self.precision, decimal_places=self.decimal_places)
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rl /= unit_scale
rl = rl_value / unit_scale
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
rl *= unit_scale
rl = "{:.3f}m".format(rl)
text_style = {
"text-anchor": "start",
"alignment-baseline": "baseline",
"dominant-baseline": "baseline",
}
self.svg.add(self.svg.text(f"RL +{rl}", insert=tuple(text_position), class_="SECTIONLEVEL", **text_style))
text_style = SvgWriter.get_box_alignment_parameters("bottom-left")
self.svg.add(
self.svg.text(
"RL {}{}".format("" if rl_value < 0 else "+", 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))
@@ -268,11 +270,7 @@ class SvgWriter:
"UP",
insert=tuple(text_position),
class_="STAIR",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
},
**SvgWriter.get_box_alignment_parameters("center"),
)
)
@@ -296,16 +294,13 @@ class SvgWriter:
)
line["stroke-dasharray"] = "12.5, 3, 3, 3"
axis_tag = tool.Ifc.get_entity(obj).Name
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
axis_tag,
insert=tuple(start * self.svg_scale),
class_="GRID",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
},
**text_style,
)
)
self.svg.add(
@@ -313,11 +308,7 @@ class SvgWriter:
axis_tag,
insert=tuple(end * self.svg_scale),
class_="GRID",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
},
**text_style,
)
)
@@ -356,7 +347,7 @@ class SvgWriter:
def get_attribute_classes(self, obj):
element = tool.Ifc.get_entity(obj)
global_id = "GlobalId-{}".format(element.GlobalId)
predefined_type = "PredefinedType-" + self.canonicalise_class_name(
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
str(ifcopenshell.util.element.get_predefined_type(element))
)
classes = [global_id, element.is_a(), predefined_type]
@@ -366,12 +357,11 @@ class SvgWriter:
for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key)
if value:
classes.append(self.canonicalise_class_name(key) + "-" + self.canonicalise_class_name(str(value)))
classes.append(
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
)
return classes
def canonicalise_class_name(self, name):
return re.sub("[^0-9a-zA-Z]+", "", name)
def draw_line_annotation(self, obj):
# TODO: properly scope these offsets
x_offset = self.raw_width / 2
@@ -574,11 +564,7 @@ class SvgWriter:
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",
}
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
reference_id,
@@ -611,11 +597,7 @@ class SvgWriter:
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",
}
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
reference_id, insert=(text_position[0], text_position[1] - 2.5), class_="ELEVATION", **text_style
@@ -642,6 +624,44 @@ class SvgWriter:
return (reference_id, sheet_id)
return ("-", "-")
@staticmethod
def get_box_alignment_parameters(box_alignment):
"""Convenience method to get svg parameters for text alignment
in a readable way.
Metehod expecting values like:
`top-left`, `top-middle`, `top-right`,
`middle-left`, `center`, `middle-right`,
`bottom-left`, `bottom-middle`, `bottom-right`
"""
# 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)]
# 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,
}
def draw_text_annotation(self, text_obj, position):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -652,21 +672,28 @@ class SvgWriter:
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
text_position_svg_str = ", ".join(map(str, text_position_svg))
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)
angle = math.degrees(
(Vector((x_offset + projected_x_axis.x, y_offset - projected_x_axis.y)) - text_position).angle_signed(
Vector((1, 0))
)
)
def get_basis_vector(matrix, i=0):
"""returns basis vector for i in world space, unaffected by object scale"""
return matrix.inverted()[i].to_3d().normalized()
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
classes_str = " ".join(self.get_attribute_classes(text_obj))
text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world)
text_dir = (self.camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
angle = math.degrees(-text_dir.angle_signed(Vector((1, 0))))
classes = self.get_attribute_classes(text_obj)
classes_str = " ".join(classes)
symbol = tool.Drawing.get_annotation_symbol(element)
template_text_fields = []
if symbol:
if not symbol:
text_transform = f"translate({text_position_svg_str}) rotate({angle})"
else:
# NOTE: for now we assume that scale is uniform
symbol_transform = f"translate({text_position_svg_str}) rotate({angle}) scale({text_obj.scale.x})"
text_transform = symbol_transform
symbol_svg = self.find_xml_symbol_by_id(symbol)
if symbol_svg:
symbol_xml = symbol_svg.get_xml()
@@ -674,7 +701,7 @@ class SvgWriter:
# 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["transform"] = symbol_transform
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):
@@ -684,35 +711,7 @@ class SvgWriter:
return None
if not symbol_svg or not template_text_fields:
self.svg.add(self.svg.use(f"#{symbol}", insert=text_position_svg))
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)]
# 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,
}
self.svg.add(self.svg.use(f"#{symbol}", transform=symbol_transform))
for text_literal in text_literals:
# after pretty indentation some redundant spaces can occur in svg tags
@@ -721,22 +720,33 @@ class SvgWriter:
# 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)
attribs = {
"transform": text_transform,
"style": "font-size: 0;",
}
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 add_text_tag(add_fill_bg):
text_tag = self.svg.text(
"",
**(attribs | {"filter": "url(#fill-background)"}) if add_fill_bg else attribs,
**SvgWriter.get_box_alignment_parameters(text_literal.BoxAlignment),
)
self.svg.add(text_tag)
text_lines = text.replace("\\n", "\n").split("\n")
for line_number, text_line in enumerate(text_lines):
# position has to be inserted at tspan to avoid x offset between tspans
# note that tspan doesn't support using `transform` attribute
# so we use (0,0) position because tspan is already offseted by text transform
tspan = self.svg.tspan(text_line, class_=classes_str, insert=(0, 0))
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
tspan.update({"dy": f"{line_number}em"})
text_tag.add(tspan)
if "fill-bg" in classes:
add_text_tag(True)
add_text_tag(False)
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
@@ -784,29 +794,24 @@ class SvgWriter:
)
)
# TODO: allow metric to be configurable
rl = ((matrix_world @ points[0].co).xyz + obj.location).z
rl_value = (matrix_world @ points[0].co).z
if bpy.context.scene.unit_settings.system == "IMPERIAL":
rl = helper.format_distance(rl)
rl = helper.format_distance(rl_value, precision=self.precision, decimal_places=self.decimal_places)
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rl /= unit_scale
rl = rl_value / unit_scale
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
rl *= unit_scale
rl = "{:.3f}m".format(rl)
if projected_points[0].x > projected_points[-1].x:
text_anchor = "end"
else:
text_anchor = "start"
box_alignment = "bottom-left" if projected_points[0].x <= projected_points[-1].x else "bottom-right"
text_style = SvgWriter.get_box_alignment_parameters(box_alignment)
self.svg.add(
self.svg.text(
"RL +{}".format(rl),
"RL {}{}".format("" if rl_value < 0 else "+", rl),
insert=tuple(text_position),
class_="PLANLEVEL",
**{
"text-anchor": text_anchor,
"alignment-baseline": "baseline",
"dominant-baseline": "baseline",
},
**text_style,
)
)
@@ -922,12 +927,7 @@ class SvgWriter:
text_offset = (text_position - center_position).xy.normalized() * 5
text_position += text_offset
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
text_style = SvgWriter.get_box_alignment_parameters("center")
angle_text = abs(round(math.degrees(angle), 3))
if is_reflex:
angle_text = 360 - angle_text
@@ -988,14 +988,9 @@ class SvgWriter:
)
text_position += text_offset
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
text_style = SvgWriter.get_box_alignment_parameters("center")
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius)
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
tag = element.Description or f"R{radius}"
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
@@ -1051,7 +1046,7 @@ class SvgWriter:
elif element.ObjectType == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{helper.format_distance(rise)} / {helper.format_distance(run)}"
return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}"
elif element.ObjectType == "SLOPE_PERCENT":
if angle == 90:
return "-"
@@ -1066,19 +1061,16 @@ class SvgWriter:
)
text_position += text_offset
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
text_style = SvgWriter.get_box_alignment_parameters("center")
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)
matrix_world = obj.matrix_world
element = tool.Ifc.get_entity(obj)
text_override = element.Description
dimension_text = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i, p in enumerate(points):
@@ -1087,22 +1079,39 @@ class SvgWriter:
v0_global = matrix_world @ points[i].co.xyz
v1_global = matrix_world @ points[i + 1].co.xyz
self.draw_dimension_annotation(
v0_global, v1_global, classes, text_override, text_format=lambda x: "D" + x
v0_global,
v1_global,
classes,
dimension_text,
text_format=lambda x: "D" + x,
show_description_only=dimension_data["show_description_only"],
suppress_zero_inches=dimension_data["suppress_zero_inches"],
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
)
def draw_dimension_annotations(self, obj):
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
element = tool.Ifc.get_entity(obj)
text_override = element.Description
dimension_text = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i, p in enumerate(points):
if i + 1 >= len(points):
continue
for i in range(len(points) - 1):
v0_global = matrix_world @ points[i].co.xyz
v1_global = matrix_world @ points[i + 1].co.xyz
self.draw_dimension_annotation(v0_global, v1_global, classes, text_override)
self.draw_dimension_annotation(
v0_global,
v1_global,
classes,
dimension_text=dimension_text,
show_description_only=dimension_data["show_description_only"],
suppress_zero_inches=dimension_data["suppress_zero_inches"],
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
)
def draw_measureit_arch_dimension_annotations(self):
try:
@@ -1116,46 +1125,117 @@ class SvgWriter:
Vector(coord[0]), Vector(coord[1]), ["IfcAnnotation", "PredefinedType-DIMENSION"]
)
def draw_dimension_annotation(self, v0_global, v1_global, classes, text_override=None, text_format=lambda x: x):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
def draw_dimension_annotation(
self,
v0_global,
v1_global,
classes,
dimension_text=None,
text_format=lambda x: x,
show_description_only=False,
suppress_zero_inches=False,
text_prefix="",
text_suffix="",
):
offset = Vector([self.raw_width, self.raw_height]) / 2
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)))
start = (offset + v0.xy * Vector((1, -1))) * self.svg_scale
end = (offset + v1.xy * Vector((1, -1))) * self.svg_scale
mid = ((end - start) / 2) + start
vector = end - start
perpendicular = Vector((vector.y, -vector.x)).normalized()
dimension = (v1_global - v0_global).length
dimension = helper.format_distance(dimension)
sheet_dimension = ((end * self.svg_scale) - (start * self.svg_scale)).length
if sheet_dimension < 5: # annotation can't fit
# offset text to right of marker
text_position = (end * self.svg_scale) + perpendicular + (3 * vector.normalized())
else:
text_position = (mid * self.svg_scale) + perpendicular
rotation = math.degrees(vector.angle_signed(Vector((1, 0))))
line = self.svg.add(
self.svg.line(
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
)
dimension = helper.format_distance(
dimension,
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
)
if text_override is not None:
text = text_override
else:
text = str(dimension)
text = text_format(text)
self.svg.add(
self.svg.text(
sheet_dimension = (end - start).length
# if annotation can't fit offset text to the right of marker
text_position = mid if sheet_dimension > 5 else (end + (3 * vector.normalized()))
angle = math.degrees(vector.angle_signed(Vector((1, 0))))
line = self.svg.line(start=start, end=end, class_=" ".join(classes))
self.svg.add(line)
if not show_description_only:
text = f"{text_prefix}{str(dimension)}{text_suffix}"
text_tag = self.create_text_tag(
text,
insert=tuple(text_position),
class_="DIMENSION",
**{
"transform": "rotate({} {} {})".format(rotation, text_position.x, text_position.y),
"text-anchor": "middle",
},
text_position + perpendicular,
angle,
"bottom-middle",
"DIMENSION",
text_format=text_format,
multiline=True,
multiline_to_bottom=False,
)
)
self.svg.add(text_tag)
if dimension_text:
text_tag = self.create_text_tag(
dimension_text,
text_position - perpendicular,
angle,
"top-middle",
"DIMENSION",
text_format=text_format,
multiline=True,
multiline_to_bottom=True,
)
self.svg.add(text_tag)
elif show_description_only and dimension_text:
text_tag = self.create_text_tag(
dimension_text,
text_position + perpendicular,
angle,
"bottom-middle",
"DIMENSION",
text_format=text_format,
multiline=True,
multiline_to_bottom=False,
)
self.svg.add(text_tag)
def create_text_tag(
self,
text,
text_position,
angle,
box_alignment,
class_str,
text_format=lambda x: x,
multiline=False,
multiline_to_bottom=False,
):
if not multiline:
text_kwargs = {"transform": "rotate({} {} {})".format(angle, text_position.x, text_position.y)}
return self.svg.text(
text_format(text),
insert=text_position,
class_=class_str,
**(text_kwargs | SvgWriter.get_box_alignment_parameters(box_alignment)),
)
text_position_svg_str = ", ".join(map(str, text_position))
text_transform = f"translate({text_position_svg_str}) rotate({angle})"
text_kwargs = {
"transform": text_transform,
"style": "font-size: 0;",
}
text_tag = self.svg.text("", **text_kwargs, **SvgWriter.get_box_alignment_parameters(box_alignment))
text_lines = text.replace("\\n", "\n").split("\n")
text_lines = text_lines if multiline_to_bottom else text_lines[::-1]
for line_number, text_line in enumerate(text_lines):
tspan = self.svg.tspan(text_format(text_line), class_=class_str, insert=(0, 0))
tspan.update({"dy": f"{line_number if multiline_to_bottom else -line_number}em"})
text_tag.add(tspan)
return text_tag
def project_point_onto_camera(self, point):
# TODO is this needlessly complex?
@@ -62,6 +62,11 @@ class BIM_PT_camera(Panel):
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = layout.row()
row.prop(props, "calculate_shapely_surfaces")
row = layout.row()
row.prop(props, "calculate_svgfill_surfaces")
row = layout.row()
row.prop(dprops, "should_extract")
@@ -79,12 +84,18 @@ class BIM_PT_camera(Panel):
row = layout.row()
row.prop(props, "diagram_scale")
if props.diagram_scale == "CUSTOM":
row = layout.row()
row.prop(props, "custom_diagram_scale")
row = layout.row(align=True)
row.prop(props, "custom_diagram_scale_input1", text="Custom Scale")
if context.scene.unit_settings.system == "IMPERIAL":
separator = " ="
else:
separator = " :"
row.label(text=separator)
row.prop(props, "custom_diagram_scale_input2", text="")
row = layout.row(align=True)
row.operator("bim.create_drawing", text="Create Drawing", icon="OUTPUT")
op = row.operator("bim.open_view", icon="URL", text="")
op = row.operator("bim.open_drawing", icon="URL", text="")
op.view = context.active_object.name.split("/")[1]
@@ -106,41 +117,53 @@ class BIM_PT_drawing_underlay(Panel):
layout.use_property_split = True
dprops = context.scene.DocProperties
props = context.active_object.data.BIMCameraProperties
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
row = layout.row(align=True)
row.operator("bim.add_drawing_style")
current_shading_style = drawing_pset_data.get("CurrentShadingStyle", None)
if current_shading_style is None:
row.label(text="Current style is not set.")
else:
row.label(text="Current Shading Style:")
row.label(text=current_shading_style)
row.operator("bim.add_drawing_style", icon="ADD", text="")
if drawing_index_is_valid:
row.operator("bim.remove_drawing_style", icon="X", text="").index = props.active_drawing_style_index
row.operator("bim.reload_drawing_styles", icon="FILE_REFRESH", text="")
if dprops.drawing_styles:
layout.template_list("BIM_UL_generic", "", dprops, "drawing_styles", props, "active_drawing_style_index")
if not dprops.drawing_styles:
return
layout.template_list("BIM_UL_generic", "", dprops, "drawing_styles", props, "active_drawing_style_index")
if props.active_drawing_style_index < len(dprops.drawing_styles):
drawing_style = dprops.drawing_styles[props.active_drawing_style_index]
if not drawing_index_is_valid:
return
drawing_style = dprops.drawing_styles[props.active_drawing_style_index]
row = layout.row(align=True)
row.prop(drawing_style, "name")
row.operator("bim.remove_drawing_style", icon="X", text="").index = props.active_drawing_style_index
row = layout.row(align=True)
row.prop(drawing_style, "name")
row = layout.row()
row.prop(drawing_style, "render_type")
row = layout.row(align=True)
row.prop(drawing_style, "vector_style")
row.operator("bim.edit_vector_style", text="", icon="GREASEPENCIL")
row = layout.row(align=True)
row.prop(drawing_style, "include_query")
row = layout.row(align=True)
row.prop(drawing_style, "exclude_query")
row = layout.row()
row.prop(drawing_style, "render_type")
row = layout.row(align=True)
row.prop(drawing_style, "include_query")
row = layout.row(align=True)
row.prop(drawing_style, "exclude_query")
row = layout.row()
row.operator("bim.add_drawing_style_attribute")
row = layout.row()
row.operator("bim.add_drawing_style_attribute")
for index, attribute in enumerate(drawing_style.attributes):
row = layout.row(align=True)
row.prop(attribute, "name", text="")
row.operator("bim.remove_drawing_style_attribute", icon="X", text="").index = index
for index, attribute in enumerate(drawing_style.attributes):
row = layout.row(align=True)
row.prop(attribute, "name", text="")
row.operator("bim.remove_drawing_style_attribute", icon="X", text="").index = index
row = layout.row(align=True)
row.operator("bim.save_drawing_style")
row.operator("bim.activate_drawing_style")
row = layout.row(align=True)
row.operator("bim.save_drawing_style")
row.operator("bim.activate_drawing_style")
class BIM_PT_drawings(Panel):
@@ -158,6 +181,14 @@ class BIM_PT_drawings(Panel):
if not DrawingsData.is_loaded:
DrawingsData.load()
if not DrawingsData.data["has_saved_ifc"]:
row = self.layout.row()
row.label(text="Project Not Yet Saved", icon="ERROR")
row = self.layout.row()
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
op.should_save_as = False
return
self.props = context.scene.DocProperties
if not self.props.is_editing_drawings:
@@ -185,9 +216,11 @@ class BIM_PT_drawings(Panel):
).drawing = active_drawing.ifc_definition_id
col = row.column()
col.alignment = "RIGHT"
op = row.operator("bim.open_view", icon="URL", text="")
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
op = row.operator("bim.open_drawing", icon="URL", text="")
op.view = active_drawing.name
op = row.operator("bim.activate_view", icon="OUTLINER_OB_CAMERA", text="")
row.operator("bim.activate_model", icon="VIEW3D", text="")
op = row.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="")
op.drawing = active_drawing.ifc_definition_id
row.operator("bim.create_drawing", text="", icon="OUTPUT")
self.layout.template_list(
@@ -220,6 +253,14 @@ class BIM_PT_schedules(Panel):
if not SchedulesData.is_loaded:
SchedulesData.load()
if not SchedulesData.data["has_saved_ifc"]:
row = self.layout.row()
row.label(text="Project Not Yet Saved", icon="ERROR")
row = self.layout.row()
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
op.should_save_as = False
return
self.props = context.scene.DocProperties
if not self.props.is_editing_schedules:
@@ -263,6 +304,14 @@ class BIM_PT_sheets(Panel):
if not SheetsData.is_loaded:
SheetsData.load()
if not SheetsData.data["has_saved_ifc"]:
row = self.layout.row()
row.label(text="Project Not Yet Saved", icon="ERROR")
row = self.layout.row()
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
op.should_save_as = False
return
self.props = context.scene.DocProperties
if not self.props.is_editing_sheets:
@@ -280,6 +329,7 @@ class BIM_PT_sheets(Panel):
active_sheet = self.props.sheets[self.props.active_sheet_index]
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.edit_sheet", icon="GREASEPENCIL", text="")
row.operator("bim.open_sheet", icon="URL", text="")
row.operator("bim.add_drawing_to_sheet", icon="IMAGE_PLANE", text="")
row.operator("bim.add_schedule_to_sheet", icon="PRESET_NEW", text="")
@@ -343,7 +393,7 @@ class BIM_PT_text(Panel):
element = tool.Ifc.get_entity(context.active_object)
if not element:
return
return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"]
return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
def draw(self, context):
obj = context.active_object
@@ -470,6 +520,7 @@ class BIM_PT_annotation_utilities(Panel):
op = row.operator("bim.add_annotation", text="Batting", icon="FORCE_FORCE")
op.object_type = "BATTING"
op.data_type = "mesh"
op.description = "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set"
op = row.operator("bim.add_annotation", text="Fill Area", icon="NODE_TEXTURE")
op.object_type = "FILL_AREA"
@@ -487,6 +538,8 @@ class BIM_UL_drawinglist(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
row.prop(item, "is_selected", text="", icon=selected_icon)
icon = "UV_FACESEL"
if item.target_view == "ELEVATION_VIEW":
icon = "UV_VERTEXSEL"
@@ -503,26 +556,35 @@ class BIM_UL_drawinglist(bpy.types.UIList):
class BIM_UL_sheets(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
if item.is_sheet:
if item.is_expanded:
row.operator(
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).sheet = item.ifc_definition_id
else:
row.operator(
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).sheet = item.ifc_definition_id
else:
row.label(text="", icon="BLANK1")
if item.reference_type == "DRAWING":
row.label(text="", icon="IMAGE_DATA")
elif item.reference_type == "SCHEDULE":
row.label(text="", icon="LONGDISPLAY")
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
row.label(text=name)
else:
if not item:
layout.label(text="", translate=False)
return
row = layout.row(align=True)
if item.is_sheet:
if item.is_expanded:
row.operator(
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).sheet = item.ifc_definition_id
else:
row.operator(
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).sheet = item.ifc_definition_id
row.label(text=f"{item.identification} - {item.name}")
else:
row.label(text="", icon="BLANK1")
if item.reference_type == "DRAWING":
row.label(text="", icon="IMAGE_DATA")
elif item.reference_type == "SCHEDULE":
row.label(text="", icon="LONGDISPLAY")
elif item.reference_type == "TITLEBLOCK":
row.label(text="", icon="MENU_PANEL")
elif item.reference_type == "REVISION":
row.label(text="", icon="RECOVER_LAST")
if item.identification:
name = f"{item.identification} - {item.name or 'Unnamed'}"
else:
name = item.name or "Unnamed"
row.label(text=name)
@@ -0,0 +1,295 @@
# 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 os
import bpy
import blenderbim.tool as tool
from blenderbim.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
# from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
from blenderbim.bim.module.drawing.data import DecoratorData, AnnotationData
from blenderbim.bim.ifc import IfcStore
import blenderbim.bim.handler
# declaring it here to avoid circular import problems
class Operator:
def execute(self, context):
IfcStore.execute_ifc_operator(self, context)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class AnnotationTool(WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.annotation_tool"
bl_label = "Annotation Tool"
bl_description = "Gives you Annotation related superpowers"
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
bl_widget = None
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
("bim.annotation_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.annotation_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.annotation_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.annotation_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
("bim.annotation_hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
("bim.annotation_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
("bim.annotation_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
("bim.annotation_hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
("bim.annotation_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.annotation_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.annotation_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("bim.annotation_hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
("bim.annotation_hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
)
def draw_settings(context, layout, ws_tool):
# Unlike operators, Blender doesn't treat workspace tools as a class, so we'll create our own.
AnnotationToolUI.draw(context, layout)
def add_layout_hotkey_operator(layout, text, hotkey, description):
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.annotation_hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op, row
# TODO: move to operator
def create_annotation_type(context):
# just empty to store parameters
props = context.scene.BIMAnnotationProperties
object_type = props.object_type
create_representation = props.create_representation_for_type
drawing = tool.Ifc.get_entity(bpy.context.scene.camera)
if props.create_representation_for_type:
obj = tool.Drawing.create_annotation_object(drawing, object_type)
else:
obj = bpy.data.objects.new(object_type, None)
obj.name = f"{object_type}_TYPE"
obj.location = context.scene.cursor.location
tool.Drawing.ensure_annotation_in_drawing_plane(obj)
drawing = tool.Ifc.get_entity(context.scene.camera)
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing), object_type)
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcTypeProduct",
predefined_type=object_type,
should_add_representation=create_representation,
context=ifc_context,
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
tool.Blender.select_and_activate_single_object(context, obj)
# TODO: move to operator
def create_annotation_occurence(context):
# object_type = context.scene.BIMAnnotationProperties.object_type
props = context.scene.BIMAnnotationProperties
relating_type = tool.Ifc.get().by_id(int(props.relating_type_id))
object_type = props.object_type
drawing = tool.Ifc.get_entity(context.scene.camera)
obj = tool.Drawing.create_annotation_object(drawing, object_type)
obj.name = relating_type.Name
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing), object_type)
relating_type_repr = tool.Drawing.get_annotation_representation(relating_type)
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type=object_type,
should_add_representation=not bool(relating_type_repr),
context=ifc_context,
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
tool.Ifc.run("group.assign_group", group=tool.Drawing.get_drawing_group(drawing), products=[element])
tool.Collector.assign(obj)
tool.Blender.select_and_activate_single_object(context, obj)
def create_annotation():
props = bpy.context.scene.BIMAnnotationProperties
create_type_occurence = props.relating_type_id != "0"
if create_type_occurence:
create_annotation_occurence(bpy.context)
else:
object_type = props.object_type
bpy.ops.bim.add_annotation(object_type=object_type, data_type=ANNOTATION_TYPES_DATA[object_type][-1])
class AnnotationToolUI:
@classmethod
def draw(cls, context, layout):
cls.layout = layout
cls.props = context.scene.BIMAnnotationProperties
row = cls.layout.row(align=True)
if not tool.Ifc.get():
row.label(text="No IFC Project", icon="ERROR")
return
if not AnnotationData.is_loaded:
AnnotationData.load()
cls.draw_type_selection_interface()
if context.active_object and context.selected_objects:
cls.draw_edit_object_interface(context)
cls.draw_create_object_interface()
@classmethod
def draw_create_object_interface(cls):
row = cls.layout.row(align=True)
op, row = add_layout_hotkey_operator(cls.layout, "Add Type", "S_C", "Create a new annotation type")
selected_icon = "CHECKBOX_HLT" if cls.props.create_representation_for_type else "CHECKBOX_DEHLT"
row.prop(cls.props, "create_representation_for_type", text="", icon=selected_icon)
@classmethod
def draw_edit_object_interface(cls, context):
if DecoratorData.get_ifc_text_data(bpy.context.object):
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
@classmethod
def draw_header_interface(cls):
cls.draw_type_selection_interface()
@classmethod
def draw_basic_annotation_tool_interface(cls):
cls.draw_type_selection_interface()
@classmethod
def draw_type_selection_interface(cls):
# shared by both sidebar and header
object_type = cls.props.object_type
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "object_type", text="")
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
create_type_occurence = cls.props.relating_type_id != "0"
label = "Add Type Occurence" if create_type_occurence else "Add Annotation"
add_layout_hotkey_operator(cls.layout, label, "S_A", "Create a new annotation")
if object_type in ("TEXT", "STAIR_ARROW"):
add_layout_hotkey_operator(
cls.layout,
"Bulk Tag",
"S_T",
"Create new annotations and automatically adjust them to the selected objects",
)
add_layout_hotkey_operator(
cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to"
)
class Hotkey(bpy.types.Operator, Operator):
bl_idname = "bim.annotation_hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return tool.Ifc.get()
@classmethod
def description(cls, context, operator):
return operator.description or ""
def _execute(self, context):
self.props = context.scene.BIMAnnotationProperties
getattr(self, f"hotkey_{self.hotkey}")()
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
self.props = context.scene.BIMAnnotationProperties
return self.execute(context)
def draw(self, context):
pass
def hotkey_S_T(self):
props = bpy.context.scene.BIMAnnotationProperties
object_type = props.object_type
related_objects = bpy.context.selected_objects
for related_object in related_objects:
create_annotation()
obj = bpy.context.active_object
bpy.ops.object.mode_set(mode="OBJECT")
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
def hotkey_S_A(self):
create_annotation()
def hotkey_S_E(self):
if not bpy.context.object:
return
if DecoratorData.get_ifc_text_data(bpy.context.object):
bpy.ops.bim.edit_text_popup()
def hotkey_S_G(self):
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcAnnotation"):
continue
related_product = tool.Drawing.get_assigned_product(element)
if not related_product:
continue
related_object = tool.Ifc.get_object(related_product)
tool.Drawing.setup_annotation_object(obj, element.ObjectType, related_object)
def hotkey_S_C(self):
create_annotation_type(bpy.context)
@@ -27,6 +27,8 @@ classes = (
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
operator.OverrideDuplicateMoveLinkedMacro,
operator.OverrideDuplicateMoveMacro,
operator.OverrideJoin,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
@@ -52,6 +54,11 @@ addon_keymaps = []
def register():
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
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)
@@ -60,9 +67,8 @@ def register():
if wm.keyconfigs.addon:
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_object_duplicate_move_macro", "D", "PRESS", shift=True)
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
@@ -72,6 +78,9 @@ def register():
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="Curve", 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 = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS")
@@ -38,7 +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)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
face = None
@@ -62,7 +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)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -90,7 +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)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -292,7 +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)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -389,7 +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)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
faces = bm.faces
@@ -189,7 +189,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
return
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.is_moved(obj):
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
# These objects are parametrically based on an axis and should not be modified as a mesh
@@ -198,6 +199,13 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
context_of_items = old_representation.ContextOfItems
# TODO: remove this code a bit later
# added this as a fallback for easier transition some annotation types to 3d
# if they were create before as 2d
element = tool.Ifc.get_entity(obj)
if tool.Drawing.is_annotation_object_type(element, ("FALL", "SECTION_LEVEL", "PLAN_LEVEL")):
context_of_items = tool.Drawing.get_annotation_context("MODEL_VIEW")
gprop = context.scene.BIMGeoreferenceProperties
coordinate_offset = None
if gprop.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT":
@@ -250,6 +258,20 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id())
obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}"
# TODO: In simple scenarios, a type has a ShapeRepresentation of ID
# 123. This is then mapped through mapped representations by
# occurrences, with no cartesian transformation. In this case, the mesh
# data is 100% shared and therefore all have the same mesh name
# referencing ID 123. (i.e. the local origins are shared). However, in
# complex scenarios, occurrences may have their own cartesian
# transformation (via MappingTarget). This will mean that occurrences
# will not share the same mesh data and will instead reference a
# different ShapeRepresentation ID. In this scenario, we have to
# propagate the obj.data back to the type itself and all sibling
# occurrences and accommodate their individual cartesian
# transformations.
core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation)
if obj.data.BIMMeshProperties.ifc_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
@@ -345,52 +367,7 @@ class CopyRepresentation(bpy.types.Operator, Operator):
return r.MappedRepresentation
class OverrideDeleteTrait:
def delete_ifc_object(self, obj):
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)
if not parent:
parent = ifcopenshell.util.element.get_container(element)
if parent:
parent_obj = tool.Ifc.get_object(parent)
if parent_obj:
parent_collection = bpy.data.collections.get(parent_obj.name)
for child in obj.users_collection[0].children:
parent_collection.children.link(child)
bpy.data.collections.remove(obj.users_collection[0])
if getattr(element, "FillsVoids", None):
self.remove_filling(element)
if element.is_a("IfcOpeningElement"):
if element.HasFillings:
for rel in element.HasFillings:
self.remove_filling(rel.RelatedBuildingElement)
else:
if element.VoidsElements:
self.delete_opening_element(element)
else:
if getattr(element, "HasOpenings", None):
for rel in element.HasOpenings:
self.delete_opening_element(rel.RelatedOpeningElement)
for port in ifcopenshell.util.system.get_ports(element):
self.remove_port(port)
def delete_opening_element(self, element):
bpy.ops.bim.remove_opening(opening_id=element.id())
def remove_filling(self, element):
bpy.ops.bim.remove_filling(filling=element.id())
def remove_port(self, port):
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
class OverrideDelete(bpy.types.Operator):
bl_idname = "bim.override_object_delete"
bl_label = "IFC Delete"
bl_options = {"REGISTER", "UNDO"}
@@ -419,18 +396,16 @@ class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
def _execute(self, context):
for obj in context.selected_objects:
self.delete_ifc_object(obj)
try:
obj.name
if tool.Ifc.get_entity(obj):
tool.Geometry.delete_ifc_object(obj)
else:
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):
class OverrideOutlinerDelete(bpy.types.Operator):
bl_idname = "bim.override_outliner_delete"
bl_label = "IFC Delete"
bl_options = {"REGISTER", "UNDO"}
@@ -481,8 +456,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
objects_to_delete.add(bpy.data.objects.get(item.name))
for obj in objects_to_delete:
# This is the only difference
self.delete_ifc_object(obj)
bpy.data.objects.remove(obj)
tool.Geometry.delete_ifc_object(obj)
return {"FINISHED"}
def get_collection_objects_and_children(self, collection):
@@ -498,6 +472,12 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
return {"objects": objects, "children": children}
class OverrideDuplicateMoveMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_macro"
bl_label = "IFC Duplicate Objects"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMove(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move"
bl_label = "IFC Duplicate Objects"
@@ -529,8 +509,6 @@ class OverrideDuplicateMove(bpy.types.Operator):
new_obj.select_set(True)
if new_active_obj:
context.view_layer.objects.active = new_active_obj
if self.is_interactive:
bpy.ops.transform.translate("INVOKE_DEFAULT")
return {"FINISHED"}
def _execute(self, context):
@@ -551,17 +529,28 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
if array_pset:
array_pset = tool.Ifc.get().by_id(array_pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
old_to_new[tool.Ifc.get_entity(obj)] = [new]
if new.is_a("IfcRelSpaceBoundary"):
tool.Boundary.decorate_boundary(new_obj)
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
if self.is_interactive:
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_linked_macro"
bl_label = "IFC Duplicate Linked"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMoveLinked(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move_linked"
bl_label = "IFC Duplicate Linked"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
@@ -586,7 +575,6 @@ class OverrideDuplicateMoveLinked(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")
return {"FINISHED"}
def _execute(self, context):
@@ -596,6 +584,8 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
old_to_new = {}
for obj in context.selected_objects:
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
if obj == context.active_object:
self.new_active_obj = new_obj
for collection in obj.users_collection:
@@ -605,10 +595,13 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
if array_pset:
array_pset = tool.Ifc.get().by_id(array_pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
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")
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
@@ -616,6 +609,12 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class OverrideJoin(bpy.types.Operator, Operator):
bl_idname = "bim.override_object_join"
bl_label = "IFC Join"
bl_options = {"REGISTER", "UNDO"}
def invoke(self, context, event):
if not tool.Ifc.get():
return bpy.ops.object.join()
return self.execute(context)
def _execute(self, context):
if not tool.Ifc.get():
@@ -638,7 +637,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
continue
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
bpy.ops.object.join()
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
elif representation.RepresentationType == "SweptSolid":
@@ -649,7 +648,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
continue
element = tool.Ifc.get_entity(obj)
# Non IFC elements cannot be join since we cannot guarantee SweptSolid compliance
# Non IFC elements cannot be joined since we cannot guarantee SweptSolid compliance
if not element:
obj.select_set(False)
continue
@@ -664,6 +663,9 @@ class OverrideJoin(bpy.types.Operator, Operator):
for item in obj_rep.Items:
copied_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
for style in item.StyledByItem:
copied_style = ifcopenshell.util.element.copy(tool.Ifc.get(), style)
copied_style.Item = copied_item
if copied_item.Position:
position = ifcopenshell.util.placement.get_axis2placement(copied_item.Position)
else:
@@ -676,7 +678,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 0][:3]]),
)
items.append(copied_item)
tool.Ifc.delete(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
representation.Items = items
bpy.ops.object.join()
core.switch_representation(
@@ -696,13 +698,14 @@ class OverrideJoin(bpy.types.Operator, Operator):
continue
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
bpy.ops.object.join()
class OverridePasteBuffer(bpy.types.Operator):
bl_idname = "bim.override_paste_buffer"
bl_label = "IFC Paste BIM Objects"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.view3d.pastebuffer()
@@ -721,10 +724,16 @@ class OverrideModeSetEdit(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
objs = context.selected_objects or [context.active_object]
objs = context.selected_objects or ([context.active_object] if context.active_object else [])
active_obj = context.active_object
if context.active_object:
context.active_object.select_set(True)
edited_objs = []
element = tool.Ifc.get_entity(context.active_object)
if element and element.is_a("IfcRelSpaceBoundary"):
return bpy.ops.bim.enable_editing_boundary_geometry()
for obj in objs:
if not obj:
continue
@@ -742,26 +751,43 @@ class OverrideModeSetEdit(bpy.types.Operator):
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)
representation = tool.Geometry.get_active_representation(obj)
if not representation:
continue
if tool.Geometry.is_meshlike(representation):
if getattr(element, "HasOpenings", None):
# 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)
else:
obj.select_set(False)
continue
if not context.selected_objects or len(context.selected_objects) != len(objs):
# We are trying to edit at least one non-mesh-like object : Display a hint to the user
self.report({"INFO"}, "Only mesh-compatible representations may be edited in edit mode.")
if context.active_object not in context.selected_objects:
# The active object is non-mesh-like. Set a valid object (or None) as active
context.view_layer.objects.active = context.selected_objects[0] if context.selected_objects else None
if context.active_object:
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
else:
# restore the selection if nothing worked
for obj in objs:
obj.select_set(True)
context.view_layer.objects.active = active_obj
return {"FINISHED"}
def invoke(self, context, event):
@@ -783,7 +809,7 @@ class OverrideModeSetObject(bpy.types.Operator):
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:
if getattr(tool.Ifc.get_entity(obj), "HasOpenings", False):
self.reload_representation(obj)
else:
self.reload_representation(obj)
@@ -806,15 +832,27 @@ class OverrideModeSetObject(bpy.types.Operator):
)
def draw(self, context):
row = self.layout.row(align=True)
row.prop(self, "should_save")
if self.is_valid:
row = self.layout.row()
row.prop(self, "should_save")
else:
row = self.layout.row()
row.label(text="No Geometry Found: Object will revert to previous state.")
def invoke(self, context, event):
self.is_valid = True
self.should_save = True
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
if not tool.Ifc.get():
return {"FINISHED"}
if context.active_object:
element = tool.Ifc.get_entity(context.active_object)
if element and element.is_a("IfcRelSpaceBoundary"):
return bpy.ops.bim.edit_boundary_geometry()
objs = context.selected_objects or [context.active_object]
self.edited_objs = []
@@ -829,13 +867,15 @@ class OverrideModeSetObject(bpy.types.Operator):
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
if not tool.Geometry.has_geometric_data(obj):
self.is_valid = False
self.should_save = False
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in (
"Tessellation",
"Brep",
if tool.Geometry.is_meshlike(
representation
) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
elif element.HasOpenings:
elif getattr(element, "HasOpenings", None):
self.unchanged_objs_with_openings.append(obj)
if self.edited_objs:
@@ -49,7 +49,7 @@ class BIM_OT_cityjson2ifc(Operator):
class BIM_OT_find_cityjson_lod(Operator):
bl_idname = "bim.find_cityjson_lod"
bl_label = "Find LODs in CityJSON file"
bl_label = "Find LODs in CityJSON File"
bl_context = "scene"
def execute(self, context):
@@ -0,0 +1,49 @@
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# 2023 Bruno Postle <bruno@postle.net>, Bruno Perdigão <brunoperdigao@tutanota.com>,
# Massimo Fabbro <maxfb87@yahoo.it>
import bpy
from . import ui, prop, operator
classes = (
operator.AddFileToRepo,
operator.AddTag,
operator.CloneRepo,
operator.CommitChanges,
operator.CreateRepo,
operator.DiscardUncommitted,
operator.DisplayRevision,
operator.DisplayUncommitted,
operator.Fetch,
operator.Merge,
operator.Push,
operator.RefreshGit,
operator.SwitchRevision,
prop.IfcGitTag,
prop.IfcGitListItem,
prop.IfcGitProperties,
ui.IFCGIT_PT_panel,
ui.COMMIT_UL_List,
)
def register():
bpy.types.Scene.IfcGitProperties = bpy.props.PointerProperty(type=prop.IfcGitProperties)
def unregister():
del bpy.types.Scene.IfcGitProperties
@@ -0,0 +1,144 @@
import bpy
import os
import shutil
# import tool
import blenderbim.tool as tool
def refresh():
IfcGitData.is_loaded = False
class IfcGitData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {
"repo": cls.repo(),
"remotes": cls.remotes(),
"branch_names": cls.branch_names(),
"remote_names": cls.remote_names(),
"remote_urls": cls.remote_urls(),
"path_ifc": cls.path_ifc(),
"branches_by_hexsha": cls.branches_by_hexsha(),
"tags_by_hexsha": cls.tags_by_hexsha(),
"name_ifc": cls.name_ifc(),
"dir_name": cls.dir_name(),
"base_name": cls.base_name(),
"is_dirty": cls.is_dirty(),
"commit": cls.commit(),
"current_revision": cls.current_revision(),
"git_exe": cls.git_exe(),
"ifcmerge_exe": cls.ifcmerge_exe(),
}
cls.is_loaded = True
@classmethod
def repo(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return tool.IfcGit.repo_from_path(path_ifc)
return None
@classmethod
def remotes(cls):
if cls.repo():
return cls.repo().remotes
return None
@classmethod
def branch_names(cls):
return []
@classmethod
def remote_names(cls):
return []
@classmethod
def remote_urls(cls):
result = {}
if cls.repo():
for remote in cls.repo().remotes:
result[remote.name] = remote.url
return result
@classmethod
def path_ifc(cls):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return tool.Ifc.get_path()
return None
@classmethod
def branches_by_hexsha(cls):
try:
if tool.IfcGitRepo.repo.branches:
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
except:
return {}
@classmethod
def tags_by_hexsha(cls):
if tool.IfcGitRepo.repo:
return tool.IfcGit.tags_by_hexsha(tool.IfcGitRepo.repo)
return {}
@classmethod
def name_ifc(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if tool.IfcGitRepo.repo and os.path.isfile(path_ifc):
working_dir = tool.IfcGitRepo.repo.working_dir
return os.path.relpath(path_ifc, working_dir)
return None
@classmethod
def dir_name(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return os.path.dirname(path_ifc)
return None
@classmethod
def base_name(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return os.path.basename(path_ifc)
return None
@classmethod
def is_dirty(cls):
if cls.repo() and cls.git_exe():
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return cls.repo().is_dirty(path=path_ifc)
return False
@classmethod
def commit(cls):
props = bpy.context.scene.IfcGitProperties
if len(props.ifcgit_commits) > 0:
item = props.ifcgit_commits[props.commit_index]
if cls.repo():
return cls.repo().commit(rev=item.hexsha)
@classmethod
def current_revision(cls):
props = bpy.context.scene.IfcGitProperties
if len(props.ifcgit_commits) > 0:
return tool.IfcGitRepo.repo.commit()
@classmethod
def git_exe(cls):
return shutil.which("git")
@classmethod
def ifcmerge_exe(cls):
return shutil.which("ifcmerge")
@@ -0,0 +1,280 @@
import os
import re
import bpy
import blenderbim.core.ifcgit as core
import blenderbim.tool as tool
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
class CreateRepo(bpy.types.Operator):
"""Initialise a Git repository"""
bl_label = "Create Git repository"
bl_idname = "ifcgit.createrepo"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
path_ifc = IfcGitData.data["path_ifc"]
if not os.path.isfile(path_ifc):
return False
if IfcGitData.data["repo"]:
# repo already exists
return False
if re.match("^/home/[^/]+/?$", os.path.dirname(path_ifc)):
# don't make ${HOME} a repo
return False
return True
def execute(self, context):
core.create_repo(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
class AddFileToRepo(bpy.types.Operator):
"""Add a file to a repository"""
bl_label = "Add file to repository"
bl_idname = "ifcgit.addfile"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
path_ifc = IfcGitData.data["path_ifc"]
if not os.path.isfile(path_ifc):
return False
if not IfcGitData.data["repo"]:
# repo doesn't exist
return False
return True
def execute(self, context):
core.add_file(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
class CloneRepo(bpy.types.Operator):
"""Clone a remote Git repository"""
bl_label = "Clone repository"
bl_idname = "ifcgit.clone_repo"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
if (
props.remote_url
and props.local_folder
and os.path.isdir(props.local_folder)
and not os.listdir(props.local_folder)
):
return True
return False
def execute(self, context):
props = context.scene.IfcGitProperties
core.clone_repo(tool.IfcGit, props.remote_url, props.local_folder, self)
refresh()
return {"FINISHED"}
class DiscardUncommitted(bpy.types.Operator):
"""Discard saved changes and update to HEAD"""
bl_label = "Discard uncommitted changes"
bl_idname = "ifcgit.discard"
bl_options = {"REGISTER"}
def execute(self, context):
core.discard_uncomitted(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
class CommitChanges(bpy.types.Operator):
"""Commit current saved changes"""
bl_label = "Commit changes"
bl_idname = "ifcgit.commit_changes"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
repo = IfcGitData.data["repo"]
if props.commit_message == "":
return False
if (
repo
and repo.head.is_detached
and (
not tool.IfcGit.is_valid_ref_format(props.new_branch_name)
or props.new_branch_name in [branch.name for branch in repo.branches]
)
):
cls.poll_message_set(
"The new branch name is invalid, please insert a valid branch name (eg. with no spaces, ...)"
)
return False
return True
def execute(self, context):
repo = IfcGitData.data["repo"]
core.commit_changes(tool.IfcGit, tool.Ifc, repo, context)
bpy.ops.ifcgit.refresh()
refresh()
return {"FINISHED"}
class AddTag(bpy.types.Operator):
"""Tag selected revision"""
bl_label = "Add tag"
bl_idname = "ifcgit.add_tag"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
repo = IfcGitData.data["repo"]
if repo and (
not tool.IfcGit.is_valid_ref_format(props.new_tag_name)
or props.new_tag_name in [tag.name for tag in repo.tags]
):
return False
return True
def execute(self, context):
repo = IfcGitData.data["repo"]
core.add_tag(tool.IfcGit, repo)
bpy.ops.ifcgit.refresh()
refresh()
return {"FINISHED"}
class RefreshGit(bpy.types.Operator):
"""Refresh revision list"""
bl_label = ""
bl_idname = "ifcgit.refresh"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
repo = IfcGitData.data["repo"]
if repo != None and repo.heads:
return True
return False
def execute(self, context):
repo = IfcGitData.data["repo"]
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
refresh()
return {"FINISHED"}
class DisplayRevision(bpy.types.Operator):
"""Colourise objects by selected revision"""
bl_label = ""
bl_idname = "ifcgit.display_revision"
bl_options = {"REGISTER"}
def execute(self, context):
core.colourise_revision(tool.IfcGit, context)
refresh()
return {"FINISHED"}
class DisplayUncommitted(bpy.types.Operator):
"""Colourise uncommitted objects"""
bl_label = "Show uncommitted changes"
bl_idname = "ifcgit.display_uncommitted"
bl_options = {"REGISTER"}
def execute(self, context):
repo = IfcGitData.data["repo"]
core.colourise_uncommitted(tool.IfcGit, tool.Ifc, repo)
refresh()
return {"FINISHED"}
class SwitchRevision(bpy.types.Operator):
"""Switches the repository to the selected revision and reloads the IFC file"""
bl_label = ""
bl_idname = "ifcgit.switch_revision"
bl_options = {"REGISTER"}
def execute(self, context):
core.switch_revision(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
class Merge(bpy.types.Operator):
"""Merges the selected branch into working branch"""
bl_label = "Merge this branch"
bl_idname = "ifcgit.merge"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
if IfcGitData.data["ifcmerge_exe"]:
return True
return False
def execute(self, context):
if core.merge_branch(tool.IfcGit, tool.Ifc, self):
refresh()
return {"FINISHED"}
else:
return {"CANCELLED"}
class Push(bpy.types.Operator):
"""Pushes the working branch to selected remote"""
bl_label = "Push working branch"
bl_idname = "ifcgit.push"
bl_options = {"REGISTER"}
def execute(self, context):
props = context.scene.IfcGitProperties
repo = IfcGitData.data["repo"]
remote = repo.remotes[props.select_remote]
remote.push()
return {"FINISHED"}
class Fetch(bpy.types.Operator):
"""Fetches from the selected remote"""
bl_label = "Fetch from remote"
bl_idname = "ifcgit.fetch"
bl_options = {"REGISTER"}
def execute(self, context):
props = context.scene.IfcGitProperties
repo = IfcGitData.data["repo"]
remote = repo.remotes[props.select_remote]
remote.fetch()
return {"FINISHED"}
@@ -0,0 +1,136 @@
import bpy
from bpy.types import PropertyGroup
from bpy.props import (
StringProperty,
BoolProperty,
CollectionProperty,
IntProperty,
EnumProperty,
)
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
def git_branches(self, context):
"""branches enum"""
# NOTE "Python must keep a reference to the strings returned by
# the callback or Blender will misbehave or even crash"
IfcGitData.data["branch_names"] = sorted([branch.name for branch in IfcGitData.data["repo"].heads])
if "main" in IfcGitData.data["branch_names"]:
IfcGitData.data["branch_names"].remove("main")
IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"]
if IfcGitData.data["remotes"]:
props = context.scene.IfcGitProperties
IfcGitData.data["branch_names"] += [r.name for r in IfcGitData.data["remotes"][props.select_remote].refs]
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
def git_remotes(self, context):
"""remotes enum"""
IfcGitData.data["remote_names"] = sorted([remote.name for remote in IfcGitData.data["remotes"]])
if "origin" in IfcGitData.data["remote_names"]:
IfcGitData.data["remote_names"].remove("origin")
IfcGitData.data["remote_names"] = ["origin"] + IfcGitData.data["remote_names"]
return [(myname, myname, myname) for myname in IfcGitData.data["remote_names"]]
def update_revlist(self, context):
"""wrapper to trigger update of the revision list"""
bpy.ops.ifcgit.refresh()
props = context.scene.IfcGitProperties
props.commit_index = 0
class IfcGitTag(PropertyGroup):
"""Properties of a Git tag"""
name: StringProperty(
name="Tag name",
default="",
)
message: StringProperty(
name="Tag message",
default="",
)
class IfcGitListItem(PropertyGroup):
"""Group of properties representing an item in the list."""
hexsha: StringProperty(
name="Git hash",
description="checksum for this commit",
default="Uncommitted data!",
)
relevant: BoolProperty(
name="Is relevant",
description="does this commit reference our ifc file",
default=False,
)
author_name: StringProperty(
name="Author Name",
default="",
)
author_email: StringProperty(
name="Author Email",
default="",
)
message: StringProperty(
name="Commit Message",
default="",
)
tags: CollectionProperty(type=IfcGitTag, name="List of revision tags")
class IfcGitProperties(PropertyGroup):
ifcgit_commits: CollectionProperty(type=IfcGitListItem, name="List of git items")
commit_index: IntProperty(name="Index for my_list", default=0)
commit_message: StringProperty(
name="Commit message",
description="A human readable description of these changes",
default="",
)
new_branch_name: StringProperty(
name="New branch name",
description="A short name used to refer to this branch",
default="",
)
new_tag_name: StringProperty(
name="New tag name",
description="A short name used to refer to this tag",
default="",
)
new_tag_message: StringProperty(
name="Tag message (optional)",
description="An optional human readable description of this tag",
default="",
)
remote_url: StringProperty(
name="Git URL",
description="A URL pointing to a Git repository",
default="",
)
local_folder: StringProperty(
name="Local folder",
description="A local Git repository path",
default="",
subtype="DIR_PATH",
)
display_branch: EnumProperty(items=git_branches, update=update_revlist)
select_remote: EnumProperty(items=git_remotes)
ifcgit_filter: EnumProperty(
items=[
("all", "All", "All revisions"),
("tagged", "Tagged", "Tagged revisions"),
("relevant", "Relevant", "Revisions for this project"),
],
update=update_revlist,
)
@@ -0,0 +1,204 @@
import bpy
import time
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
class IFCGIT_PT_panel(bpy.types.Panel):
"""Scene Properties panel to interact with IFC repository data"""
bl_label = "IFC Git"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_project_info"
def draw(self, context):
if not IfcGitData.is_loaded:
IfcGitData.load()
layout = self.layout
path_ifc = IfcGitData.data["path_ifc"]
if not IfcGitData.data["git_exe"]:
row = layout.row()
row.label(text="Git is not installed", icon="ERROR")
return
props = context.scene.IfcGitProperties
# TODO if file isn't saved, offer to save to disk
row = layout.row()
if path_ifc:
if IfcGitData.data["repo"]:
name_ifc = IfcGitData.data["name_ifc"]
row.label(text=IfcGitData.data["repo"].working_dir, icon="SYSTEM")
if name_ifc in IfcGitData.data["repo"].untracked_files:
row.operator(
"ifcgit.addfile",
text="Add '" + name_ifc + "' to repository",
icon="FILE",
)
else:
row.label(text=name_ifc, icon="FILE")
else:
row.operator(
"ifcgit.createrepo",
text="Create '" + IfcGitData.data["dir_name"] + "' repository",
icon="SYSTEM",
)
row.label(text=IfcGitData.data["base_name"], icon="FILE")
return
else:
row.label(text="No Git repository found", icon="SYSTEM")
row.label(text="No IFC project saved", icon="FILE")
box = layout.box()
row = box.row()
row.label(text="Clone a remote Git repository")
row = box.row()
row.prop(props, "remote_url")
row = box.row()
row.prop(props, "local_folder")
row = box.row()
row.operator("ifcgit.clone_repo", icon="IMPORT")
return
is_dirty = IfcGitData.data["is_dirty"]
if is_dirty:
row = layout.row()
row.label(text="Saved changes have not been committed", icon="ERROR")
row = layout.row()
row.operator("ifcgit.display_uncommitted", icon="SELECT_DIFFERENCE")
row.operator("ifcgit.discard", icon="TRASH")
row = layout.row()
row.prop(props, "commit_message")
if IfcGitData.data["repo"].head.is_detached:
row = layout.row()
row.label(text="HEAD is detached, commit will create a branch", icon="ERROR")
row.prop(props, "new_branch_name")
row = layout.row()
row.operator("ifcgit.commit_changes", icon="GREASEPENCIL")
row = layout.row()
if IfcGitData.data["repo"].head.is_detached:
row.label(text="Working branch: Detached HEAD")
else:
row.label(text="Working branch: " + IfcGitData.data["repo"].active_branch.name)
grouped = layout.row()
column = grouped.column()
row = column.row()
row.prop(props, "display_branch", text="Browse branch")
row.prop(props, "ifcgit_filter", text="Filter revisions")
row = column.row()
row.template_list(
"COMMIT_UL_List",
"The_List",
props,
"ifcgit_commits",
props,
"commit_index",
)
column = grouped.column()
row = column.row()
row.operator("ifcgit.refresh", icon="FILE_REFRESH")
if not is_dirty:
row = column.row()
row.operator("ifcgit.display_revision", icon="SELECT_DIFFERENCE")
row = column.row()
row.operator("ifcgit.switch_revision", icon="CURRENT_FILE")
# TODO operator to tag selected
row = column.row()
row.operator("ifcgit.merge", icon="EXPERIMENTAL", text="")
if not props.ifcgit_commits:
return
item = props.ifcgit_commits[props.commit_index]
if not item.relevant:
row = layout.row()
row.label(text="Revision unrelated to current IFC project", icon="ERROR")
box = layout.box()
column = box.column(align=True)
row = column.row()
row.label(text=item.hexsha)
row = column.row()
row.label(text=item.author_name + " <" + item.author_email + ">")
row = column.row()
row.label(text=item.message)
for tag in item.tags:
box = layout.box()
item = box.row()
column = item.column(align=True)
row = column.row()
row.label(text=tag.name)
if tag.message:
row = column.row()
row.label(text=tag.message)
# TODO
# item.operator("ifcgit.delete_tag", icon="PANEL_CLOSE")
box = layout.box()
row = box.row()
row.prop(props, "new_tag_name")
row = box.row()
row.prop(props, "new_tag_message")
row = box.row()
row.operator("ifcgit.add_tag", icon="GREASEPENCIL")
if IfcGitData.data["remotes"]:
row = layout.row()
row.prop(props, "select_remote", text="Select remote")
urls = IfcGitData.data["remote_urls"]
row.label(text=urls[props.select_remote])
row = layout.row()
row.operator("ifcgit.push", icon="EXPERIMENTAL")
row.operator("ifcgit.fetch", icon="IMPORT")
class COMMIT_UL_List(bpy.types.UIList):
"""List of Git commits"""
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
props = context.scene.IfcGitProperties
current_revision = IfcGitData.data["current_revision"]
# TODO Figure how this "item" can be acesse in "data.py"
# so it's possible to move the ".commit"
commit = IfcGitData.data["repo"].commit(rev=item.hexsha)
lookup = IfcGitData.data["branches_by_hexsha"]
refs = ""
if item.hexsha in lookup:
for branch in lookup[item.hexsha]:
if branch.name == props.display_branch:
refs = "[" + branch.name + "] "
lookup = IfcGitData.data["tags_by_hexsha"]
if item.hexsha in lookup:
for tag in lookup[item.hexsha]:
refs += "{" + tag.name + "} "
if commit == current_revision:
layout.label(text="[HEAD] " + refs + commit.message, icon="DECORATE_KEYFRAME")
else:
layout.label(text=refs + commit.message, icon="DECORATE_ANIMATE")
layout.label(text=time.strftime("%c", time.localtime(commit.committed_date)))
@@ -31,13 +31,16 @@ classes = (
operator.ContractMaterialCategory,
operator.CopyMaterial,
operator.DisableEditingAssignedMaterial,
operator.DisableEditingMaterial,
operator.DisableEditingMaterials,
operator.DisableEditingMaterialSetItem,
operator.DisableEditingMaterialSetItemProfile,
operator.DisableEditingMaterials,
operator.EditAssignedMaterial,
operator.EditMaterial,
operator.EditMaterialSetItem,
operator.EditMaterialSetItemProfile,
operator.EnableEditingAssignedMaterial,
operator.EnableEditingMaterial,
operator.EnableEditingMaterialSetItem,
operator.EnableEditingMaterialSetItemProfile,
operator.ExpandMaterialCategory,
@@ -24,6 +24,7 @@ import ifcopenshell.util.attribute
import ifcopenshell.util.representation
import blenderbim.bim.helper
import blenderbim.tool as tool
import blenderbim.core.style
import blenderbim.core.material as core
import blenderbim.bim.module.model.profile as model_profile
from blenderbim.bim.module.material.prop import purge as material_prop_purge
@@ -58,6 +59,36 @@ class SelectByMaterial(bpy.types.Operator, tool.Ifc.Operator):
core.select_by_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
class EnableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_material"
bl_label = "Enable Editing Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def _execute(self, context):
core.enable_editing_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
class EditMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material"
bl_label = "Edit Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def _execute(self, context):
core.edit_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material))
class DisableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_material"
bl_label = "Disable Editing Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def _execute(self, context):
core.disable_editing_material(tool.Material)
class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_parameterized_profile"
bl_label = "Assign Parameterized Profile"
@@ -337,7 +368,8 @@ class RemoveLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
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
self.report({"ERROR"}, "Cannot remove material layer - IfcMaterialLayerSet should alawys have atleast 1 layer")
return {"ERROR"}
ifcopenshell.api.run("material.remove_layer", tool.Ifc.get(), layer=tool.Ifc.get().by_id(self.layer))
@@ -515,7 +547,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_set_item_profile"
bl_label = "Enable Editing Material Set Item"
bl_label = "Enable Editing Material Set Item Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
@@ -531,7 +563,7 @@ class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
class DisableEditingMaterialSetItemProfile(bpy.types.Operator):
bl_idname = "bim.disable_editing_material_set_item_profile"
bl_label = "Disable Editing Material Set Item"
bl_label = "Disable Editing Material Set Item Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
@@ -666,8 +698,12 @@ class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
blender_material = context.active_object.active_material
material = tool.Ifc.get_entity(blender_material)
if tool.Ifc.has_changed_shading(blender_material):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=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_blender_material = blender_material.copy()
copied_style = self.get_style(copied_material)
tool.Ifc.link(copied_material, copied_blender_material)
if copied_style:
@@ -131,6 +131,9 @@ class BIMMaterialProperties(PropertyGroup):
materials: CollectionProperty(name="Materials", type=Material)
active_material_index: IntProperty(name="Active Material Index")
profiles: EnumProperty(items=get_profiles, name="Profiles")
active_material_id: IntProperty(name="Active Material ID")
material_attributes: CollectionProperty(name="Material Attributes", type=Attribute)
editing_material_type = StringProperty(name="Editing Material Type")
class BIMObjectMaterialProperties(PropertyGroup):
@@ -57,13 +57,21 @@ class BIM_PT_materials(Panel):
row.alignment = "RIGHT"
if self.props.material_type == "IfcMaterial":
row.operator("bim.add_material", text="", icon="ADD")
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
if material.ifc_definition_id:
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
if self.props.active_material_id:
row.operator("bim.edit_material", text="", icon="CHECKMARK").material = material.ifc_definition_id
row.operator("bim.disable_editing_material", text="", icon="CANCEL").material = material.ifc_definition_id
self.draw_editable_material_attributes_ui()
else:
row.operator("bim.add_material", text="", icon="ADD")
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL").material = material.ifc_definition_id
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
else:
row.operator("bim.add_material", text="", icon="ADD")
else:
row.operator("bim.add_material_set", text="", icon="ADD").set_type = self.props.material_type
if self.props.materials and self.props.active_material_index < len(self.props.materials):
@@ -76,6 +84,9 @@ class BIM_PT_materials(Panel):
self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index")
def draw_editable_material_attributes_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.material_attributes, self.layout)
class BIM_PT_material(Panel):
bl_label = "IFC Material"
bl_idname = "BIM_PT_material"
@@ -196,7 +207,7 @@ class BIM_PT_object_material(Panel):
else:
row = self.layout.row(align=True)
if ObjectMaterialData.data["set_item_name"] == "profile":
row.prop(self.mprops, "profiles", icon="ITALIC", text="")
prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="")
prop_with_search(row, self.props, "material", icon="MATERIAL", text="")
op = row.operator(f"bim.add_{ObjectMaterialData.data['set_item_name']}", icon="ADD", text="")
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_set", ObjectMaterialData.data["set"]["id"])
@@ -230,11 +241,11 @@ class BIM_PT_object_material(Panel):
draw_attributes(self.props.material_set_item_attributes, box)
row = box.row()
row.prop(self.props, "material_set_item_material", icon="MATERIAL", text="Material")
prop_with_search(row, self.props, "material_set_item_material", icon="MATERIAL", text="Material")
if ObjectMaterialData.data["set_item_name"] == "profile":
row = box.row()
row.prop(self.mprops, "profiles", icon="ITALIC", text="Profile")
prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="Profile")
def draw_read_only_set_item_ui(self, set_item, index, is_first=False, is_last=False):
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
@@ -47,6 +47,9 @@ classes = (
array.EnableEditingArray,
array.RemoveArray,
array.SelectArrayParent,
array.Input3DCursorXArray,
array.Input3DCursorYArray,
array.Input3DCursorZArray,
product.AddConstrTypeInstance,
product.AddEmptyType,
product.AlignProduct,
@@ -93,9 +96,7 @@ classes = (
slab.ResetVertex,
slab.SetArcIndex,
space.GenerateSpace,
prop.ConstrTypeInfo,
prop.ConstrClassInfo,
prop.ConstrBrowserState,
space.GenerateSpacesFromWalls,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -113,7 +114,6 @@ classes = (
ui.BIM_PT_railing,
ui.BIM_PT_roof,
ui.LaunchTypeManager,
ui.HelpConstrTypes,
ui.BIM_MT_model,
grid.BIM_OT_add_object,
stair.BIM_OT_add_object,
@@ -126,6 +126,7 @@ classes = (
pie.OpenPieClass,
pie.PieUpdateContainer,
pie.PieAddOpening,
pie.PieAssignObjectAggregation,
pie.VIEW3D_MT_PIE_bim,
pie.VIEW3D_MT_PIE_bim_class,
sverchok_modifier.CreateNewSverchokGraph,
@@ -149,6 +150,7 @@ classes = (
railing.AddRailing,
railing.CancelEditingRailing,
railing.FinishEditingRailing,
railing.FlipRailingPathOrder,
railing.EnableEditingRailing,
railing.CancelEditingRailingPath,
railing.FinishEditingRailingPath,
@@ -22,19 +22,28 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.tool as tool
from mathutils import Vector
from mathutils import Vector, Matrix
class AddArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_array"
bl_label = "Add Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0, "use_local_space": True}
array = {
"children": [],
"count": 1,
"x": 0.0,
"y": 0.0,
"z": 0.0,
"use_local_space": True,
"sync_children": False,
"method": "OFFSET",
}
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
@@ -58,7 +67,7 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_array"
bl_label = "Disable Editing Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
context.active_object.BIMArrayProperties.is_editing = -1
@@ -68,7 +77,7 @@ class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_array"
bl_label = "Enable Editing Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.IntProperty()
def _execute(self, context):
@@ -81,6 +90,8 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
props.y = data["y"]
props.z = data["z"]
props.use_local_space = data.get("use_local_space", False)
props.sync_children = data.get("sync_children", False)
props.method = data.get("method", "OFFSET")
props.is_editing = self.item
return {"FINISHED"}
@@ -88,7 +99,7 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
class EditArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_array"
bl_label = "Edit Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.IntProperty()
def _execute(self, context):
@@ -105,6 +116,8 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
"y": props.y,
"z": props.z,
"use_local_space": props.use_local_space,
"sync_children": props.sync_children,
"method": props.method,
}
props.is_editing = -1
@@ -125,7 +138,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_array"
bl_label = "Remove Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.IntProperty()
def _execute(self, context):
@@ -173,3 +186,48 @@ class SelectArrayParent(bpy.types.Operator):
context.view_layer.objects.active = obj
obj.select_set(True)
return {"FINISHED"}
class Input3DCursorXArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_x_array"
bl_label = "Get 3d Cursor X Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.x = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).x
else:
props.x = cursor.location.x - obj.location.x
return {"FINISHED"}
class Input3DCursorYArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_y_array"
bl_label = "Get 3d Cursor Y Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.y = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).y
else:
props.y = cursor.location.y - obj.location.y
return {"FINISHED"}
class Input3DCursorZArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_z_array"
bl_label = "Get 3d Cursor Z Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.z = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).z
else:
props.z = cursor.location.z - obj.location.z
return {"FINISHED"}
@@ -24,8 +24,6 @@ import ifcopenshell
import ifcopenshell.util.element
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
def refresh():
@@ -49,9 +47,8 @@ class AuthoringData:
def load(cls):
cls.is_loaded = True
cls.props = bpy.context.scene.BIMModelProperties
cls.load_ifc_classes()
cls.load_relating_types()
cls.load_relating_types_browser()
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["relating_type_id"] = cls.relating_type_id()
cls.data["type_class"] = cls.type_class()
cls.data["type_predefined_type"] = cls.type_predefined_type()
cls.data["total_types"] = cls.total_types()
@@ -62,8 +59,19 @@ class AuthoringData:
cls.data["type_thumbnail"] = cls.type_thumbnail()
cls.data["is_voidable_element"] = cls.is_voidable_element()
cls.data["has_visible_openings"] = cls.has_visible_openings()
cls.data["has_visible_boundaries"] = cls.has_visible_boundaries()
cls.data["active_class"] = cls.active_class()
cls.data["active_material_usage"] = cls.active_material_usage()
cls.data["active_representation_type"] = cls.active_representation_type()
cls.data["boundary_class"] = cls.boundary_class()
@classmethod
def boundary_class(cls):
declaration = tool.Ifc.schema().declaration_by_name("IfcRelSpaceBoundary")
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
version = tool.Ifc.get_schema()
return [(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted(names)]
@classmethod
def type_class(cls):
@@ -71,7 +79,10 @@ class AuthoringData:
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
if tool.Ifc.get_schema() == "IFC2X3":
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialStructureElementType")
else:
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names.extend([d.name() for d in declarations])
@@ -99,23 +110,11 @@ class AuthoringData:
@classmethod
def type_thumbnail(cls):
if not cls.data["relating_types_ids"]:
if not cls.data["relating_type_id"]:
return 0
element = tool.Ifc.get().by_id(int(cls.props.relating_type_id))
return cls.type_thumbnails.get(element.id(), None) or 0
@classmethod
def load_ifc_classes(cls):
cls.data["ifc_classes"] = cls.ifc_classes()
@classmethod
def load_relating_types(cls):
cls.data["relating_types_ids"] = cls.relating_types()
@classmethod
def load_relating_types_browser(cls):
cls.data["relating_types_ids_browser"] = cls.relating_types_browser()
@classmethod
def total_types(cls):
type_class = cls.props.type_class
@@ -175,6 +174,17 @@ class AuthoringData:
return True
return False
@classmethod
def has_visible_boundaries(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
if element.is_a("IfcRelSpaceBoundary"):
return True
for boundary in getattr(element, "BoundedBy", []):
if tool.Ifc.get_object(boundary):
return True
return False
@classmethod
def active_class(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -187,169 +197,45 @@ class AuthoringData:
if element:
return tool.Model.get_usage_type(element)
@classmethod
def active_representation_type(cls):
if bpy.context.active_object:
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
if representation and representation.is_a("IfcShapeRepresentation"):
return representation.RepresentationType
@classmethod
def ifc_classes(cls):
results = []
classes = {
e.is_a()
for e in tool.Ifc.get().by_type("IfcElementType")
+ tool.Ifc.get().by_type("IfcDoorStyle")
+ tool.Ifc.get().by_type("IfcWindowStyle")
+ tool.Ifc.get().by_type("IfcSpaceType")
e.is_a() for e in (tool.Ifc.get().by_type("IfcElementType") + tool.Ifc.get().by_type("IfcSpaceType"))
}
if tool.Ifc.get_schema() in ("IFC2X3", "IFC4"):
classes.update(
{e.is_a() for e in (tool.Ifc.get().by_type("IfcDoorStyle") + tool.Ifc.get().by_type("IfcWindowStyle"))}
)
results.extend([(c, c, "") for c in sorted(classes)])
return results
@classmethod
def constr_class_entities(cls, ifc_class=None):
def relating_type_id(cls):
ifc_classes = cls.data["ifc_classes"]
if not ifc_classes:
return []
results = []
if ifc_class is None:
ifc_class = cls.props.ifc_class
ifc_class = cls.props.ifc_class
if not ifc_class and ifc_classes:
ifc_class = ifc_classes[0][0]
if ifc_class:
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name or "Unnamed")
results.extend(elements)
return results
return [
(str(e.id()), e.Name or "Unnamed", e.Description or "")
for e in results
]
return []
@classmethod
def relating_types(cls, ifc_class=None):
return [
(str(e.id()), e.Name or "Unnamed", e.Description or "")
for e in cls.constr_class_entities(ifc_class=ifc_class)
]
@classmethod
def relating_types_browser(cls):
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):
if ifc_class not in cls.props.constr_classes:
cls.props.constr_classes.add().name = ifc_class
return cls.props.constr_classes[ifc_class]
@classmethod
def assetize_constr_class(cls, ifc_class=None):
selected_ifc_class = cls.props.ifc_class
selected_relating_type_id = cls.props.relating_type_id
if ifc_class is None:
ifc_class = cls.props.ifc_class_browser
if cls.constr_class_info(ifc_class) is None:
cls.new_constr_class_info(ifc_class)
constr_class_occurrences = cls.constr_class_entities(ifc_class)
constr_classes = cls.props.constr_classes
for constr_class_entity in constr_class_occurrences:
if (
ifc_class not in constr_classes
or constr_class_entity.Name not in constr_classes[ifc_class].constr_types
):
obj = tool.Ifc.get_object(constr_class_entity)
cls.assetize_object(obj, ifc_class, constr_class_entity)
cls.constr_class_info(ifc_class).fully_loaded = True
cls.props.updating = True
cls.props.ifc_class = selected_ifc_class
cls.props.relating_type_id = selected_relating_type_id
cls.props.updating = False
@classmethod
def assetize_object(cls, obj, ifc_class, ifc_class_entity, from_selection=False):
relating_type_id = ifc_class_entity.id()
to_be_deleted = False
if obj.type == "EMPTY":
kwargs = {}
if not from_selection:
kwargs.update({"ifc_class": ifc_class, "relating_type_id": relating_type_id})
new_obj = cls.new_relating_type(**kwargs)
if new_obj is not None:
to_be_deleted = True
obj = new_obj
obj.hide_set(True)
obj.asset_mark()
obj.asset_generate_preview()
blender33_or_above = bpy.app.version >= (3, 3, 0)
interval = 1e-4
def wait_for_asset_previews_generation(check_interval_seconds=interval):
if blender33_or_above and bpy.app.is_job_running("RENDER_PREVIEW"):
return check_interval_seconds
else:
if ifc_class not in cls.props.constr_classes:
cls.props.constr_classes.add().name = ifc_class
constr_class_info = cls.props.constr_classes[ifc_class]
# relating_type = cls.relating_type_name_by_id(ifc_class, relating_type_id)
if str(relating_type_id) not in constr_class_info.constr_types:
constr_class_info.constr_types.add().name = str(relating_type_id)
relating_type_info = constr_class_info.constr_types[str(relating_type_id)]
relating_type_info.object = obj
relating_type_info.icon_id = obj.preview.icon_id
if to_be_deleted:
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
tool.Ifc.unlink(obj=obj)
for collection in obj.users_collection:
collection.objects.unlink(obj)
return None
first_interval = 0 if blender33_or_above else interval
bpy.app.timers.register(wait_for_asset_previews_generation, first_interval=first_interval)
@classmethod
def assetize_relating_type_from_selection(cls, browser=False):
ifc_class = cls.props.ifc_class_browser if browser else cls.props.ifc_class
relating_type_id = cls.props.relating_type_id_browser if browser else cls.props.relating_type_id
constr_class_occurrences = cls.constr_class_entities(ifc_class=ifc_class)
constr_class_occurrences = [
entity for entity in constr_class_occurrences if entity.id() == int(relating_type_id)
]
if len(constr_class_occurrences) == 0:
raise ConstrTypeEntityNotFound()
constr_class_entity = constr_class_occurrences[0]
if (obj := tool.Ifc.get_object(constr_class_entity)) is None:
raise ConstrTypeEntityNotFound()
cls.assetize_object(obj, ifc_class, constr_class_entity, from_selection=True)
@staticmethod
def constr_class_info(ifc_class):
props = bpy.context.scene.BIMModelProperties
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
@classmethod
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)
)
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()
return bpy.context.selected_objects[-1]
@staticmethod
def relating_type_name_by_id(ifc_class, relating_type_id):
file = IfcStore.get_file()
try:
constr_class_entity = file.by_id(int(relating_type_id))
except (RuntimeError, ValueError):
return None
return constr_class_entity.Name if constr_class_entity.is_a() == ifc_class else None
@classmethod
def relating_type_id_by_name(cls, ifc_class, relating_type):
relating_types = [ct[0] for ct in cls.relating_types(ifc_class=ifc_class) if ct[1] == relating_type]
return None if len(relating_types) == 0 else relating_types[0]
class ArrayData:
data = {}
@@ -17,25 +17,22 @@
# 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 math import 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
white = (1, 1, 1, 1)
lightgrey = (0.7, 0.7, 0.7, 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)
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
faces_color = (0.494, 0.540, 0.593, 1)
preview_edges_color = (0.130, 0.141, 0.371, 1)
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
@@ -71,13 +68,11 @@ class ProfileDecorator:
pass
cls.installed = None
def create_batch(self, shader_type, content_pos, color, indices=None, bind=True):
batch = batch_for_shader(self.shader, shader_type, {"pos": content_pos}, indices=indices)
# TODO: what's bind is for?
if bind:
self.shader.bind()
self.shader.uniform_float("color", color)
batch.draw(self.shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_faces(self, bm, vertices_coords):
"""mutates original bm (triangulates it)
@@ -87,9 +82,15 @@ class ProfileDecorator:
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
self.create_batch("TRIS", vertices_coords, faces_color, face_indices)
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
obj = context.active_object
if obj.mode != "EDIT":
@@ -103,20 +104,8 @@ class ProfileDecorator:
else:
bm = bmesh.from_edit_mesh(obj.data)
def gl_init(use_bgl=False):
# TODO: remove as deprecated?
if use_bgl:
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
else:
gpu.state.line_width_set(2)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
bgl.glEnable(bgl.GL_LINE_SMOOTH)
gl_init(True)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
### Actually drawing
all_vertices = []
@@ -202,22 +191,31 @@ class ProfileDecorator:
unselected_edges.append(edge_indices)
### Actually drawing
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader.bind()
# Draw faces
if draw_faces:
self.draw_faces(bm, all_vertices)
self.create_batch("LINES", all_vertices, lightgrey, unselected_edges)
self.create_batch("LINES", all_vertices, green, selected_edges)
self.create_batch("LINES", all_vertices, grey, arc_edges)
self.create_batch("LINES", all_vertices, preview_edges_color, preview_edges)
self.create_batch("LINES", all_vertices, blue, roof_angle_edges)
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
self.draw_batch("LINES", all_vertices, UNSPECIAL_ELEMENT_COLOR, arc_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
self.create_batch("POINTS", unselected_vertices, lightgrey)
self.create_batch("POINTS", error_vertices, red)
self.create_batch("POINTS", special_vertices, blue)
self.create_batch("POINTS", selected_vertices, green)
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
self.draw_batch("POINTS", special_vertices, special_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
# Draw arcs
arc_centroids = []
@@ -242,9 +240,9 @@ class ProfileDecorator:
arc_centroids.append(tuple(centroid))
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
self.create_batch("POINTS", arc_centroids, grey, bind=False)
self.draw_batch("POINTS", arc_centroids, UNSPECIAL_ELEMENT_COLOR)
for verts, edges in arc_segments:
self.create_batch("LINES", verts, blue, edges, bind=False)
self.draw_batch("LINES", verts, special_elements_color, edges)
# Draw circles
circle_centroids = []
@@ -263,9 +261,9 @@ class ProfileDecorator:
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
circle_segments.append(segments)
self.create_batch("POINTS", circle_centroids, grey, bind=False)
self.draw_batch("POINTS", circle_centroids, UNSPECIAL_ELEMENT_COLOR)
for verts, edges in circle_segments:
self.create_batch("LINES", verts, blue, edges, bind=False)
self.draw_batch("LINES", verts, special_elements_color, edges)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
@@ -43,6 +43,7 @@ def update_door_modifier_representation(context):
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
sliding_door = "SLIDING" in props.door_type
representation_data = {
"operation_type": props.door_type,
@@ -87,13 +88,37 @@ def update_door_modifier_representation(context):
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
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
# Body/PLAN_VIEW representation
plan_body = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
if plan_body:
representation_data["context"] = plan_body
plan_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
tool.Model.replace_object_ifc_representation(plan_body, obj, plan_representation)
# Annotation/PLAN_VIEW representation
plan_annotation = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Annotation", "PLAN_VIEW")
if plan_annotation:
if not sliding_door:
# only sliding doors have Annotation/PLAN_VIEW
# for other types we just check for old representation and remove it if it's there
old_representation = ifcopenshell.util.representation.get_representation(
element, "Plan", "Annotation", "PLAN_VIEW"
)
if old_representation:
core.remove_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=old_representation,
)
else:
representation_data["context"] = plan_annotation
plan_representation = ifcopenshell.api.run(
"geometry.add_door_representation", ifc_file, **representation_data
)
tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation)
if plan_body or plan_annotation:
# adding switch representation at the end instead of changing order of representations
# to prevent #2744
core.switch_representation(
@@ -263,13 +288,17 @@ def update_door_modifier_bmesh(context):
door_type = props.door_type
double_swing_door = "DOUBLE_SWING" in door_type
double_door = "DOUBLE_DOOR" in door_type
sliding_door = "SLIDING" in door_type
# lining params
lining_depth = props.lining_depth * si_conversion
lining_thickness_default = props.lining_thickness * si_conversion
lining_offset = props.lining_offset * si_conversion
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_to_panel_offset_x = (
props.lining_to_panel_offset_x * si_conversion if not sliding_door else lining_thickness_default
)
panel_depth = props.panel_depth * si_conversion
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion if not sliding_door else -panel_depth
transom_thickness = props.transom_thickness * si_conversion / 2
transfom_offset = props.transom_offset * si_conversion
@@ -278,10 +307,10 @@ def update_door_modifier_bmesh(context):
window_lining_height = overall_height - transfom_offset - transom_thickness
side_lining_thickness = lining_thickness_default
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0) if not sliding_door else 0
top_lining_thickness = transom_thickness or lining_thickness_default
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0) if not sliding_door else 0
door_opening_width = overall_width - lining_to_panel_offset_x * 2
if double_swing_door:
side_lining_thickness = side_lining_thickness - panel_lining_overlap_x
@@ -296,7 +325,6 @@ def update_door_modifier_bmesh(context):
casing_depth = props.casing_depth * si_conversion
# panel params
panel_depth = props.panel_depth * si_conversion
panel_width = door_opening_width * props.panel_width_ratio
frame_depth = props.frame_depth * si_conversion
frame_thickness = props.frame_thickness * si_conversion
@@ -389,7 +417,7 @@ def update_door_modifier_bmesh(context):
panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y, threshold_thickness)
if double_door:
# TODO: keep a little space between doors for readibility?
# keeping a little space between doors for readibility
double_door_offset = 0.001 * si_conversion
panel_size.x = panel_size.x / 2 - double_door_offset
door_verts.extend(create_bm_door_panel(panel_size, panel_position, "LEFT"))
@@ -516,7 +544,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_door"
bl_label = "Cancel editing Door"
bl_label = "Cancel Editing Door"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -545,7 +573,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_door"
bl_label = "Finish editing door"
bl_label = "Finish Editing Door"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -60,9 +60,6 @@ def load_post(*args):
product.regenerate_profile_usage,
)
IfcStore.add_element_listener(wall.element_listener)
IfcStore.add_element_listener(profile.element_listener)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbWall.CalculateQuantities", wall.calculate_quantities
)
@@ -18,7 +18,6 @@
import bpy
import gpu
import bgl
import bmesh
import logging
import numpy as np
@@ -36,7 +35,6 @@ from bpy.types import Operator
from bpy.types import SpaceView3D
from bpy.props import FloatProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
@@ -217,8 +215,9 @@ class FilledOpeningGenerator:
def generate_opening_from_filling(self, filling, filling_obj, voided_obj):
thickness = voided_obj.dimensions[1] + 0.1 + 0.1
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
profile = None
filling_type = ifcopenshell.util.element.get_type(filling)
@@ -227,47 +226,59 @@ class FilledOpeningGenerator:
filling_type, "Model", "Profile", "ELEVATION_VIEW"
)
filling_obj = tool.Ifc.get_object(filling_type)
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
if profile:
curve_3d = ifcopenshell.util.representation.resolve_representation(profile).Items[0]
def get_curve_2d_from_3d(curve_3d):
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
ifc_points = ifc_file.createIfcCartesianPointList2D([Vector(p).xz for p in curve_3d.Points.CoordList])
ifc_curve = ifc_file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return ifc_curve
extrusion = shape_builder.extrude(
ifcopenshell.util.representation.resolve_representation(profile).Items[0],
get_curve_2d_from_3d(curve_3d),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_x_axis=Vector((1, 0, 0)),
position_z_axis=Vector((0, -1, 0)),
extrusion_vector=Vector((0, 0, -1)),
)
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
opening_size = Vector([x, 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
has_width_attribute, has_height_attribute = False, False
if filling.is_a() in ["IfcWindow", "IfcDoor"]:
if filling.OverallWidth:
opening_size.x = filling.OverallWidth
x_redefined = True
has_width_attribute = True
if filling.OverallHeight:
opening_size.z = filling.OverallHeight
z_redefined = True
opening_size.y = filling.OverallHeight
has_height_attribute = True
# making sure if min_x or min_z != 0 to shift the opening accordingly
# to prevent something like #2784
if not x_redefined:
if not has_width_attribute:
opening_position.x = min(v[0] for v in filling_obj.bound_box)
if not z_redefined:
if not has_height_attribute:
opening_position.z = min(v[2] for v in filling_obj.bound_box)
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=opening_size),
magnitude=thickness / unit_scale,
position=opening_position,
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_z_axis=Vector((0.0, -1.0, 0.0)),
position_x_axis=Vector((1.0, 0.0, 0.0)),
extrusion_vector=Vector((0.0, 0.0, -1.0)),
)
return shape_builder.get_representation(context, [extrusion])
@@ -325,15 +336,13 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
for building_element in decomposed_building_elements:
building_obj = tool.Ifc.get_object(building_element)
if building_obj and building_obj.data:
body = ifcopenshell.util.representation.get_representation(
building_element, "Model", "Body", "MODEL_VIEW"
)
if body:
representation = tool.Geometry.get_active_representation(building_obj)
if representation:
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=building_obj,
representation=body,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
@@ -648,7 +657,7 @@ class ShowOpenings(Operator, tool.Ifc.Operator):
props = bpy.context.scene.BIMModelProperties
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
if not element or not getattr(element, "HasOpenings", None):
continue
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
@@ -673,7 +682,6 @@ class HideOpenings(Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
to_delete = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -685,7 +693,7 @@ class HideOpenings(Operator, tool.Ifc.Operator):
if opening_obj:
to_delete.add(opening_obj)
for opening_obj in to_delete:
tool.Ifc.unlink(element=opening, obj=opening_obj)
tool.Ifc.unlink(obj=opening_obj)
bpy.data.objects.remove(opening_obj)
tool.Model.clear_scene_openings()
return {"FINISHED"}
@@ -709,13 +717,12 @@ class EditOpenings(Operator, tool.Ifc.Operator):
if opening_obj:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
building_objs.add(obj)
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
elif tool.Ifc.is_moved(opening_obj):
blenderbim.core.geometry.edit_object_placement(
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
)
building_objs.add(obj)
building_objs.add(obj)
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
@@ -759,6 +766,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
return results
# TODO: merge with ProfileDecorator?
class DecorationsHandler:
installed = None
@@ -777,27 +785,38 @@ class DecorationsHandler:
pass
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
for opening in context.scene.BIMModelProperties.openings:
obj = opening.obj
if not obj:
continue
white = (1, 1, 1, 1)
white_t = (1, 1, 1, 0.1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
red_t = (1, 0.2, 0.322, 0.1)
blue = (0.157, 0.565, 1, 1)
blue_t = (0.157, 0.565, 1, 0.1)
grey = (0.2, 0.2, 0.2, 1)
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
verts = []
@@ -829,41 +848,22 @@ class DecorationsHandler:
else:
unselected_edges.append(edge_indices)
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=selected_edges)
self.shader.uniform_float("color", green)
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": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
self.draw_batch("LINES", verts, transparent_color(unselected_elements_color, 0.5), unselected_edges)
self.draw_batch("LINES", verts, selected_elements_color, selected_edges)
self.draw_batch("POINTS", unselected_vertices, unselected_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
edges = [tuple([v.index for v in e.verts]) for e in bm.edges]
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.bind()
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
batch.draw(self.shader)
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges)
obj.data.calc_loop_triangles()
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
batch = batch_for_shader(self.shader, "TRIS", {"pos": verts}, indices=tris)
self.shader.bind()
self.shader.uniform_float("color", blue_t)
batch.draw(self.shader)
self.draw_batch("TRIS", verts, transparent_color(special_elements_color), tris)
if "HalfSpaceSolid" in obj.name:
# Arrow shape
@@ -876,10 +876,8 @@ class DecorationsHandler:
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
]
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.bind()
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
batch.draw(self.shader)
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges)
if obj.mode != "EDIT":
bm.free()
@@ -19,6 +19,7 @@
import bpy
import blenderbim.tool as tool
import blenderbim.core.spatial
import blenderbim.core.aggregate
from blenderbim.bim.ifc import IfcStore
@@ -75,6 +76,22 @@ class PieUpdateContainer(bpy.types.Operator):
break
return {"FINISHED"}
class PieAssignObjectAggregation(bpy.types.Operator,tool.Ifc.Operator):
bl_idname = "bim.pie_assign_object_aggregation"
bl_label = "Assign Parts to Active Object"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
for obj in context.selected_objects:
if obj == context.active_object:
continue
blenderbim.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=context.active_object,
related_obj=obj,
)
class VIEW3D_MT_PIE_bim(bpy.types.Menu):
bl_label = "Geometry"
@@ -86,6 +103,7 @@ class VIEW3D_MT_PIE_bim(bpy.types.Menu):
pie.operator("bim.pie_add_opening")
pie.operator("bim.pie_update_container")
pie.operator("bim.open_pie_class", text="Assign IFC Class")
pie.operator("bim.pie_assign_object_aggregation", text ="Assign Aggregation")
class VIEW3D_MT_PIE_bim_class(bpy.types.Menu):
@@ -37,12 +37,6 @@ from . import prop
import json
def select_and_activate_single_object(context, obj):
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = obj
obj.select_set(True)
class AddEmptyType(bpy.types.Operator, AddObjectHelper):
bl_idname = "bim.add_empty_type"
bl_label = "Add Empty Type"
@@ -52,7 +46,7 @@ class AddEmptyType(bpy.types.Operator, AddObjectHelper):
obj = bpy.data.objects.new("TYPEX", None)
context.scene.collection.objects.link(obj)
context.scene.BIMRootProperties.ifc_product = "IfcElementType"
select_and_activate_single_object(context, obj)
tool.Blender.select_and_activate_single_object(context, obj)
return {"FINISHED"}
@@ -97,7 +91,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
return {"FINISHED"}
elif material and material.is_a("IfcMaterialLayerSet"):
if self.generate_layered_element(ifc_class, relating_type):
select_and_activate_single_object(context, context.selected_objects[-1])
tool.Blender.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():
@@ -179,7 +173,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
if ifc_class == "IfcDoorType" and len(context.selected_objects) >= 1:
pass
else:
select_and_activate_single_object(context, obj)
tool.Blender.select_and_activate_single_object(context, obj)
return {"FINISHED"}
@staticmethod
@@ -402,7 +396,7 @@ 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."
bl_description = "Faux-mirrors the selected objects by an active empty along a mirror plane"
@classmethod
def poll(cls, context):
@@ -429,6 +423,13 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
mirror.select_set(False)
if not context.selected_objects:
self.report(
{"INFO"},
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
)
return {"FINISHED"}
bpy.ops.bim.override_object_duplicate_move(is_interactive=False)
for obj in context.selected_objects:
@@ -31,64 +31,10 @@ import blenderbim.core.geometry
from math import pi, degrees, inf
from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.wall import DumbWallRecalculator
from blenderbim.bim.module.model.decorator import ProfileDecorator
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
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":
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)
else:
material_usage = ifcopenshell.util.element.get_material(product)
x, y = obj.dimensions[0:2]
if not material_usage.CardinalPoint:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
elif material_usage.CardinalPoint == 1:
new_origin = obj.matrix_world @ Vector(obj.bound_box[4])
elif material_usage.CardinalPoint == 2:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, 0, 0)) / 2))
elif material_usage.CardinalPoint == 3:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
elif material_usage.CardinalPoint == 4:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[4]) + (Vector((0, y, 0)) / 2))
elif material_usage.CardinalPoint == 5:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
elif material_usage.CardinalPoint == 6:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((0, y, 0)) / 2))
elif material_usage.CardinalPoint == 7:
new_origin = obj.matrix_world @ Vector(obj.bound_box[7])
elif material_usage.CardinalPoint == 8:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[3]) + (Vector((x, 0, 0)) / 2))
elif material_usage.CardinalPoint == 9:
new_origin = obj.matrix_world @ Vector(obj.bound_box[3])
if (obj.matrix_world.translation - new_origin).length < 0.001:
return
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
class DumbProfileGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
@@ -108,7 +54,7 @@ class DumbProfileGenerator:
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.depth = props.extrusion_depth * self.unit_scale
self.depth = props.extrusion_depth
self.rotation = 0
self.location = Vector((0, 0, 0))
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
@@ -314,7 +260,7 @@ class DumbProfileJoiner:
body[1 if connection == "ATEND" else 0] = intersect
self.recreate_profile(element1, profile1, axis, body)
def set_depth(self, profile1, length):
def set_depth(self, profile1, si_length):
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
@@ -324,8 +270,6 @@ class DumbProfileJoiner:
axis1 = self.get_profile_axis(profile1)
axis = copy.deepcopy(axis1)
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((0, 0, si_length))
axis[1] = end
body[1] = end
@@ -892,15 +836,24 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects
def _execute(self, context):
objs = []
profile_objs = []
layer2_objs = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
usage = tool.Model.get_usage_type(element)
if usage == "PROFILE":
profile_objs.append(obj)
elif usage == "LAYER2":
layer2_objs.append(obj)
if element.ConnectedTo or element.ConnectedFrom:
objs.append(obj)
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH")
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
obj.matrix_world @= rotate_matrix
bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(objs)
DumbProfileRecalculator().recalculate(profile_objs)
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
@@ -1022,5 +975,5 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.view_layer.update()
joiner = DumbProfileJoiner()
joiner.set_depth(obj, depth / self.unit_scale)
joiner.set_depth(obj, depth)
return {"FINISHED"}
@@ -21,7 +21,6 @@ import ifcopenshell
import blenderbim.tool as tool
from blenderbim.bim.prop import ObjProperty
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
from bpy.types import PropertyGroup, NodeTree
from math import pi
@@ -38,16 +37,16 @@ def get_type_class(self, context):
return AuthoringData.data["type_class"]
def get_relating_type(self, context):
def get_boundary_class(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["relating_types_ids"]
return AuthoringData.data["boundary_class"]
def get_relating_type_browser(self, context):
def get_relating_type_id(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["relating_types_ids_browser"]
return AuthoringData.data["relating_type_id"]
def get_type_predefined_type(self, context):
@@ -56,33 +55,9 @@ def get_type_predefined_type(self, context):
return AuthoringData.data["type_predefined_type"]
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
relating_type_id = self.relating_type_id_browser if browser else self.relating_type_id
# relating_type = AuthoringData.relating_type_name_by_id(ifc_class, relating_type_id)
if ifc_class not in self.constr_classes or relating_type_id not in self.constr_classes[ifc_class].constr_types:
try:
AuthoringData.assetize_relating_type_from_selection(browser=browser)
except ConstrTypeEntityNotFound:
return
def set_icon(update_interval_seconds=1e-4):
if (
ifc_class not in self.constr_classes
or relating_type_id not in self.constr_classes[ifc_class].constr_types
):
return update_interval_seconds
else:
self.icon_id = self.constr_classes[ifc_class].constr_types[relating_type_id].icon_id
bpy.app.timers.register(set_icon)
def update_ifc_class(self, context):
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
AuthoringData.data["relating_types_ids"] = AuthoringData.relating_types()
AuthoringData.data["relating_type_id"] = AuthoringData.relating_types()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
@@ -95,21 +70,8 @@ def update_type_class(self, context):
AuthoringData.data["type_predefined_type"] = AuthoringData.type_predefined_type()
def update_ifc_class_browser(self, context):
if context.region is not None and context.region.type != "TOOL_HEADER":
AuthoringData.load_ifc_classes()
AuthoringData.load_relating_types_browser()
if self.updating:
return
ifc_class = self.ifc_class_browser
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)
def update_relating_type(self, context):
AuthoringData.load_relating_types()
def update_relating_type_id(self, context):
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
@@ -117,81 +79,20 @@ def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
def update_relating_type_browser(self, context):
AuthoringData.load_relating_types_browser()
if not self.updating:
update_icon_id(self, context, browser=True)
def update_relating_type_by_name(self, context):
AuthoringData.load_relating_types()
relating_type_id = AuthoringData.relating_type_id_by_name(self.ifc_class, self.relating_type)
if relating_type_id is not None:
self.relating_type_id = relating_type_id
def get_constr_class_info(props, ifc_class):
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
def update_preview_multiple(self, context):
if context.region is not None and context.region.type != "TOOL_HEADER":
if self.preview_multiple_constr_types:
ifc_class = self.ifc_class
constr_class_info = get_constr_class_info(self, ifc_class)
if constr_class_info is None or not constr_class_info.fully_loaded:
AuthoringData.assetize_constr_class(ifc_class)
else:
update_relating_type(self, context)
class ConstrTypeInfo(PropertyGroup):
name: bpy.props.StringProperty(name="Construction type ID")
icon_id: bpy.props.IntProperty(name="Icon ID")
object: bpy.props.PointerProperty(name="Object", type=bpy.types.Object)
class ConstrClassInfo(PropertyGroup):
name: bpy.props.StringProperty(name="Construction class")
constr_types: bpy.props.CollectionProperty(type=ConstrTypeInfo)
fully_loaded: bpy.props.BoolProperty(default=False)
class ConstrBrowserState(PropertyGroup):
cursor_x: bpy.props.IntProperty()
cursor_y: bpy.props.IntProperty()
window_x: bpy.props.IntProperty()
window_y: bpy.props.IntProperty()
far_away_x: bpy.props.IntProperty(default=10) # lower left corner to temporarily warp the mouse
far_away_y: bpy.props.IntProperty(default=10) # useful to close popup operators
updating: bpy.props.BoolProperty()
update_delay: bpy.props.FloatProperty(default=3e-2)
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
ifc_class_browser: bpy.props.EnumProperty(
items=get_ifc_class, name="Construction Class", update=update_ifc_class_browser
)
relating_type: bpy.props.StringProperty(update=update_relating_type_by_name)
relating_type_id: bpy.props.EnumProperty(
items=get_relating_type, name="Construction Type", update=update_relating_type
)
relating_type_id_browser: bpy.props.EnumProperty(
items=get_relating_type_browser, name="Construction Type", update=update_relating_type_browser
items=get_relating_type_id, name="Construction Type", update=update_relating_type_id
)
icon_id: bpy.props.IntProperty()
preview_multiple_constr_types: bpy.props.BoolProperty(default=False, update=update_preview_multiple)
updating: bpy.props.BoolProperty(default=False)
occurrence_name_style: bpy.props.EnumProperty(
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
name="Occurrence Name Style",
)
occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function")
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type}
constr_classes: bpy.props.CollectionProperty(type=ConstrClassInfo)
constr_browser_state: bpy.props.PointerProperty(type=ConstrBrowserState)
extrusion_depth: bpy.props.FloatProperty(default=42.0)
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type_id}
extrusion_depth: bpy.props.FloatProperty(default=42.0, subtype="DISTANCE")
cardinal_point: bpy.props.EnumProperty(
items=(
# TODO: complain to buildingSMART
@@ -218,14 +119,14 @@ class BIMModelProperties(PropertyGroup):
name="Cardinal Point",
default="5",
)
length: bpy.props.FloatProperty(default=42.0)
length: bpy.props.FloatProperty(default=42.0, subtype="DISTANCE")
openings: bpy.props.CollectionProperty(type=ObjProperty)
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)
rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc
rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture
x_angle: bpy.props.FloatProperty(name="X Angle", default=0)
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
type_template: bpy.props.EnumProperty(
items=(
@@ -246,13 +147,14 @@ class BIMModelProperties(PropertyGroup):
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")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
class BIMArrayProperties(PropertyGroup):
is_editing: bpy.props.IntProperty(
default=-1, description="Currently edited array index. -1 if not in array editing mode."
)
count: bpy.props.IntProperty(name="Count", default=0)
count: bpy.props.IntProperty(name="Count", default=0, min=0)
x: bpy.props.FloatProperty(name="X", default=0)
y: bpy.props.FloatProperty(name="Y", default=0)
z: bpy.props.FloatProperty(name="Z", default=0)
@@ -261,6 +163,16 @@ class BIMArrayProperties(PropertyGroup):
description="Use local space for array items offset instead of world space",
default=True,
)
method: bpy.props.EnumProperty(
items=(("OFFSET", "Offset", ""), ("DISTRIBUTE", "Distribute", "")),
name="Method",
default="OFFSET",
)
sync_children: bpy.props.BoolProperty(
name="Sync Children",
description="Regenerate all children based on the parent object",
default=False,
)
class BIMStairProperties(PropertyGroup):
@@ -342,6 +254,7 @@ class BIMWindowProperties(PropertyGroup):
)
# number of panels and default mullion/transom values
# fmt: off
window_types_panels = {
"SINGLE_PANEL": (1, ((0, 0 ), (0, 0 ))),
"DOUBLE_PANEL_HORIZONTAL": (2, ((0, 0 ), (0.45, 0 ))),
@@ -353,6 +266,7 @@ class BIMWindowProperties(PropertyGroup):
"TRIPLE_PANEL_HORIZONTAL": (3, ((0, 0 ), (0.3, 0.6))),
"TRIPLE_PANEL_VERTICAL": (3, ((0.2, 0.4), (0, 0 ))),
}
# fmt: on
window_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
@@ -455,7 +369,9 @@ class BIMDoorProperties(PropertyGroup):
("DOUBLE_SWING_LEFT", "DOUBLE_SWING_LEFT", ""),
("DOUBLE_SWING_RIGHT", "DOUBLE_SWING_RIGHT", ""),
("DOUBLE_DOOR_SINGLE_SWING", "DOUBLE_DOOR_SINGLE_SWING", ""),
("DOUBLE_DOOR_DOUBLE_SWING", "DOUBLE_DOOR_DOUBLE_SWING", ""),
("SLIDING_TO_LEFT", "SLIDING_TO_LEFT", ""),
("SLIDING_TO_RIGHT", "SLIDING_TO_RIGHT", ""),
("DOUBLE_DOOR_SLIDING", "DOUBLE_DOOR_SLIDING", ""),
)
door_added_previously: bpy.props.BoolProperty(default=False)
@@ -525,10 +441,12 @@ class BIMDoorProperties(PropertyGroup):
"lining_depth": self.lining_depth,
"lining_thickness": self.lining_thickness,
"lining_offset": self.lining_offset,
"lining_to_panel_offset_x": self.lining_to_panel_offset_x,
"lining_to_panel_offset_y": self.lining_to_panel_offset_y,
}
if "SLIDING" not in self.door_type:
kwargs["lining_to_panel_offset_x"] = self.lining_to_panel_offset_x
kwargs["lining_to_panel_offset_y"] = self.lining_to_panel_offset_y
kwargs["transom_thickness"] = self.transom_thickness
if self.transom_thickness:
kwargs["transom_offset"] = self.transom_offset
@@ -556,7 +474,18 @@ class BIMDoorProperties(PropertyGroup):
class BIMRailingProperties(PropertyGroup):
railing_types = (("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),)
railing_types = (
("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),
("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""),
)
cap_types = (
("TO_END_POST_AND_FLOOR", "TO_END_POST_AND_FLOOR", ""),
("TO_END_POST", "TO_END_POST", ""),
("TO_FLOOR", "TO_FLOOR", ""),
("TO_WALL", "TO_WALL", ""),
("180", "180", ""),
("NONE", "NONE", ""),
)
railing_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
@@ -567,13 +496,42 @@ class BIMRailingProperties(PropertyGroup):
thickness: bpy.props.FloatProperty(name="Thickness", default=0.050)
spacing: bpy.props.FloatProperty(name="Spacing", default=0.050)
# wall mounted handrail specific properties
use_manual_supports: bpy.props.BoolProperty(
name="Use Manual Supports",
default=False,
description="If enabled, supports are added on every vertex on the edges of the railing path.\n"
"If disabled, supports are added automatically based on the support spacing",
)
support_spacing: bpy.props.FloatProperty(
name="Support Spacing", default=1.0, description="Distance between supports if automatic supports are used"
)
railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050)
clear_width: bpy.props.FloatProperty(
name="Clear Width", default=0.040, description="Clear width between the railing and the wall"
)
terminal_type: bpy.props.EnumProperty(name="Terminal Type", items=cap_types, default="180")
def get_general_kwargs(self):
return {
base_kwargs = {
"railing_type": self.railing_type,
"height": self.height,
"thickness": self.thickness,
"spacing": self.spacing,
}
additional_kwargs = {}
if self.railing_type == "FRAMELESS_PANEL":
additional_kwargs = {
"thickness": self.thickness,
"spacing": self.spacing,
}
elif self.railing_type == "WALL_MOUNTED_HANDRAIL":
additional_kwargs = {
"railing_diameter": self.railing_diameter,
"clear_width": self.clear_width,
"use_manual_supports": self.use_manual_supports,
"support_spacing": self.support_spacing,
"terminal_type": self.terminal_type,
}
return base_kwargs | additional_kwargs
class BIMRoofProperties(PropertyGroup):
@@ -18,14 +18,11 @@
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
@@ -41,6 +38,15 @@ import json
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailingType.htm
NON_SI_RAILING_PROPS = (
"is_editing",
"railing_type",
"railing_added_previously",
"use_manual_supports",
"terminal_type",
)
def bm_split_edge_at_offset(edge, offset):
v0, v1 = edge.verts
@@ -82,7 +88,31 @@ def update_railing_modifier_ifc_data(context):
"Height": props.height,
},
)
tool.Ifc.edit(obj)
if props.railing_type == "WALL_MOUNTED_HANDRAIL":
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
railing_path = [Vector(v) for v in RailingData.data["parameters"]["data_dict"]["path_data"]["verts"]]
representation_data = {
"railing_type": props.railing_type,
"context": body,
"railing_path": railing_path,
"use_manual_supports": props.use_manual_supports,
"support_spacing": props.support_spacing,
"railing_diameter": props.railing_diameter,
"clear_width": props.clear_width,
"terminal_type": props.terminal_type,
"height": props.height,
}
model_representation = ifcopenshell.api.run(
"geometry.add_railing_representation", ifc_file, **representation_data
)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# hacky way to ensure tha ifc representation won't get tessellated at project save
IfcStore.edited_objs.discard(obj)
elif props.railing_type == "FRAMELESS_PANEL":
tool.Ifc.edit(obj)
def update_bbim_railing_pset(element, railing_data):
@@ -118,77 +148,114 @@ def update_railing_modifier_bmesh(context):
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
if props.railing_type != "FRAMELESS_PANEL":
return
# 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)
def generate_frameless_panel_railing():
# generating FRAMELESS_PANEL railing
height = props.height * si_conversion
thickness = props.thickness * si_conversion
spacing = props.spacing * si_conversion
# 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])
# 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)
edge_dir = ((v1.co - v0.co) * V(1, 1, 0)).normalized()
ortho_vector = edge_dir.cross(V(0, 0, 1))
# 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])
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
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"]
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
extrusion_vector = Vector((0, 0, 1)) * height
bmesh.ops.translate(bm, vec=extrusion_vector, 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 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)
# dissolve middle edge
bmesh.ops.dissolve_edges(bm, edges=[main_edge])
# to remove unnecessary verts in 0 spacing case
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
# 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)
tool.Blender.apply_bmesh(obj.data, bm)
# 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)
generate_frameless_panel_railing()
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"]:
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
end_points = [v for v in bm.verts if len(v.link_edges) == 1]
if not end_points:
return None
# if we have some previous data then we try to match
# start or end of the path with the previous path
previous_data = False
if previous_data:
previous_start = previous_data[0]
previous_end = previous_data[-1]
potential_start = min([(v, (v.co - previous_start).length) for v in end_points], key=lambda v_data: v_data[1])
potential_end = min([(v, (v.co - previous_end).length) for v in end_points], key=lambda v_data: v_data[1])
if potential_start[1] < potential_end[1]:
start_point = potential_start[0]
else:
start_point = next(v for v in end_points if v != potential_start[0])
else:
start_point = min(end_points, key=lambda v: v.index)
# walking through the path
# to make sure all verts and in consequent order
edge = start_point.link_edges[0]
v = edge.other_vert(start_point)
points = [start_point.co, v.co]
segments = [(0, 1)]
i = 2
other_edge = lambda edges, edge: next(e for e in edges if e != edge)
while len(link_edges := v.link_edges) != 1:
link_edges = v.link_edges
edge = other_edge(link_edges, edge)
v = edge.other_vert(v)
points.append(v.co)
segments.append((i - 1, i))
i += 1
path_data = {"edges": segments, "verts": [p / si_conversion for p in points]}
return path_data
@@ -248,8 +315,7 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
# 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)
convert_property_group_from_si(props, skip_props=NON_SI_RAILING_PROPS)
railing_data = props.get_general_kwargs()
path_data = get_path_data(obj)
@@ -289,8 +355,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
# 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())
skip_props = NON_SI_RAILING_PROPS + tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
@@ -299,7 +364,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_railing"
bl_label = "Cancel editing Railing"
bl_label = "Cancel Editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -328,7 +393,7 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_railing"
bl_label = "Finish editing railing"
bl_label = "Finish Editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -349,6 +414,37 @@ class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class FlipRailingPathOrder(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.flip_railing_path_order"
bl_label = "Flip Railing Path Order"
bl_description = "Can be useful to maintain railing supports direction"
bl_options = {"REGISTER", "UNDO"}
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"]
# flip the vertex order and edges
path_data["verts"] = path_data["verts"][::-1]
last_vert_i = len(path_data["verts"]) - 1
edges = []
for edge in path_data["edges"][::-1]:
edge = [abs(vi - last_vert_i) for vi in edge[::-1]]
edges.append(edge)
railing_data = props.get_general_kwargs()
railing_data["path_data"] = path_data
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"
@@ -17,7 +17,6 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import Operator
import bmesh
import ifcopenshell
@@ -29,20 +28,18 @@ from blenderbim.bim.module.model.data import RoofData, refresh
from blenderbim.bim.module.model.decorator import ProfileDecorator
import json
from math import tan, radians, degrees, atan
from math import tan, pi
from mathutils import Vector, Matrix
from bpypolyskel import bpypolyskel
import shapely
from pprint import pprint
from itertools import chain
from math import pi
# 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
# create read only property in blender operator
NON_SI_ROOF_PROPS = ("is_editing", "roof_type", "roof_added_previously", "generation_method", "angle")
def float_is_zero(f):
@@ -116,10 +113,13 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
original_geometry_data = dict()
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
if angle_layer:
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), e[angle_layer]) for e in bm.edges]
original_geometry_data["edges"] = [
(set(bm_get_indices(e.verts)), e[angle_layer], e[separate_verts_layer]) for e in bm.edges
]
else:
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None) for e in bm.edges]
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None, None) for e in bm.edges]
original_geometry_data["verts"] = {v.index: v.co.copy() for v in bm.verts}
footprint_z = bm.verts[:][0].co.z
@@ -212,6 +212,8 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
footprint_edges = []
footprint_verts = set()
verts_to_change = {}
verts_to_rip = []
bottom_chords_to_remove = []
# find footprint edges
for edge in bm.edges:
@@ -227,7 +229,7 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
# iterate over edges from original geometry
# if their angle was redefined by user - apply the changes to the related vertices
# to match the requested angle
for old_edge_verts, defined_angle in original_geometry_data["edges"]:
for old_edge_verts, defined_angle, separate_verts in original_geometry_data["edges"]:
if not defined_angle:
continue
@@ -240,14 +242,38 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
verts_to_move = find_other_polygon_verts(identical_edge)
for v in verts_to_move:
vert_co = verts_to_change.get(v, v.co)
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_change[v] = new_vert_co
if not separate_verts:
vert_co = verts_to_change.get(v, v.co)
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_change[v] = new_vert_co
else:
vert_co = v.co
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_rip.append([v, new_vert_co, identical_edge])
if defined_angle >= pi / 2:
bottom_chords_to_remove.append(identical_edge)
def separate_vert(bm, vert, edge, new_co):
face = next(f for f in vert.link_faces if edge in f.edges)
new_v = bmesh.utils.face_vert_separate(face, vert)
new_v.co = new_co
new_edge = bm.edges.new((vert, new_v))
for cur_edge in face.edges:
if cur_edge == edge:
continue
bmesh.ops.contextual_create(bm, geom=[new_edge, cur_edge])
# apply all changes once at the end
for v in verts_to_change:
v.co = verts_to_change[v]
for v, new_co, edge in verts_to_rip:
separate_vert(bm, v, edge, new_co)
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
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)
@@ -289,9 +315,6 @@ def update_roof_modifier_ifc_data(context):
# 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)
@@ -311,12 +334,14 @@ def update_roof_modifier_bmesh(context):
RoofData.load()
path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
angle_layer_data = path_data.get("gable_roof_angles", None)
separate_verts_data = path_data.get("gable_roof_separate_verts", None)
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)
angle_layer = bm.edges.layers.float.new("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.new("BBIM_gable_roof_separate_verts")
# generating roof path
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
@@ -325,6 +350,7 @@ def update_roof_modifier_bmesh(context):
e = path_data["edges"][i]
edge = bm.edges.new((new_verts[e[0]], new_verts[e[1]]))
edge[angle_layer] = angle_layer_data[i] if angle_layer_data else 0
edge[separate_verts_layer] = separate_verts_data[i] if separate_verts_data else 0
new_edges.append(edge)
if props.is_editing_path:
@@ -349,12 +375,15 @@ def get_path_data(obj):
bm_mesh_clean_up(bm)
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
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 angle_layer:
path_data["gable_roof_angles"] = [e[angle_layer] for e in bm.edges]
if separate_verts_layer:
path_data["gable_roof_separate_verts"] = [e[separate_verts_layer] for e in bm.edges]
if not path_data["edges"] or not path_data["verts"]:
return None
@@ -417,21 +446,32 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
# 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)
convert_property_group_from_si(props, skip_props=NON_SI_ROOF_PROPS)
# rejecting original roof shape to be safe
# taking into account only it's bounding box dimensions
if obj.dimensions.x == 0 or obj.dimensions.y == 0:
min_x, min_y = -5, -5
max_x, max_y = 5, 5
min_z = 0
else:
bbox = tool.Blender.get_object_bounding_box(obj)
min_x = bbox["min_x"]
min_y = bbox["min_y"]
max_x = bbox["max_x"]
max_y = bbox["max_y"]
min_z = bbox["min_z"]
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,
],
}
path_data = {
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
"verts": [
Vector([min_x, min_y, min_z]) / si_conversion,
Vector([min_x, max_y, min_z]) / si_conversion,
Vector([max_x, max_y, min_z]) / si_conversion,
Vector([max_x, min_y, min_z]) / si_conversion,
],
}
roof_data["path_data"] = path_data
update_bbim_roof_pset(element, roof_data)
@@ -459,8 +499,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
# 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())
skip_props = NON_SI_ROOF_PROPS + tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
@@ -469,7 +508,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_roof"
bl_label = "Cancel editing Roof"
bl_label = "Cancel Editing Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -498,7 +537,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_roof"
bl_label = "Finish editing roof"
bl_label = "Finish Editing Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -633,9 +672,10 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
class SetGableRoofEdgeAngle(bpy.types.Operator):
bl_idname = "bim.set_gable_roof_edge_angle"
bl_label = "Set gable roof edge angle"
bl_label = "Set Gable Roof Edge Angle"
bl_options = {"REGISTER", "UNDO"}
angle: bpy.props.FloatProperty(name="Angle", default=90)
separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
@classmethod
def poll(cls, context):
@@ -658,12 +698,17 @@ class SetGableRoofEdgeAngle(bpy.types.Operator):
if "BBIM_gable_roof_angles" not in me.attributes:
me.attributes.new("BBIM_gable_roof_angles", type="FLOAT", domain="EDGE")
if "BBIM_gable_roof_separate_verts" not in me.attributes:
me.attributes.new("BBIM_gable_roof_separate_verts", type="INT", domain="EDGE")
angles_layer = bm.edges.layers.float["BBIM_gable_roof_angles"]
separate_verts_layer = bm.edges.layers.int["BBIM_gable_roof_separate_verts"]
for e in bm.edges:
if not e.select:
continue
e[angles_layer] = self.angle
e[separate_verts_layer] = self.separate_verts
tool.Blender.apply_bmesh(me, bm)
return {"FINISHED"}
@@ -48,8 +48,3 @@ def sync_name(usecase_path, ifc_file, settings):
collection.name = new_name
obj.name = new_name
blenderbim.bim.handler.refresh_ui_data()
class ConstrTypeEntityNotFound(Exception):
pass
@@ -139,7 +139,7 @@ class DumbSlabGenerator:
self.length = 3
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)
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
return self.derive_from_cursor()
def derive_from_cursor(self):
@@ -238,11 +238,11 @@ class DumbSlabPlaner:
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
self.change_thickness(element, thickness)
self.change_thickness(element, total_thickness)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
self.change_thickness(element, thickness)
self.change_thickness(element, total_thickness)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
obj = tool.Ifc.get_object(settings["related_object"])
@@ -274,7 +274,7 @@ class DumbSlabPlaner:
extrusion.Depth = thickness
else:
props = bpy.context.scene.BIMModelProperties
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
new_rep = ifcopenshell.api.run(
"geometry.add_slab_representation",
tool.Ifc.get(),
@@ -299,7 +299,7 @@ class DumbSlabPlaner:
return
else:
props = bpy.context.scene.BIMModelProperties
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
representation = ifcopenshell.api.run(
"geometry.add_slab_representation",
tool.Ifc.get(),
@@ -604,6 +604,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion.Position:
@@ -636,8 +637,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(representation)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.resolve_representation(body)
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
@@ -670,37 +672,42 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.data.meshes.remove(profile_mesh)
# Only certain classes should have a footprint
if element.is_a() not in ("IfcSlab", "IfcRamp"):
return
footprint_context = ifcopenshell.util.representation.get_context(
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
)
if footprint_context:
curves = [profile.OuterCurve]
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
curves.extend(profile.InnerCurves)
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
if not footprint_context:
return
curves = [profile.OuterCurve]
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
curves.extend(profile.InnerCurves)
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
)
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
)
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
blenderbim.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint
)
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
else:
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_footprint
)
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
blenderbim.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint
)
else:
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_footprint
)
return {"FINISHED"}
class ResetVertex(bpy.types.Operator):
@@ -26,12 +26,20 @@ import blenderbim.tool as tool
import blenderbim.core.type
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_options = {"REGISTER"}
bl_description = "Create a space from the cursor position. Move the cursor position into the desired position, select the right space collection and run the operator"
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
return tool.Ifc.get_entity(collection_obj)
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -65,8 +73,13 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
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
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
x = global_bbox_center.x
y = global_bbox_center.y
z = (mat @ Vector(active_obj.bound_box[0])).z
h = active_obj.dimensions.z
else:
x, y = context.scene.cursor.location.xy
@@ -166,3 +179,177 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
],
}
class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_spaces_from_walls"
bl_label = "Generate Spaces From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Generate spaces from selected walls. The active object must be a wall."
@classmethod
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
return context.selected_objects and element.is_a("IfcWall")
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
# walls (i.e. prismatic) in the active object storey.
# In order to run, the active objct must be a wall and
# have to be selected walls
props = context.scene.BIMModelProperties
active_obj = bpy.context.active_object
if not active_obj:
self.report({'ERROR'}, "No active object. Please select a wall")
return
element = None
element = tool.Ifc.get_entity(active_obj)
if element:
if not element.is_a("IfcWall"):
self.report({'ERROR'}, "The active object is not a wall. Please select a wall.")
return
collection = active_obj.users_collection[0]
collection_obj = bpy.data.objects.get(collection.name)
if not collection_obj:
self.report({'ERROR'}, "No collection found. Please insert one.")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
self.report({'ERROR'}, "The collection hasn't an ifc space entity. Please provide one.")
return
if not bpy.context.selected_objects:
self.report({'ERROR'}, "No selected objects found. Please select walls.")
return
x, y, z = active_obj.matrix_world.translation.xyz
mat = active_obj.matrix_world
h = active_obj.dimensions.z
selected_objects = bpy.context.selected_objects
boundary_elements = self.get_boundary_elements(selected_objects)
polys = self.get_polygons(boundary_elements)
converted_tolerance = self.get_converted_tolerance(tolerance = 0.03)
union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style = 2, join_style = 2)
i=0
for linear_ring in union.interiors:
poly = Polygon(linear_ring)
poly = poly.buffer(converted_tolerance, single_sided=True, cap_style = 2, join_style = 2)
bm = self.get_bmesh_from_polygon(poly, mat, h)
name = "Space" + str(i)
mesh = bpy.data.meshes.new(name = name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
obj.matrix_world = mat
self.set_obj_origin_to_bboxcenter(obj)
collection.objects.link(obj)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
i+=1
return {"FINISHED"}
def get_boundary_elements(self, selected_objects):
boundary_elements = []
for obj in selected_objects:
subelement = tool.Ifc.get_entity(obj)
if subelement.is_a("IfcWall"):
boundary_elements.append(subelement)
return boundary_elements
def get_polygons(self, boundary_elements):
polys = []
for boundary_element in boundary_elements:
obj = tool.Ifc.get_object(boundary_element)
if not obj:
continue
points = []
base = self.get_obj_base_points(obj)
for index in ["low_left", "low_right", "high_right", "high_left"]:
point = base[index]
points.append(point)
polys.append(Polygon(points))
return polys
def get_obj_base_points(self, obj):
x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box]
y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box]
return {
"low_left": (x_values[0], y_values[0]),
"high_left": (x_values[3], y_values[3]),
"low_right": (x_values[4], y_values[4]),
"high_right": (x_values[7], y_values[7]),
}
def get_converted_tolerance(self, tolerance):
model = tool.Ifc.get()
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
prefix=getattr(project_unit, "Prefix", None)
converted_tolerance = ifcopenshell.util.unit.convert(
value = tolerance,
from_prefix = None,
from_unit = "METRE",
to_prefix = prefix,
to_unit = project_unit.Name,
)
return tolerance
def get_bmesh_from_polygon(self, poly, mat, h):
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], 0])) for v in poly.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]))
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)
bmesh.ops.recalc_face_normals(bm, faces = bm.faces)
return bm
def set_obj_origin_to_bboxcenter(self, obj):
mat = obj.matrix_world
inverted = mat.inverted()
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
oldLoc = obj.location
newLoc = global_bbox_center
diff = newLoc - oldLoc
for vert in obj.data.vertices:
aux_vector = mat @ vert.co
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
@@ -287,6 +287,22 @@ def update_ifc_stair_props(obj):
)
tool.Ifc.edit(obj)
# update related annotation objects
def get_elements_from_product(product):
elements = []
for rel in product.ReferencedBy:
if not rel.is_a("IfcRelAssignsToProduct"):
continue
elements.extend(rel.RelatedObjects)
return elements
stair_obj = obj
for rel_element in get_elements_from_product(element):
if not rel_element.is_a("IfcAnnotation") or rel_element.ObjectType != "STAIR_ARROW":
continue
if annotation_obj := tool.Ifc.get_object(rel_element):
tool.Drawing.setup_annotation_object(annotation_obj, "STAIR_ARROW", stair_obj)
class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_clever_stair"
@@ -368,7 +384,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_stair"
bl_label = "Cancel editing Stair"
bl_label = "Cancel Editing Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -388,7 +404,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_stair"
bl_label = "Finish editing stair"
bl_label = "Finish Editing Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -182,7 +182,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
# removed the part that was relying on node graph to be opened at execution
class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.import_sverchok_graph"
bl_label = "Import sverchok graph"
bl_label = "Import Sverchok Graph"
bl_options = {"REGISTER"}
filepath: bpy.props.StringProperty(
@@ -224,7 +224,7 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
# removed the part that was relying on node graph to be opened at execution
class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.export_sverchok_graph"
bl_label = "Export sverchok graph"
bl_label = "Export Sverchok Graph"
bl_options = {"REGISTER"}
filepath: bpy.props.StringProperty(
@@ -132,62 +132,9 @@ class BIM_PT_authoring(Panel):
def draw(self, context):
row = self.layout.row(align=True)
row.operator("bim.align_wall", icon="ANCHOR_TOP", text="Ext.").align_type = "EXTERIOR"
row.operator("bim.align_wall", icon="ANCHOR_CENTER", text="C/L").align_type = "CENTERLINE"
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
class HelpConstrTypes(Operator):
bl_idname = "bim.help_relating_types"
bl_label = "Construction Types Help"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Click to read some contextual help"
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=510)
def draw(self, context):
layout = self.layout
layout.row().separator(factor=0.5)
row = layout.row()
row.alignment = "CENTER"
row.label(text="BlenderBIM Help", icon="BLENDER")
layout.row().separator(factor=0.5)
row = layout.row().row()
row.label(text="Overview:", icon="KEYTYPE_MOVING_HOLD_VEC")
self.draw_lines(layout, self.message_summary)
layout.row().separator()
row = layout.row().row()
row.label(text="Further support:", icon="KEYTYPE_MOVING_HOLD_VEC")
layout.row().separator(factor=0.5)
row = layout.row()
op = row.operator("bim.open_upstream", text="Homepage", icon="HOME")
op.page = "home"
op = row.operator("bim.open_upstream", text="Docs", icon="DOCUMENTS")
op.page = "docs"
op = row.operator("bim.open_upstream", text="Wiki", icon="CURRENT_FILE")
op.page = "wiki"
op = row.operator("bim.open_upstream", text="Community", icon="COMMUNITY")
op.page = "community"
layout.row().separator()
def draw_lines(self, layout, lines):
box = layout.box()
for line in lines:
row = box.row()
row.label(text=f" {line}")
@property
def message_summary(self):
return [
"The Construction Type Browser allows to preview and add new instances to the model.",
"For further support, please click on the Documentation link below.",
]
row.operator("bim.generate_space")
row = self.layout.row(align=True)
row.operator("bim.generate_spaces_from_walls")
class BIM_PT_array(bpy.types.Panel):
@@ -224,21 +171,29 @@ class BIM_PT_array(bpy.types.Panel):
row.operator("bim.edit_array", icon="CHECKMARK", text="").item = i
row.operator("bim.disable_editing_array", icon="CANCEL", text="")
row = box.row(align=True)
row.prop(props, "use_local_space")
row.prop(props, "method")
row = box.row(align=True)
row.prop(props, "use_local_space")
row.prop(props, "sync_children")
col = box.column()
row = col.row(align=True)
row.prop(props, "x")
row.operator("bim.input_cursor_x_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "y")
row.operator("bim.input_cursor_y_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "z")
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
else:
row = box.row(align=True)
row.label(text=f"{array['count']} Items", icon="MOD_ARRAY")
name = f"{array['count']} Items ({array.get('method', 'OFFSET').capitalize()})"
row.label(text=name, icon="MOD_ARRAY")
row.operator("bim.enable_editing_array", icon="GREASEPENCIL", text="").item = i
row.operator("bim.remove_array", icon="X", text="").item = i
row = box.row(align=True)
row.label(text=f"Use Local Space: {array.get('use_local_space', False)}")
row = box.row(align=True)
row.label(text=f"X: {array['x']}")
icon = "EMPTY_ARROWS" if array.get("use_local_space", False) else "EMPTY_AXIS"
row.label(text=f"X: {array['x']}", icon=icon)
row.label(text=f"Y: {array['y']}")
row.label(text=f"Z: {array['z']}")
else:
@@ -273,7 +228,7 @@ class BIM_PT_stair(bpy.types.Panel):
stair_data = StairData.data["parameters"]["data_dict"]
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
@@ -373,7 +328,7 @@ class BIM_PT_window(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -488,7 +443,7 @@ class BIM_PT_door(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -572,7 +527,7 @@ class BIM_PT_railing(bpy.types.Panel):
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.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -588,6 +543,9 @@ class BIM_PT_railing(bpy.types.Panel):
else:
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
# TODO: good for preview but probably should move to .is_editing == -1
# since it's writing to ifc
row.operator("bim.flip_railing_path_order", icon="ARROW_LEFTRIGHT", text="")
row.operator("bim.remove_railing", icon="X", text="")
box = self.layout.box()
@@ -631,7 +589,7 @@ class BIM_PT_roof(bpy.types.Panel):
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.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -656,7 +614,7 @@ class BIM_PT_roof(bpy.types.Panel):
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}")
if prop == 'angle':
if prop == "angle":
prop_value = round(degrees(prop_value), 2)
row.label(text=str(prop_value))
else:
@@ -37,38 +37,6 @@ from mathutils import Vector, Matrix
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2":
return
if obj.mode == "EDIT":
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)
else:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.join_wall"
bl_label = "Join Wall"
@@ -239,6 +207,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
layer2_objs = []
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -251,7 +220,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
return
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
extrusion.Depth = self.depth * (1 / cos(x_angle))
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
@@ -271,7 +240,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_x_angle"
bl_label = "Change Extrusion X Angle"
bl_options = {"REGISTER", "UNDO"}
x_angle: bpy.props.FloatProperty()
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
@classmethod
def poll(cls, context):
@@ -280,7 +249,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
layer2_objs = []
other_objs = []
x_angle = radians(self.x_angle)
x_angle = self.x_angle
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -467,11 +436,11 @@ class DumbWallGenerator:
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.width = self.layers["thickness"]
self.height = props.extrusion_depth * self.unit_scale
self.length = props.length * self.unit_scale
self.height = props.extrusion_depth
self.length = props.length
self.rotation = 0.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)
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
return self.derive_from_cursor()
@@ -1028,7 +997,7 @@ class DumbWallJoiner:
self.recreate_wall(element1, wall1, axis, body)
def set_length(self, wall1, length):
def set_length(self, wall1, si_length):
element1 = tool.Ifc.get_entity(wall1)
if not element1:
return
@@ -1038,8 +1007,6 @@ class DumbWallJoiner:
axis1 = tool.Model.get_wall_axis(wall1)
axis = copy.deepcopy(axis1["reference"])
body = copy.deepcopy(axis1["reference"])
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
si_length = unit_scale * length
end = (wall1.matrix_world @ Vector((si_length, 0, 0))).to_2d()
axis[1] = end
body[1] = end
@@ -69,10 +69,12 @@ def update_simple_openings(element, opening_width, opening_height):
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height])),
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height]).xz),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_x_axis=V(1, 0, 0),
position_z_axis=V(0, -1, 0),
extrusion_vector=V(0, 0, -1),
)
new_representation = shape_builder.get_representation(context, extrusion)
@@ -480,7 +482,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_window"
bl_label = "Cancel editing Window"
bl_label = "Cancel Editing Window"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -508,7 +510,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_window"
bl_label = "Finish editing window"
bl_label = "Finish Editing Window"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -44,6 +44,7 @@ class BimTool(WorkSpaceTool):
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
# ("bim.sync_modeling", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.hotkey", {"type": "B", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_B")]}),
("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")]}),
@@ -57,6 +58,7 @@ class BimTool(WorkSpaceTool):
("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": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}),
("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")]}),
@@ -68,7 +70,17 @@ class BimTool(WorkSpaceTool):
def add_layout_hotkey_operator(layout, text, hotkey, description):
op = layout.operator("bim.hotkey", text=text)
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op
@@ -101,7 +113,7 @@ class BimToolUI:
@classmethod
def draw_create_object_interface(cls):
if not AuthoringData.data["relating_types_ids"]:
if not AuthoringData.data["relating_type_id"]:
return
if cls.props.ifc_class == "IfcWallType":
row = cls.layout.row(align=True)
@@ -132,10 +144,7 @@ class BimToolUI:
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__)
add_layout_hotkey_operator(cls.layout, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
@classmethod
def draw_edit_object_interface(cls, context):
@@ -155,52 +164,25 @@ class BimToolUI:
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
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_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Y")
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_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")
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")
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")
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
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.recalculate_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "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")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Profile").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
elif AuthoringData.data["active_material_usage"] == "PROFILE":
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="cardinal_point", text="Axis")
@@ -215,32 +197,26 @@ class BimToolUI:
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
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"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Y")
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
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.recalculate_profile.__doc__)
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not context.active_object.BIMRailingProperties.is_editing_path
):
# NOTE: should be above "active_representation_type" = "SweptSolid" check
# because it could be a SweptSolid too
add_layout_hotkey_operator(cls.layout, "Edit Railing Path", "S_E", "")
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
elif AuthoringData.data["active_class"] in (
"IfcWindow",
"IfcWindowStandardCase",
@@ -254,20 +230,11 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl1", text="RL")
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.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"
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "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__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
elif AuthoringData.data["active_class"] in (
"IfcCableCarrierSegmentType",
@@ -275,39 +242,22 @@ class BimToolUI:
"IfcDuctSegmentType",
"IfcPipeSegmentType",
):
row = cls.layout.row(align=True)
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"
add_layout_hotkey_operator(cls.layout, "Extend", "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"
add_layout_hotkey_operator(cls.layout, "Edit Roof Path", "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"
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if len(context.selected_objects) == 2:
row.operator("bim.add_opening", text="Apply Void")
else:
@@ -320,31 +270,22 @@ class BimToolUI:
else:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
row = cls.layout.row(align=True)
row.label(text="Align")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Exterior", icon="EVENT_X")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Centerline", icon="EVENT_C")
row = cls.layout.row(align=True)
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.label(text="", icon="EVENT_B")
row.prop(cls.props, "boundary_class", text="")
row.operator("bim.add_boundary", text="Add Boundary")
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")
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")
row.operator("bim.hotkey", text="Decomposition").hotkey = "A_D"
cls.layout.row(align=True).label(text="Align")
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "")
add_layout_hotkey_operator(cls.layout, "Align Interior", "S_V", "")
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
cls.layout.row(align=True).label(text="Mode")
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
add_layout_hotkey_operator(cls.layout, "Boundaries", "A_B", "Toggle boundaries")
@classmethod
def draw_header_interface(cls):
@@ -369,7 +310,7 @@ class BimToolUI:
prop_with_search(row, cls.props, "ifc_class", text="")
row = cls.layout.row(align=True)
if AuthoringData.data["relating_types_ids"]:
if AuthoringData.data["relating_type_id"]:
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
else:
@@ -475,6 +416,16 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects:
return
# NOTE: placing it before the other operations because railing can also be SweptSolid
# and it might conflict with one of the conditions below
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
):
bpy.ops.bim.enable_editing_railing_path()
return
selected_usages = {}
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -483,8 +434,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
continue
usage = tool.Model.get_usage_type(element)
if not usage:
obj.select_set(False)
continue
representation = tool.Geometry.get_active_representation(obj)
if representation and representation.RepresentationType == "SweptSolid":
usage = "SWEPTSOLID"
else:
obj.select_set(False)
continue
selected_usages.setdefault(usage, []).append(obj)
if len(bpy.context.selected_objects) == 1:
@@ -498,7 +453,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
elif self.active_material_usage == "PROFILE":
# Extend PROFILE to cursor
bpy.ops.bim.extend_profile(join_type="T")
else:
# Edit SWEPTSOLID profile (assuming single profile for now)
bpy.ops.bim.enable_editing_extrusion_profile()
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", [])]
@@ -523,15 +480,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
[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"]
@@ -573,7 +521,13 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if self.active_material_usage == "LAYER2":
bpy.ops.bim.merge_wall()
else:
bpy.ops.bim.mirror_elements()
if len(bpy.context.selected_objects) == 1:
self.report(
{"INFO"},
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
)
else:
bpy.ops.bim.mirror_elements()
def hotkey_S_R(self):
if not bpy.context.selected_objects:
@@ -624,6 +578,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.extend_profile(join_type="V")
def hotkey_S_B(self):
bpy.ops.bim.add_boundary()
def hotkey_S_O(self):
if len(bpy.context.selected_objects) == 2:
bpy.ops.bim.add_opening()
@@ -634,6 +591,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
self.props.y = self.y
self.props.z = self.z
def hotkey_A_B(self):
if not bpy.context.selected_objects:
return
if AuthoringData.data["has_visible_boundaries"]:
bpy.ops.bim.hide_boundaries()
else:
bpy.ops.bim.show_boundaries()
def hotkey_A_D(self):
if not bpy.context.selected_objects:
return
@@ -102,7 +102,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
class UpdateIfcPatchArguments(bpy.types.Operator):
bl_idname = "bim.update_ifc_patch_arguments"
bl_label = "Update IFC Patch arguments"
bl_label = "Update IFC Patch Arguments"
recipe: bpy.props.StringProperty()
def execute(self, context):
@@ -136,6 +136,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
props = context.scene.BIMProfileProperties
profile = tool.Ifc.get().by_id(props.active_profile_id)
obj = tool.Model.import_profile(profile)
tool.Ifc.link(profile, obj)
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="EDIT")
@@ -812,7 +812,6 @@ class ExportIFC(bpy.types.Operator):
bl_idname = "export_ifc.bim"
bl_label = "Export IFC"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export the IFC project"
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@@ -892,6 +891,12 @@ class ExportIFC(bpy.types.Operator):
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
@classmethod
def description(cls, context, properties):
if properties.should_save_as:
return "Export the IFC project to a selected file"
return "Export the IFC project to this file"
class ImportIFC(bpy.types.Operator):
bl_idname = "import_ifc.bim"
@@ -96,7 +96,10 @@ class MaterialPsetsData(Data):
@classmethod
def load(cls):
cls.data = {"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material))}
cls.data = {
"ifc_definition_id": bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id,
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material)),
}
cls.is_loaded = True
@@ -248,6 +248,7 @@ class EditPset(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
objects = tool.Blender.get_selected_objects()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
element = tool.Ifc.get().by_id(ifc_definition_id)
@@ -275,29 +276,42 @@ class EditPset(bpy.types.Operator, Operator):
e[value_name] for e in prop.enumerated_value.enumerated_values if e.is_selected
]
if pset.is_a() in ("IfcPropertySet", "IfcMaterialProperties", "IfcProfileProperties"):
ifcopenshell.api.run(
"pset.edit_pset",
self.file,
pset=pset,
name=props.active_pset_name,
properties=properties,
pset_template=blenderbim.bim.schema.ifc.psetqto.get_by_name(props.active_pset_name),
)
else:
for key, value in properties.items():
if isinstance(value, float):
properties[key] = round(value, 4)
ifcopenshell.api.run(
"pset.edit_qto",
self.file,
qto=pset,
name=props.active_pset_name,
properties=properties,
)
bpy.ops.bim.load_cost_item_quantities()
for obj in objects:
element = blenderbim.tool.Ifc.get_entity(obj)
pset_list = element.IsDefinedBy
copy_of_properties = properties.copy()
for rel in pset_list:
rel_name = rel.RelatingPropertyDefinition.Name
if rel.is_a("IfcRelDefinesByProperties") and rel_name == props.active_pset_name:
rel_ID = rel.RelatingPropertyDefinition.id()
rel_pset = rel.RelatingPropertyDefinition
if rel_pset.is_a() in ("IfcPropertySet", "IfcMaterialProperties", "IfcProfileProperties"):
ifcopenshell.api.run(
"pset.edit_pset",
self.file,
pset=rel_pset,
name=props.active_pset_name,
properties=copy_of_properties,
pset_template=blenderbim.bim.schema.ifc.psetqto.get_by_name(props.active_pset_name),
)
else:
for key, value in copy_of_properties.items():
if isinstance(value, float):
copy_of_properties[key] = round(value, 4)
ifcopenshell.api.run(
"pset.edit_qto",
self.file,
qto=rel_pset,
name=props.active_pset_name,
properties=copy_of_properties,
)
if tool.Cost.has_schedules():
tool.Cost.update_cost_items(pset=rel_pset)
bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type)
tool.Blender.update_viewport()
class RemovePset(bpy.types.Operator, Operator):
@@ -380,32 +394,21 @@ class CalculateQuantity(bpy.types.Operator):
self.qto_calculator = QtoCalculator()
obj = context.active_object
prop = obj.PsetProperties.properties.get(self.prop)
prop.metadata.float_value = self.calculate_quantity(obj, context)
quantity = self.calculate_quantity(obj, context)
if quantity is None:
self.report({"ERROR"}, "Could not calculate quantity")
return {"CANCELLED"}
prop.metadata.float_value = quantity
return {"FINISHED"}
def calculate_quantity(self, obj, context):
quantity = self.qto_calculator.calculate_quantity(obj.PsetProperties.active_pset_name, self.prop, obj)
prefix, name = self.get_blender_prefix_name(context)
quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name)
if quantity is None:
return
return round(quantity, 3)
def get_prefix_name(self, value):
if "/" in value:
return value.split("/")
return None, value
def get_blender_prefix_name(self, context):
unit_settings = context.scene.unit_settings
if unit_settings.system == "IMPERIAL":
if unit_settings.length_unit == "INCHES":
return None, "inch"
elif unit_settings.length_unit == "FEET":
return None, "foot"
elif unit_settings.system == "METRIC":
if unit_settings.length_unit == "METERS":
return None, "METRE"
return unit_settings.length_unit[0 : -len("METERS")], "METRE"
class GuessQuantity(bpy.types.Operator):
bl_idname = "bim.guess_quantity"
@@ -422,35 +425,7 @@ class GuessQuantity(bpy.types.Operator):
def guess_quantity(self, obj, context):
quantity = self.qto_calculator.guess_quantity(self.prop, [p.name for p in obj.PsetProperties.properties], obj)
if "area" in self.prop.lower():
if context.scene.BIMProperties.area_unit:
prefix, name = self.get_prefix_name(context.scene.BIMProperties.area_unit)
quantity = ifcopenshell.util.unit.convert(quantity, None, "SQUARE_METRE", prefix, name)
elif "volume" in self.prop.lower():
if context.scene.BIMProperties.volume_unit:
prefix, name = self.get_prefix_name(context.scene.BIMProperties.volume_unit)
quantity = ifcopenshell.util.unit.convert(quantity, None, "CUBIC_METRE", prefix, name)
else:
prefix, name = self.get_blender_prefix_name(context)
quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name)
return round(quantity, 3)
def get_prefix_name(self, value):
if "/" in value:
return value.split("/")
return None, value
def get_blender_prefix_name(self, context):
unit_settings = context.scene.unit_settings
if unit_settings.system == "IMPERIAL":
if unit_settings.length_unit == "INCHES":
return None, "inch"
elif unit_settings.length_unit == "FEET":
return None, "foot"
elif unit_settings.system == "METRIC":
if unit_settings.length_unit == "METERS":
return None, "METRE"
return unit_settings.length_unit[0 : -len("METERS")], "METRE"
return round(quantity, 3) if quantity is not None else None
class CopyPropertyToSelection(bpy.types.Operator, Operator):
@@ -494,7 +469,7 @@ class BIM_OT_add_property_to_edit(bpy.types.Operator):
class BIM_OT_remove_property_to_edit(bpy.types.Operator):
bl_label = "Remove property to be renamed"
bl_label = "Remove Property to Be Renamed"
bl_idname = "bim.remove_property_to_edit"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -510,7 +485,7 @@ class BIM_OT_remove_property_to_edit(bpy.types.Operator):
class BIM_OT_clear_list(bpy.types.Operator):
bl_label = "Clear list of properties"
bl_label = "Clear List of Properties"
bl_idname = "bim.clear_list"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.StringProperty()
@@ -558,7 +533,7 @@ class BIM_OT_rename_parameters(bpy.types.Operator):
class BIM_OT_add_edit_custom_property(bpy.types.Operator):
bl_label = "Add or edit a custom property"
bl_label = "Add or Edit a Custom Property"
bl_idname = "bim.add_edit_custom_property"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -615,7 +590,7 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator):
class BIM_OT_bulk_remove_psets(bpy.types.Operator):
bl_label = "Bulk remove psets from selected objects"
bl_label = "Bulk Remove Psets from Selected Objects"
bl_idname = "bim.bulk_remove_psets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Bulk remove psets from selected objects"

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