Compare commits

...

133 Commits

Author SHA1 Message Date
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
151 changed files with 6167 additions and 2448 deletions
+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
+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
- {{ pin_compatible('occt', max_pin='x.x.x') }}
- occt ==7.7.0
- libxml2
- cgal-cpp
- hdf5
@@ -44,6 +45,7 @@ requirements:
- gmp # [unix]
- mpir # [win]
- nlohmann_json
- zlib
test:
imports:
+7
View File
@@ -664,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
@@ -95,6 +95,8 @@ classes = [
operator.SelectIfcFile,
operator.ReloadSelectedIfcFile,
operator.SelectSchemaDir,
operator.FileAssociate,
operator.FileUnassociate,
operator.SelectURIAttribute,
operator.EditBlenderCollection,
operator.BIM_OT_open_webbrowser,
@@ -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"
}
}
@@ -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>
@@ -5,7 +5,7 @@ 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,#16,#17,#18,#19));
#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','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.);
@@ -24,5 +24,7 @@ DATA;
#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;
@@ -18,7 +18,7 @@ DATA;
#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));
#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.);
@@ -27,8 +27,12 @@ DATA;
#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=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#24,#25));
#24=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#25=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.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;
+50 -70
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,6 +101,53 @@ 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):
@@ -233,73 +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 = "Blender Default"
drawing_style.render_type = "DEFAULT"
bpy.ops.bim.save_drawing_style(index="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,
}
)
AuthoringData.type_thumbnails = {}
AuthoringData.type_thumbnails = {}
+35 -61
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):
@@ -178,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()
@@ -683,28 +688,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:
@@ -771,10 +779,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):
@@ -862,40 +870,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
@@ -1333,14 +1307,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]
@@ -1731,14 +1711,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 []:
@@ -60,6 +60,8 @@ class EnableEditingAttributes(bpy.types.Operator):
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
@@ -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
@@ -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)
@@ -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")
@@ -255,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():
@@ -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):
@@ -51,7 +51,7 @@ class BIM_PT_cost_schedules(Panel):
row = self.layout.row()
row.prop(self.props, "cost_schedule_predefined_types")
row.operator("bim.add_cost_schedule", icon="ADD", text="Add new")
row.operator("bim.add_cost_schedule", icon="ADD", text="Add")
for schedule in CostSchedulesData.data["schedules"]:
self.draw_cost_schedule_ui(schedule)
@@ -594,7 +594,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 +608,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):
@@ -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()
@@ -61,11 +61,13 @@ classes = (
operator.RemoveDrawingStyle,
operator.RemoveDrawingStyleAttribute,
operator.RemoveSchedule,
operator.ReloadDrawingStyles,
operator.RemoveSheet,
operator.RemoveTextLiteral,
operator.SelectAllDrawings,
operator.ResizeText,
operator.SaveDrawingStyle,
operator.SaveDrawingStylesData,
operator.SelectAssignedProduct,
operator.SelectDocIfcFile,
prop.Variable,
@@ -32,6 +32,7 @@ def refresh():
AnnotationData.is_loaded = False
DecoratorData.data = {}
DecoratorData.cut_cache = {}
DecoratorData.layerset_cache = {}
class ProductAssignmentsData:
@@ -102,6 +103,7 @@ class DrawingsData:
"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
@@ -123,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 = {}
@@ -155,6 +166,7 @@ class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
layerset_cache = {}
# used by Ifc Annotations with ObjectType = "BATTING"
@classmethod
@@ -226,18 +238,21 @@ class DecoratorData:
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)
@@ -255,7 +270,7 @@ 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
@@ -279,11 +294,15 @@ class DecoratorData:
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
File diff suppressed because it is too large Load Diff
@@ -23,7 +23,7 @@ from bpy import types
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
@@ -251,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:
@@ -264,13 +265,17 @@ 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)
gpu.state.blend_set("NONE")
@@ -360,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"""
@@ -389,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
@@ -407,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"""
@@ -486,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
@@ -496,6 +505,7 @@ 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)
@@ -41,7 +41,7 @@ import blenderbim.bim.module.drawing.sheeter as sheeter
import blenderbim.bim.module.drawing.scheduler as scheduler
import blenderbim.bim.module.drawing.helper as helper
from blenderbim.bim.module.drawing.decoration import CutDecorator
from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData
import blenderbim.bim.export_ifc
from lxml import etree
from mathutils import Vector
@@ -189,7 +189,7 @@ class CreateDrawing(bpy.types.Operator):
self.cprops = self.camera.data.BIMCameraProperties
self.drawing_name = self.file.by_id(drawing_id).Name
self.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
self.get_scale()
self.get_scale(context)
if self.cprops.update_representation(self.camera):
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
@@ -486,6 +486,14 @@ class CreateDrawing(bpy.types.Operator):
results = self.svg_buffer.get_value()
root = etree.fromstring(results)
group = root.find("{http://www.w3.org/2000/svg}g")
if not group:
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
return svg_path
self.move_projection_to_bottom(root)
self.merge_linework_and_add_metadata(root)
@@ -620,7 +628,6 @@ class CreateDrawing(bpy.types.Operator):
# Loop over the cell paths
for pi, p in enumerate(g2.getElementsByTagName("path")):
d = p.getAttribute("d")
coords = [co[1:].split(",") for co in d.split() if co[1:]]
polygon = shapely.Polygon(coords)
@@ -829,6 +836,10 @@ class CreateDrawing(bpy.types.Operator):
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
continue
classes = self.get_svg_classes(element)
classes.append("cut")
@@ -865,7 +876,11 @@ class CreateDrawing(bpy.types.Operator):
if is_closed_polygon:
el.getparent().remove(el)
merged_polygons = shapely.ops.unary_union(polygons)
try:
merged_polygons = shapely.ops.unary_union(polygons)
except:
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
merged_polygons = polygons
if type(merged_polygons) == shapely.MultiPolygon:
merged_polygons = merged_polygons.geoms
@@ -923,13 +938,49 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
def get_scale(self):
if self.camera.data.BIMCameraProperties.diagram_scale == "CUSTOM":
self.human_scale, fraction = self.camera.data.BIMCameraProperties.custom_diagram_scale.split("|")
else:
self.human_scale, fraction = self.camera.data.BIMCameraProperties.diagram_scale.split("|")
def get_scale(self, context):
camera_props = self.camera.data.BIMCameraProperties
if camera_props.diagram_scale == "CUSTOM":
if context.scene.unit_settings.system == "IMPERIAL":
if (
camera_props.custom_diagram_scale_input1[-1] == "'"
or camera_props.custom_diagram_scale_input1[-1] == '"'
):
camera_props.custom_diagram_scale_input1 = camera_props.custom_diagram_scale_input1[:-1]
if camera_props.custom_diagram_scale_input2[-1] == "'":
camera_props.custom_diagram_scale_input2 = camera_props.custom_diagram_scale_input2[:-1]
if (
camera_props.custom_diagram_scale_input2[1:] == "'-0\""
or camera_props.custom_diagram_scale_input2[1:] == "' 0\""
):
camera_props.custom_diagram_scale_input2 = camera_props.custom_diagram_scale_input2[0]
if "/" in camera_props.custom_diagram_scale_input1:
if " " in camera_props.custom_diagram_scale_input1:
n1, n2 = camera_props.custom_diagram_scale_input1.split(" ")
n2_1, n2_2 = n2.split("/")
numerator = int(n1) + (int(n2_1) / int(n2_2))
elif "-" in camera_props.custom_diagram_scale_input1:
n1, n2 = camera_props.custom_diagram_scale_input1.split("-")
n2_1, n2_2 = n2.split("/")
numerator = int(n1) + (int(n2_1) / int(n2_2))
else:
n1, n2 = camera_props.custom_diagram_scale_input1.split("/")
numerator = int(n1) / int(n2)
else:
numerator = 1
denominator = (int(camera_props.custom_diagram_scale_input2) / numerator) * 12
scale = "1" + "/" + str(denominator)
human_scale = camera_props.custom_diagram_scale_input1 + "=" + camera_props.custom_diagram_scale_input2
else:
scale = camera_props.custom_diagram_scale_input1 + "/" + camera_props.custom_diagram_scale_input2
human_scale = camera_props.custom_diagram_scale_input1 + ":" + camera_props.custom_diagram_scale_input2
if self.camera.data.BIMCameraProperties.is_nts:
camera_props.custom_diagram_scale = human_scale + "|" + scale
self.human_scale, fraction = camera_props.custom_diagram_scale.split("|")
else:
self.human_scale, fraction = camera_props.diagram_scale.split("|")
if camera_props.is_nts:
self.human_scale = "NTS"
numerator, denominator = fraction.split("/")
@@ -1010,6 +1061,8 @@ class OpenSheet(bpy.types.Operator, Operator):
def _execute(self, context):
self.props = context.scene.DocProperties
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.update_sheet_drawing_sizes(sheet)
core.open_sheet(tool.Drawing, sheet=sheet)
@@ -1026,7 +1079,7 @@ class AddDrawingToSheet(bpy.types.Operator, Operator):
def _execute(self, context):
props = context.scene.DocProperties
active_drawing = props.drawings[props.active_drawing_index]
active_sheet = props.sheets[props.active_sheet_index]
active_sheet = tool.Drawing.get_active_sheet(context)
drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id)
drawing_reference = tool.Drawing.get_drawing_document(drawing)
@@ -1285,9 +1338,12 @@ class ActivateDrawing(bpy.types.Operator):
def execute(self, context):
drawing = tool.Ifc.get().by_id(self.drawing)
dprops = bpy.context.scene.DocProperties
core.activate_drawing_view(tool.Ifc, tool.Drawing, drawing=drawing)
bpy.context.scene.DocProperties.active_drawing_id = self.drawing
dprops.active_drawing_id = self.drawing
dprops.drawing_styles.clear()
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
@@ -1357,18 +1413,81 @@ class RemoveDrawing(bpy.types.Operator, Operator):
core.remove_drawing(tool.Ifc, tool.Drawing, drawing=drawing)
class AddDrawingStyle(bpy.types.Operator):
class ReloadDrawingStyles(bpy.types.Operator):
bl_idname = "bim.reload_drawing_styles"
bl_label = "Reload Drawing Styles"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
camera_props = context.active_object.data.BIMCameraProperties
# added this part as a temporary fallback
# TODO: should remove it a bit later when projects get more accommodated
# with saving shadingstyles to ifc and separate json file
if "ShadingStyles" not in drawing_pset_data:
ifc_file = tool.Ifc.get()
pset = ifc_file.by_id(drawing_pset_data["id"])
edit_properties = {
"ShadingStyles": (
shading_styles_path := tool.Drawing.get_default_drawing_resource_path("ShadingStyles")
),
"CurrentShadingStyle": tool.Drawing.get_default_shading_style(),
}
ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties=edit_properties)
tool.Drawing.setup_shading_styles_path(shading_styles_path)
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
if "ShadingStyles" not in drawing_pset_data:
self.report({"ERROR"}, "Could not find shading styles path in EPset_Drawing.ShadingStyles.")
return {"CANCELLED"}
rel_path = drawing_pset_data["ShadingStyles"]
current_style = drawing_pset_data.get("CurrentShadingStyle", None)
json_path = Path(tool.Ifc.resolve_uri(rel_path))
if not json_path.exists():
self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path))
return {"CANCELLED"}
with open(json_path, "r") as fi:
shading_styles_json = json.load(fi)
drawing_styles = context.scene.DocProperties.drawing_styles
drawing_styles.clear()
styles = [style for style in shading_styles_json]
for style_name in styles:
style_data = shading_styles_json[style_name]
drawing_style = drawing_styles.add()
drawing_style["name"] = style_name # setting as attribute to avoid triggering setter
drawing_style.render_type = style_data["render_type"]
drawing_style.raster_style = json.dumps(style_data["raster_style"])
if current_style is not None:
camera_props.active_drawing_style_index = styles.index(current_style)
return {"FINISHED"}
class AddDrawingStyle(bpy.types.Operator, Operator):
bl_idname = "bim.add_drawing_style"
bl_label = "Add Drawing Style"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
new = context.scene.DocProperties.drawing_styles.add()
new.name = "New Drawing Style"
drawing_styles = context.scene.DocProperties.drawing_styles
new = drawing_styles.add()
# drawing style is saved to ifc on rename
new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles)
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = len(drawing_styles) - 1
return {"FINISHED"}
class RemoveDrawingStyle(bpy.types.Operator):
class RemoveDrawingStyle(bpy.types.Operator, Operator):
bl_idname = "bim.remove_drawing_style"
bl_label = "Remove Drawing Style"
bl_options = {"REGISTER", "UNDO"}
@@ -1376,13 +1495,17 @@ class RemoveDrawingStyle(bpy.types.Operator):
def execute(self, context):
context.scene.DocProperties.drawing_styles.remove(self.index)
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = max(self.index - 1, 0)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
class SaveDrawingStyle(bpy.types.Operator):
class SaveDrawingStyle(bpy.types.Operator, Operator):
bl_idname = "bim.save_drawing_style"
bl_label = "Save Drawing Style"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save current render setting to currently selected drawing style. Also resaves styles data to IFC"
index: bpy.props.StringProperty()
# TODO: check undo redo
@@ -1398,11 +1521,14 @@ class SaveDrawingStyle(bpy.types.Operator):
except TypeError:
pass
style[prop.value] = value
if self.index:
index = int(self.index)
else:
index = context.active_object.data.BIMCameraProperties.active_drawing_style_index
scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
def get_view_3d(self, context):
@@ -1415,19 +1541,83 @@ class SaveDrawingStyle(bpy.types.Operator):
return space
class ActivateDrawingStyle(bpy.types.Operator):
class SaveDrawingStylesData(bpy.types.Operator, Operator):
bl_idname = "bim.save_drawing_styles_data"
bl_label = "Save Drawing Styles Data"
bl_options = {"REGISTER", "UNDO"}
skip_updating_current_style: bpy.props.BoolProperty(default=False)
rename_style: bpy.props.BoolProperty(default=False)
rename_style_from: bpy.props.StringProperty(default="")
rename_style_to: bpy.props.StringProperty(default="")
def execute(self, context):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
drawing_styles = context.scene.DocProperties.drawing_styles
rel_path = drawing_pset_data["ShadingStyles"]
current_style = drawing_pset_data.get("CurrentShadingStyle", None)
json_path = Path(tool.Ifc.resolve_uri(rel_path))
if not json_path.exists():
self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path))
return {"CANCELLED"}
styles_data = {}
for style in drawing_styles:
style_data = {"render_type": style.render_type, "raster_style": json.loads(style.raster_style)}
styles_data[style.name] = style_data
with open(json_path, "w") as fo:
json.dump(styles_data, fo, indent=4)
# TODO: currently it doesn't update current style for the other drawings
# handling case when current style is not present in styles saved in ifc
if not self.skip_updating_current_style and current_style not in styles_data and current_style is not None:
# style was renamed
if self.rename_style and current_style == self.rename_style_from:
new_style_name = self.rename_style_to
# style was removed
else:
new_style_name = None
ifc_file = tool.Ifc.get()
drawing = ifc_file.by_id(context.scene.DocProperties.active_drawing_id)
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name}
)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class ActivateDrawingStyle(bpy.types.Operator, Operator):
bl_idname = "bim.activate_drawing_style"
bl_label = "Activate Drawing Style"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
scene = context.scene
if scene.camera.data.BIMCameraProperties.active_drawing_style_index < len(scene.DocProperties.drawing_styles):
self.drawing_style = scene.DocProperties.drawing_styles[
scene.camera.data.BIMCameraProperties.active_drawing_style_index
]
self.set_raster_style(context)
self.set_query(context)
ifc_file = tool.Ifc.get()
active_drawing_style_index = scene.camera.data.BIMCameraProperties.active_drawing_style_index
if active_drawing_style_index >= len(scene.DocProperties.drawing_styles):
return
self.drawing_style = scene.DocProperties.drawing_styles[active_drawing_style_index]
self.set_raster_style(context)
self.set_query(context)
drawing = ifc_file.by_id(scene.DocProperties.active_drawing_id)
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name}
)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
def set_raster_style(self, context):
@@ -1512,7 +1702,10 @@ class AddSchedule(bpy.types.Operator, Operator):
ifc_path = tool.Ifc.get_path()
if os.path.isfile(ifc_path):
ifc_path = os.path.dirname(ifc_path)
filepath = os.path.relpath(filepath, ifc_path)
# taking into account different drives on windows
if Path(filepath).drive == Path(ifc_path).drive:
filepath = os.path.relpath(filepath, ifc_path)
core.add_schedule(
tool.Ifc,
tool.Drawing,
@@ -1566,7 +1759,7 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
def _execute(self, context):
props = context.scene.DocProperties
active_schedule = props.schedules[props.active_schedule_index]
active_sheet = props.sheets[props.active_sheet_index]
active_sheet = tool.Drawing.get_active_sheet(context)
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
schedule_location = tool.Drawing.get_path_with_ext(schedule.DocumentReferences[0].Location, "svg")
@@ -1607,6 +1800,7 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
tool.Drawing.import_sheets()
# TODO: dead code - drawing style attributes are never used?
class AddDrawingStyleAttribute(bpy.types.Operator):
bl_idname = "bim.add_drawing_style_attribute"
bl_label = "Add Drawing Style Attribute"
@@ -1618,6 +1812,7 @@ class AddDrawingStyleAttribute(bpy.types.Operator):
return {"FINISHED"}
# TODO: dead code - drawing style attributes are never used?
class RemoveDrawingStyleAttribute(bpy.types.Operator):
bl_idname = "bim.remove_drawing_style_attribute"
bl_label = "Remove Drawing Style Attribute"
@@ -156,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)
@@ -201,6 +216,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)
@@ -236,8 +254,8 @@ class Sheet(PropertyGroup):
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", ""),
@@ -312,9 +330,6 @@ class DocProperties(PropertyGroup):
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(
@@ -327,6 +342,10 @@ class DocProperties(PropertyGroup):
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):
@@ -339,6 +358,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")
@@ -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,9 +16,23 @@
# 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 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 = """
@@ -29,12 +43,6 @@ BASE_DEF_GLSL = """
#define lineSmooth true
"""
# 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_LIB_GLSL = """
// TODO: redefine as macor instead
uniform vec2 winsize;
@@ -160,12 +168,11 @@ void do_vertex_util(vec4 pos, vec2 ofs)
// geometry utils
void triangle_head(in vec4 side, in vec4 dir, in float length, in float width, in float radius, out vec4 head[5]) {
vec4 nose = side * length;
vec4 ear = dir * width;
// TODO: radius is unnecessary?
head[0] = side * -radius;
head[1] = nose * -.5;
head[2] = vec4(0) + ear;
head[3] = nose * .5;
head[1] = side * length * -.5;
head[2] = dir * width;
head[3] = side * length * .5;
head[4] = side * radius;
}
@@ -189,6 +196,22 @@ void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) {
"""
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:
"""Wrapper for GPUShader
To use for viewport decorations with geometry generated on GPU side.
@@ -265,48 +288,49 @@ class BaseShader:
"""
def __init__(self):
# NB: libcode arg doesn't work
# TODO: rename to .shader
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):
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
uniform_floats = {
"viewMatrix": region3d.perspective_matrix,
"winsize": (region.width, region.height),
"ModelViewProjectionMatrix": region3d.perspective_matrix,
# POLYLINE_UNIFORM_COLOR specific uniforms
"viewportSize": (region.width, region.height),
"lineWidth": 2.5,
}
for name, value in uniform_floats.items():
try:
self.prog.uniform_float(name, value)
# TODO: shouldn't just try'n'catch them
# because they may indicate errors in code
except ValueError: # unused uniform
pass
shader.uniform_float(name, value)
# TODO: add smoothing if this shades is going to be used
# 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"""
@@ -429,37 +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(triangle_strip, max_vertices=MAX_POINTS) out;
output_verts = []
output_edges = []
out_kwargs = {
"output_verts": output_verts,
"output_edges": output_edges,
}
void main() {
// default setup for macro to work
vec2 EDGE_DIR;
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)
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
do_edge_verts(p0, p1);
EndPrimitive();
vec4 bx = ModelViewProjectionMatrix[0] * CROSS_SIZE;
vec4 by = ModelViewProjectionMatrix[1] * CROSS_SIZE;
do_edge_verts(p0 - bx, p0 + bx);
EndPrimitive();
do_edge_verts(p0 - by, p0 + by);
EndPrimitive();
do_edge_verts(p1 - bx, p1 + bx);
EndPrimitive();
do_edge_verts(p1 - by, p1 + by);
EndPrimitive();
}
"""
return output_verts, output_edges
@@ -99,6 +99,7 @@ class SheetBuilder:
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"
@@ -108,6 +109,7 @@ class SheetBuilder:
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"
@@ -120,6 +122,36 @@ class SheetBuilder:
self.add_view_title(30, view_height + 35, 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"))
for image in drawing_view.findall(f"{SVG}image"):
if image.attrib["data-type"] == "view-title":
image.attrib["y"] = str(view_height + 35)
else:
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")
@@ -234,7 +234,7 @@ class SvgWriter:
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
rl *= unit_scale
rl = "{:.3f}m".format(rl)
text_style = self.get_box_alignment_parameters("bottom-left")
text_style = SvgWriter.get_box_alignment_parameters("bottom-left")
self.svg.add(self.svg.text(f"RL +{rl}", insert=tuple(text_position), class_="SECTIONLEVEL", **text_style))
if tag:
self.svg.add(self.svg.text(tag, insert=(text_position[0], text_position[1] - 5), **text_style))
@@ -263,7 +263,7 @@ class SvgWriter:
"UP",
insert=tuple(text_position),
class_="STAIR",
**self.get_box_alignment_parameters("center"),
**SvgWriter.get_box_alignment_parameters("center"),
)
)
@@ -287,7 +287,7 @@ class SvgWriter:
)
line["stroke-dasharray"] = "12.5, 3, 3, 3"
axis_tag = tool.Ifc.get_entity(obj).Name
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
axis_tag,
@@ -557,7 +557,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 = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
reference_id,
@@ -590,7 +590,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 = self.get_box_alignment_parameters("center")
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
@@ -617,7 +617,8 @@ class SvgWriter:
return (reference_id, sheet_id)
return ("-", "-")
def get_box_alignment_parameters(self, box_alignment):
@staticmethod
def get_box_alignment_parameters(box_alignment):
"""Convenience method to get svg parameters for text alignment
in a readable way.
@@ -664,6 +665,7 @@ 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))
def get_basis_vector(matrix, i=0):
"""returns basis vector for i in world space, unaffected by object scale"""
@@ -673,13 +675,18 @@ class SvgWriter:
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))))
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
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()
@@ -687,7 +694,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):
@@ -697,7 +704,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))
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
@@ -707,7 +714,7 @@ class SvgWriter:
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
attribs = {
"transform": transform,
"transform": text_transform,
"style": "font-size: 0;",
}
@@ -715,7 +722,7 @@ class SvgWriter:
text_tag = self.svg.text(
"",
**(attribs | {"filter": "url(#fill-background)"}) if add_fill_bg else attribs,
**self.get_box_alignment_parameters(text_literal.BoxAlignment),
**SvgWriter.get_box_alignment_parameters(text_literal.BoxAlignment),
)
self.svg.add(text_tag)
@@ -723,7 +730,9 @@ class SvgWriter:
for line_number, text_line in enumerate(text_lines):
# position has to be inserted at tspan to avoid x offset between tspans
tspan = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
# 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)
@@ -732,7 +741,6 @@ class SvgWriter:
add_text_tag(True)
add_text_tag(False)
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -790,7 +798,7 @@ class SvgWriter:
rl = "{:.3f}m".format(rl)
box_alignment = "bottom-left" if projected_points[0].x <= projected_points[-1].x else "bottom-right"
text_style = self.get_box_alignment_parameters(box_alignment)
text_style = SvgWriter.get_box_alignment_parameters(box_alignment)
self.svg.add(
self.svg.text(
"RL +{}".format(rl),
@@ -912,7 +920,7 @@ class SvgWriter:
text_offset = (text_position - center_position).xy.normalized() * 5
text_position += text_offset
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
angle_text = abs(round(math.degrees(angle), 3))
if is_reflex:
angle_text = 360 - angle_text
@@ -973,7 +981,7 @@ class SvgWriter:
)
text_position += text_offset
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
tag = element.Description or f"R{radius}"
@@ -1046,7 +1054,7 @@ class SvgWriter:
)
text_position += text_offset
text_style = self.get_box_alignment_parameters("center")
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):
@@ -1071,6 +1079,8 @@ class SvgWriter:
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):
@@ -1092,6 +1102,8 @@ class SvgWriter:
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):
@@ -1115,6 +1127,8 @@ class SvgWriter:
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)
@@ -1135,27 +1149,86 @@ class SvgWriter:
# if annotation can't fit offset text to the right of marker
text_position = mid if sheet_dimension > 5 else (end + (3 * vector.normalized()))
rotation = math.degrees(vector.angle_signed(Vector((1, 0))))
angle = math.degrees(vector.angle_signed(Vector((1, 0))))
line = self.svg.line(start=start, end=end, class_=" ".join(classes))
self.svg.add(line)
def create_text_tag(text, text_position, box_alignment):
text_kwargs = {"transform": "rotate({} {} {})".format(rotation, text_position.x, text_position.y)}
if not show_description_only:
text = f"{text_prefix}{str(dimension)}{text_suffix}"
text_tag = self.create_text_tag(
text,
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_="DIMENSION",
**(text_kwargs | self.get_box_alignment_parameters(box_alignment)),
class_=class_str,
**(text_kwargs | SvgWriter.get_box_alignment_parameters(box_alignment)),
)
if not show_description_only:
self.svg.add(create_text_tag(str(dimension), text_position + perpendicular, "bottom-middle"))
if dimension_text:
self.svg.add(create_text_tag(dimension_text, text_position - perpendicular, "top-middle"))
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;",
}
elif show_description_only and dimension_text:
self.svg.add(create_text_tag(dimension_text, text_position + perpendicular, "bottom-middle"))
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?
@@ -84,8 +84,14 @@ 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")
@@ -111,38 +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, "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):
@@ -179,8 +179,7 @@ class AnnotationToolUI:
if context.active_object and context.selected_objects:
cls.draw_edit_object_interface(context)
elif not context.selected_objects:
cls.draw_create_object_interface()
cls.draw_create_object_interface()
@classmethod
def draw_create_object_interface(cls):
@@ -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
@@ -527,6 +527,8 @@ class OverrideDuplicateMove(bpy.types.Operator):
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)
blenderbim.bim.handler.purge_module_data()
@@ -720,6 +722,11 @@ class OverrideModeSetEdit(bpy.types.Operator):
if context.active_object:
context.active_object.select_set(True)
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
@@ -834,6 +841,11 @@ class OverrideModeSetObject(bpy.types.Operator):
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 = []
@@ -21,6 +21,8 @@ from . import ui, prop, operator
classes = (
operator.AddFileToRepo,
operator.AddTag,
operator.CloneRepo,
operator.CommitChanges,
operator.CreateRepo,
operator.DiscardUncommitted,
@@ -29,6 +31,7 @@ classes = (
operator.Merge,
operator.RefreshGit,
operator.SwitchRevision,
prop.IfcGitTag,
prop.IfcGitListItem,
prop.IfcGitProperties,
ui.IFCGIT_PT_panel,
@@ -57,6 +57,33 @@ class AddFileToRepo(bpy.types.Operator):
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"""
@@ -88,7 +115,7 @@ class CommitChanges(bpy.types.Operator):
repo
and repo.head.is_detached
and (
not tool.is_valid_ref_format(props.new_branch_name)
not tool.IfcGit.is_valid_ref_format(props.new_branch_name)
or props.new_branch_name in [branch.name for branch in repo.branches]
)
):
@@ -107,6 +134,33 @@ class CommitChanges(bpy.types.Operator):
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"""
@@ -32,6 +32,19 @@ def update_revlist(self, context):
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."""
@@ -45,6 +58,19 @@ class IfcGitListItem(PropertyGroup):
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):
@@ -61,6 +87,27 @@ class IfcGitProperties(PropertyGroup):
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)
ifcgit_filter: EnumProperty(
items=[
@@ -54,6 +54,16 @@ class IFCGIT_PT_panel(bpy.types.Panel):
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"]
@@ -119,7 +129,6 @@ class IFCGIT_PT_panel(bpy.types.Panel):
return
item = props.ifcgit_commits[props.commit_index]
commit = IfcGitData.data["commit"]
if not item.relevant:
row = layout.row()
row.label(text="Revision unrelated to current IFC project", icon="ERROR")
@@ -127,11 +136,30 @@ class IFCGIT_PT_panel(bpy.types.Panel):
box = layout.box()
column = box.column(align=True)
row = column.row()
row.label(text=commit.hexsha)
row.label(text=item.hexsha)
row = column.row()
row.label(text=commit.author.name + " <" + commit.author.email + ">")
row.label(text=item.author_name + " <" + item.author_email + ">")
row = column.row()
row.label(text=commit.message)
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")
row = layout.row()
row.prop(props, "new_tag_name")
row = layout.row()
row.prop(props, "new_tag_message")
row = layout.row()
row.operator("ifcgit.add_tag", icon="GREASEPENCIL")
class COMMIT_UL_List(bpy.types.UIList):
@@ -96,6 +96,7 @@ classes = (
slab.ResetVertex,
slab.SetArcIndex,
space.GenerateSpace,
space.GenerateSpacesFromWalls,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -125,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,
@@ -59,9 +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):
@@ -69,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])
@@ -161,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)
@@ -177,7 +201,7 @@ class AuthoringData:
def active_representation_type(cls):
if bpy.context.active_object:
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
if representation:
if representation and representation.is_a("IfcShapeRepresentation"):
return representation.RepresentationType
@classmethod
@@ -82,14 +82,14 @@ 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]
faces_color = transparent_color(self.model_props.decorator_color_special)
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.model_props = context.scene.BIMModelProperties
selected_elements_color = self.model_props.decorator_color_selected
unselected_elements_color = self.model_props.decorator_color_unselected
special_elements_color = self.model_props.decorator_color_special
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
@@ -214,8 +214,8 @@ class ProfileDecorator:
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, self.model_props.decorator_color_special)
self.draw_batch("POINTS", selected_vertices, self.model_props.decorator_color_selected)
self.draw_batch("POINTS", special_vertices, special_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
# Draw arcs
arc_centroids = []
@@ -657,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)
@@ -682,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)
@@ -694,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"}
@@ -793,10 +792,10 @@ class DecorationsHandler:
batch.draw(shader)
def __call__(self, context):
self.model_props = context.scene.BIMModelProperties
selected_elements_color = self.model_props.decorator_color_selected
unselected_elements_color = self.model_props.decorator_color_unselected
special_elements_color = self.model_props.decorator_color_special
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]
@@ -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
@@ -54,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)
@@ -37,6 +37,12 @@ def get_type_class(self, context):
return AuthoringData.data["type_class"]
def get_boundary_class(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["boundary_class"]
def get_relating_type_id(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
@@ -120,7 +126,7 @@ class BIMModelProperties(PropertyGroup):
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=(
@@ -141,33 +147,7 @@ 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")
decorator_color_selected: bpy.props.FloatVectorProperty(
name="Selected Elements Color",
subtype="COLOR",
default=(0.545, 0.863, 0, 1), # green
min=0.0,
max=1.0,
size=4,
description="Color of selected verts/edges (used in profile editing mode)",
)
decorator_color_unselected: bpy.props.FloatVectorProperty(
name="Not Selected Elements Color",
subtype="COLOR",
default=(1, 1, 1, 1), # green
min=0.0,
max=1.0,
size=4,
description="Color of not selected verts/edges (used in profile editing mode)",
)
decorator_color_special: bpy.props.FloatVectorProperty(
name="Special Elements Color",
subtype="COLOR",
default=(0.157, 0.565, 1, 1), # blue
min=0.0,
max=1.0,
size=4,
description="Color of special selected verts/edges (openings, preview verts/edges in roof editing, verts with arcs/circles in profile editing)",
)
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
class BIMArrayProperties(PropertyGroup):
@@ -499,12 +479,12 @@ class BIMRailingProperties(PropertyGroup):
("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""),
)
cap_types = (
("none", "none", ""),
("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", ""),
("to_wall", "to_wall", ""),
("to_floor", "to_floor", ""),
("to_end_post", "to_end_post", ""),
("to_end_post_and_floor", "to_end_post_and_floor", ""),
("NONE", "NONE", ""),
)
railing_added_previously: bpy.props.BoolProperty(default=False)
@@ -448,18 +448,30 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
if not props.roof_added_previously:
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)
@@ -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):
@@ -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(),
@@ -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
@@ -132,9 +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"
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):
@@ -240,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):
@@ -249,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)
@@ -436,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()
@@ -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")]}),
@@ -180,6 +182,7 @@ class BimToolUI:
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")
@@ -267,6 +270,12 @@ class BimToolUI:
else:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_B")
row.prop(cls.props, "boundary_class", text="")
row.operator("bim.add_boundary", text="Add Boundary")
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", "")
@@ -274,11 +283,9 @@ class BimToolUI:
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", "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"
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):
@@ -571,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()
@@ -581,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
@@ -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")
@@ -49,9 +49,12 @@ classes = (
operator.DisableEditingResourceCostValue,
operator.CalculateResourceWork,
operator.ImportResources,
operator.EditProductivityData,
prop.Resource,
prop.BIMResourceProperties,
prop.BIMResourceTreeProperties,
prop.ISODuration,
prop.BIMResourceProductivity,
ui.BIM_PT_resources,
ui.BIM_UL_resources,
)
@@ -60,8 +63,10 @@ classes = (
def register():
bpy.types.Scene.BIMResourceProperties = bpy.props.PointerProperty(type=prop.BIMResourceProperties)
bpy.types.Scene.BIMResourceTreeProperties = bpy.props.PointerProperty(type=prop.BIMResourceTreeProperties)
bpy.types.Scene.BIMResourceProductivity = bpy.props.PointerProperty(type=prop.BIMResourceProductivity)
def unregister():
del bpy.types.Scene.BIMResourceProperties
del bpy.types.Scene.BIMResourceTreeProperties
del bpy.types.Scene.BIMResourceProductivity
@@ -52,9 +52,25 @@ class ResourceData:
if resource.BaseQuantity:
base_quantity = resource.BaseQuantity.get_info()
del base_quantity["Unit"]
results[resource.id()] = {"type": resource.is_a(), "BaseQuantity": base_quantity}
results[resource.id()] = {
"type": resource.is_a(),
"BaseQuantity": base_quantity,
}
if resource.is_a() in ["IfcLaborResource", "IfcConstructionEquipmentResource"]:
results[resource.id()]["Productivity"] = {}
productivity = cls.get_productivity(resource)
if productivity:
results[resource.id()]["Productivity"] = {
"QuantityProduced": ifcopenshell.util.resource.get_quantity_produced(productivity),
"TimeConsumed": ifcopenshell.util.resource.get_unit_consumed(productivity),
"QuantityProducedName": ifcopenshell.util.resource.get_quantity_produced_name(productivity),
}
return results
@classmethod
def get_productivity(cls, resource):
return ifcopenshell.util.resource.get_productivity(resource, should_inherit=False)
@classmethod
def cost_values(cls):
results = []
@@ -346,3 +346,13 @@ class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
core.import_resources(tool.Resource, file_path=self.filepath)
class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_productivity_data"
bl_description = "Apply"
bl_label = "Edit Productivity"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.edit_productivity_pset(tool.Ifc, tool.Resource)
@@ -18,7 +18,9 @@
import bpy
import ifcopenshell.api
import ifcopenshell.util.resource
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
import blenderbim.bim.module.pset.data
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
@@ -44,13 +46,12 @@ def purge():
def updateResourceName(self, context):
props = context.scene.BIMResourceProperties
if not props.is_resource_update_enabled or self.name == "Unnamed":
if not props.is_resource_update_enabled:
return
self.file = IfcStore.get_file()
ifcopenshell.api.run(
tool.Ifc.run(
"resource.edit_resource",
self.file,
**{"resource": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
resource=tool.Ifc.get().by_id(self.ifc_definition_id),
attributes={"Name": self.name},
)
if props.active_resource_id == self.ifc_definition_id:
attribute = props.resource_attributes.get("Name")
@@ -71,12 +72,42 @@ def get_quantity_types(self, context):
def update_active_resource_index(self, context):
blenderbim.bim.module.pset.data.refresh()
if self.should_show_productivity:
tool.Resource.load_productivity_data()
def updateResourceUsage(self, context):
props = context.scene.BIMResourceProperties
if not props.is_resource_update_enabled:
return
if self.schedule_usage == "":
return
resource = tool.Ifc.get().by_id(self.ifc_definition_id)
if not resource.Usage:
tool.Ifc.run(
"resource.add_resource_time",
resource=resource,
)
resource.Usage.ScheduleUsage = self.schedule_usage
blenderbim.bim.module.pset.data.refresh()
tool.Resource.load_resource_properties()
class ISODuration(PropertyGroup):
name: StringProperty(name="Name")
years: IntProperty(name="Years", default=0)
months: IntProperty(name="Months", default=0)
days: IntProperty(name="Days", default=0)
hours: IntProperty(name="Hours", default=0)
minutes: IntProperty(name="Minutes", default=0)
seconds: IntProperty(name="Seconds", default=0)
class Resource(PropertyGroup):
name: StringProperty(name="Name", update=updateResourceName)
ifc_definition_id: IntProperty(name="IFC Definition ID")
schedule_usage: FloatProperty(name="Schedule Usage")
schedule_usage: FloatProperty(name="Schedule Usage", update=updateResourceUsage)
has_children: BoolProperty(name="Has Children")
is_expanded: BoolProperty(name="Is Expanded")
level_index: IntProperty(name="Level Index")
@@ -113,3 +144,11 @@ class BIMResourceProperties(PropertyGroup):
quantity_types: EnumProperty(items=get_quantity_types, name="Quantity Types")
is_editing_quantity: BoolProperty(name="Is Editing Quantity")
quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute)
should_show_productivity: BoolProperty(name="Edit Productivity", update=update_active_resource_index)
class BIMResourceProductivity(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
quantity_consumed: CollectionProperty(name="Duration", type=ISODuration)
quantity_produced: FloatProperty(name="Quantity Produced")
quantity_produced_name: StringProperty(name="Quantity Produced Name")
@@ -65,7 +65,11 @@ class BIM_PT_resources(Panel):
self.props,
"active_resource_index",
)
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.props, "should_show_productivity", icon="RECOVER_LAST")
if self.props.should_show_productivity:
self.draw_productivity_ui(context)
if self.props.active_resource_id and self.props.editing_resource_type == "ATTRIBUTES":
self.draw_editable_resource_attributes_ui()
elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY":
@@ -75,6 +79,45 @@ class BIM_PT_resources(Panel):
elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE":
self.draw_editable_resource_time_attributes_ui()
def draw_productivity_ui(self, context):
total_resources = len(self.tprops.resources)
if not total_resources or self.props.active_resource_index >= total_resources:
return
ifc_definition_id = self.tprops.resources[self.props.active_resource_index].ifc_definition_id
resource = ResourceData.data["resources"][ifc_definition_id]
if not resource["type"] in ["IfcConstructionEquipmentResource", "IfcLaborResource"]:
row = self.layout.row(align=True)
row.label(text="Resource type cannot have productivity data", icon="ERROR")
return
self.productivity_props = context.scene.BIMResourceProductivity
if resource["Productivity"]:
produtivitiy_rate_message = "Current Rate: {}/{}".format(
resource["Productivity"]["QuantityProduced"], resource["Productivity"]["TimeConsumed"]
)
row = self.layout.row()
row.alignment = "LEFT"
row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA")
else:
row = self.layout.row(align=True)
row.alignment = "LEFT"
produtivitiy_rate_message = "No productivity data found"
row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.productivity_props, "quantity_produced", text="Quantity Produced")
row.prop(self.productivity_props, "quantity_produced_name", text="Quantity Name")
row = self.layout.row()
row.alignment = "RIGHT"
self.draw_duration_property(self.productivity_props.quantity_consumed, row)
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.edit_productivity_data", text="Apply", icon="CHECKMARK")
def draw_resource_operators(self):
row = self.layout.row(align=True)
op = row.operator("bim.add_resource", text="Add SubContract", icon="TEXT")
@@ -111,8 +154,9 @@ class BIM_PT_resources(Panel):
if not self.props.active_resource_id:
if resource["type"] in ["IfcLaborResource", "IfcConstructionEquipmentResource"]:
op = row.operator("bim.calculate_resource_work", text="", icon="TEMP")
op.resource = ifc_definition_id
if resource["Productivity"]:
op = row.operator("bim.calculate_resource_work", text="", icon="TEMP")
op.resource = ifc_definition_id
row.operator("bim.enable_editing_resource_time", text="", icon="TIME").resource = ifc_definition_id
op = row.operator("bim.enable_editing_resource_base_quantity", text="", icon="PROPERTIES")
op.resource = ifc_definition_id
@@ -204,10 +248,20 @@ class BIM_PT_resources(Panel):
op.parent = parent_id
op.cost_value = cost_value_id
def draw_duration_property(self, duration_props, layout):
for duration_prop in duration_props:
if duration_prop.name == "BaseQuantityConsumed":
layout.label(text=duration_prop.name)
layout.prop(duration_prop, "years", text="Y")
layout.prop(duration_prop, "months", text="M")
layout.prop(duration_prop, "days", text="D")
layout.prop(duration_prop, "hours", text="H")
layout.prop(duration_prop, "minutes", text="Min")
layout.prop(duration_prop, "seconds", text="S")
class BIM_UL_resources(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
resource = ResourceData.data["resources"][item.ifc_definition_id]
icon_map = {
"IfcSubContractResource": "TEXT",
"IfcCrewResource": "COMMUNITY",
@@ -217,6 +271,7 @@ class BIM_UL_resources(UIList):
"IfcConstructionProductResource": "PACKAGE",
}
if item:
resource = ResourceData.data["resources"][item.ifc_definition_id]
props = context.scene.BIMResourceProperties
row = layout.row(align=True)
for i in range(0, item.level_index):
@@ -97,8 +97,11 @@ classes = (
operator.RemoveWorkPlan,
operator.RemoveWorkSchedule,
operator.RemoveWorkTime,
operator.ReorderTask,
operator.SelectTaskRelatedProducts,
operator.SelectTaskRelatedInputs,
operator.SelectWorkScheduleProducts,
operator.SelectUnassignedWorkScheduleProducts,
operator.SetTaskSortColumn,
operator.SetupDefaultTaskColumns,
operator.UnassignLagTime,
@@ -114,6 +117,7 @@ classes = (
operator.DisableEditingTaskAnimationColors,
operator.LoadProductTasks,
operator.HighlightTask,
operator.CreateBaseline,
prop.WorkPlan,
prop.BIMWorkPlanProperties,
prop.Task,
@@ -19,12 +19,15 @@
import bpy
import blenderbim.tool as tool
import ifcopenshell
from ifcopenshell.util.doc import get_predefined_type_doc
import ifcopenshell.util.date as dateutil
def refresh():
SequenceData.is_loaded = False
WorkPlansData.is_loaded = False
TaskICOMData.is_loaded = False
WorkScheduleData.is_loaded = False
class SequenceData:
@@ -34,6 +37,7 @@ class SequenceData:
@classmethod
def load(cls):
cls.data = {
"predefined_types": cls.get_work_schedule_types(),
"has_work_plans": cls.has_work_plans(),
"has_work_schedules": cls.has_work_schedules(),
"has_work_calendars": cls.has_work_calendars(),
@@ -221,8 +225,66 @@ class SequenceData:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl:
if rel.RelatingControl.is_a("IfcWorkCalendar"):
data["HasAssignmentsWorkCalendar"].append(rel.RelatingControl.id())
data["NestingIndex"] = None
for rel in task.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(task)
cls.data["tasks"][task.id()] = data
@classmethod
def get_work_schedule_types(cls):
results = []
declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule")
version = tool.Ifc.get_schema()
for attribute in declaration.attributes():
if attribute.name() == "PredefinedType":
results.extend(
[
(e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e))
for e in attribute.type_of_attribute().declared_type().enumeration_items()
]
)
break
return results
class WorkScheduleData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {
"can_have_baselines": cls.can_have_baselines(),
"active_work_schedule_baselines": cls.active_work_schedule_baselines(),
}
cls.is_loaded = True
@classmethod
def can_have_baselines(cls):
if not bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
return False
return (
tool.Ifc.get().by_id(bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id).PredefinedType
== "PLANNED"
)
@classmethod
def active_work_schedule_baselines(cls):
results = []
if not bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
return []
for rel in tool.Ifc.get().by_id(bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id).Declares:
for work_schedule in rel.RelatedObjects:
if work_schedule.PredefinedType == "BASELINE":
results.append(
{
"id": work_schedule.id(),
"name": work_schedule.Name or "Unnamed",
"date": str(dateutil.ifc2datetime(work_schedule.CreationDate)),
}
)
return results
class WorkPlansData:
data = {}
@@ -0,0 +1,112 @@
let g;
function create_gantt_chart(json_data) {
g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'week');
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: true, // 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'}, "resourceUsage": {title: 'Resource Usage'}},
vUseToolTip: true, // Disable tooltips.
vTooltipTemplate: generateTooltip,
vTotalHeight: 900,
});
JSGantt.parseJSONString(json_data, g);
g.Draw();
}
function set_language(e){
lang = e && e.target ? e.target.value : "en";
g.setOptions({
vLang: lang,
});
g.Draw();
}
function generateTooltip(task) {
var dataObject = task.getDataObject();
var numberResources = dataObject.resourceUsage ? dataObject.resourceUsage : "NULL"; ;
return `
<dl>
<dt>Name:</dt><dd>{{pName}}</dd>
<dt>Start:</dt><dd>{{pStart}}</dd>
<dt>End:</dt><dd>{{pEnd}}</dd>
<dt>Duration:</dt><dd>${dataObject.ifcduration}</dd>
<dt>Number of Resources:</dt><dd>${numberResources}</dd>
<dt>Resources:</dt><dd>{{pRes}}</dd>
</dl>
`;
}
function draw_table(json_object){
let table = document.createElement("table");
table.classList.add("table-description");
for (let key in json_object){
let row = document.createElement("tr");
let key_cell = document.createElement("td");
let key_text = document.createTextNode(key);
key_cell.appendChild(key_text);
row.appendChild(key_cell);
let value_cell = document.createElement("td");
let value_text = document.createTextNode(json_object[key]);
value_cell.appendChild(value_text);
row.appendChild(value_cell);
table.appendChild(row);
}
return table;
}
function setupPage(workScheduleData){
workScheduleData = JSON.parse(workScheduleData);
let print_button = document.createElement("button");
print_button.id = "print-schedule";
print_button.innerHTML = "Print";
print_button.addEventListener("click", function(){
g.setTotalHeight("");
g.Draw();
var values = document.getElementById("print_page_size").value.split(",")
let css =
"@media print {\n @page {\n size: " + values[0] + "mm " + values[1] + "mm;\n }\n";
g.printChart(values[0], values[1], css);
g.setTotalHeight(900);
g.Draw();
});
document.getElementById("print_options").appendChild(print_button);
let table = draw_table(workScheduleData)
document.getElementById("schedule-data").appendChild(table);
table.style.display = "none";
let toggle_button = document.createElement("button");
toggle_button.id = "toggle-table";
toggle_button.innerHTML = "Show Schedule Information";
toggle_button.onclick = function(){
if (table.style.display === "none"){
table.style.display = "table";
toggle_button.innerHTML = "Hide Schedule Information";
} else {
toggle_button.innerHTML = "Show Schedule Information";
table.style.display = "none";
}
}
document.getElementById("options").appendChild(toggle_button);
let schedule_name = document.createElement("h2");
let schedule_name_string = document.createTextNode("Schedule: " + workScheduleData.Name);
schedule_name.appendChild(schedule_name_string);
document.getElementById("schedule-header").appendChild(schedule_name);
let creation_date = document.createElement("h2");
let creation_date_string = document.createTextNode("Created: " + new Date(workScheduleData.CreationDate).toLocaleDateString());
creation_date.appendChild(creation_date_string);
document.getElementById("schedule-header").appendChild(creation_date);
}
@@ -0,0 +1,172 @@
.gtaskcellwkend,
.gtaskcellcurrent,
.gminorheadingwkend {
background-color: #e1e1e1;
}
.gitemhighlight td {
background-color: #ffdaaa;
}
.gtaskblue {
background: #4281A4;
}
.gtaskred {
background: #C1666B;
}
.gtaskgreen {
background: #48A9A6;
}
.gtaskyellow {
background: #D4B483;
}
.gname.ggroupitem {
background-color: #4c92ec;
font-weight: bold;
}
.gtaskcellwkend,
.gtaskcellcurrent,
.gminorheadingwkend {
background-color: #e1e1e1;
}
.gitemhighlight td {
background-color: #ffdaaa;
}
.gtaskblue {
background: #4281A4;
}
.gtaskred {
background: #C1666B;
}
.gtaskgreen {
background: #48A9A6;
}
.gtaskyellow {
background: #D4B483;
}
.gmainleft {
resize: horizontal;
height:100%;
/* flex: 0 1 auto; */
}
.gmainright {
height:100%;
overflow: auto;
font-size: 1.5rem;
}
.gcaption {
/* github style header 3 */
font-size: 0.75rem;
font-weight: 200;
line-height: 1.25;
color: black;
}
.gchartgrid {
background-color: transparent;
height: fit-content;
}
.gchartcontainer {
border: 1px solid #e1e1e1;
height: fit-content;
}
@media print {
.no-print {
display: none;
}
}
button, .sweet_button {
border: none;
background-color: #ffffff;
color: black;
padding: 0.5rem 1rem;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 1rem;
margin: 0.25rem 0.5rem;
cursor: pointer;
border-radius: 0.25rem;
/* add shadow */
box-shadow: 0 0 0.7rem rgba(95, 95, 95, 0.4);
}
h1 {
font-size: 2rem;
font-weight: 500;
line-height: 1.25;
color: black;
margin: 0.5rem;
}
h2 {
font-size: 1.5rem;
font-weight: 500;
line-height: 1.25;
color: black;
margin: 0.5rem;
}
h4 {
font-size: 1.2rem;
font-weight: 500;
line-height: 1.25;
color: black;
margin: 0.5rem;
}
.table-description {
/* create a github style table similar to a wild car */
margin-bottom: 1rem;
width : 50%;
background-color: transparent;
border-collapse: collapse;
border-spacing: 0;
border-color: grey;
border: 1px solid #e1e1e1;
border-radius: 0.25rem;
box-shadow: 0 0 0.7rem rgba(95, 95, 95, 0.4);
text-align: center;
}
.table-description tr {
display: table-row;
vertical-align: inherit;
border-color: inherit;
border-bottom: 1px solid #e1e1e1;
}
.table-description thead {
display: table-header-group;
vertical-align: middle;
border-color: inherit;
background-color: #95A5A6;
}
.top-right {
position: absolute;
top: 8px;
right: 16px;
}
#schedule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
@@ -18,7 +18,7 @@
import isodate
from dateutil import parser
import ifcopenshell.util.date as ifcdateutils
import ifcopenshell.util.date
from datetime import timedelta
@@ -33,11 +33,7 @@ def parse_datetime(value):
def parse_duration(value):
try:
return isodate.parse_duration(value)
except:
return None
return ifcopenshell.util.date.parse_duration(value)
def canonicalise_time(time):
if not time:
@@ -48,7 +44,7 @@ def canonicalise_time(time):
def parse_duration_as_blender_props(dt, simplify=True):
if simplify:
if isinstance(dt, str):
dt = ifcdateutils.ifc2datetime(dt)
dt = ifcopenshell.util.date.ifc2datetime(dt)
seconds = getattr(dt, "seconds", 0)
hours, seconds = divmod(seconds, 3600)
@@ -66,10 +62,12 @@ def parse_duration_as_blender_props(dt, simplify=True):
}
def simplify_duration(durations_attributes, duration_type, prop_name):
for item in durations_attributes:
if item.name == prop_name:
duration_props = item
def blender_props_to_iso_duration(durations_attributes, duration_type, prop_name):
duration_props = None
for collection in durations_attributes:
if collection.name == prop_name:
duration_props = collection
break
if duration_props and not duration_type or duration_type == "ELAPSEDTIME":
duration_string = "P{}Y{}M{}DT{}H{}M{}S".format(
duration_props.years if duration_props.years else 0,
@@ -79,7 +77,7 @@ def simplify_duration(durations_attributes, duration_type, prop_name):
duration_props.minutes if duration_props.minutes else 0,
duration_props.seconds if duration_props.seconds else 0,
)
duration_object = ifcdateutils.ifc2datetime(duration_string)
duration_object = ifcopenshell.util.date.ifc2datetime(duration_string)
elif duration_props and duration_type == "WORKTIME":
years = (duration_props.years * 365 * 24 * 60 * 60) if duration_props.years else 0
months = (duration_props.months * 30 * 24 * 60 * 60) if duration_props.months else 0
@@ -96,6 +94,8 @@ def simplify_duration(durations_attributes, duration_type, prop_name):
extra_days, seconds_left = divmod(total_seconds, calendar_seconds_per_day)
total_days = days_subtotal + extra_days
duration_object = timedelta(days=total_days, seconds=seconds_left)
else:
return None
if duration_object:
total_days = int(duration_object.days)
seconds_left = int(duration_object.seconds)
@@ -113,12 +113,21 @@ def simplify_duration(durations_attributes, duration_type, prop_name):
hours, seconds = divmod(seconds_left, 3600)
minutes, seconds = divmod(seconds, 60)
return "P{}Y{}M{}DT{}H{}M{}S".format(
int(years),
int(months),
int(days),
int(hours),
int(minutes),
int(seconds),
)
if years > 0 or months > 0 or total_days > 0 or hours > 0 or minutes > 0 or seconds > 0:
duration_string = "P"
duration_string += "{}Y".format(int(years)) if years > 0 else ""
duration_string += "{}M".format(int(months)) if months > 0 else ""
duration_string += "{}D".format(int(total_days)) if total_days > 0 else ""
if hours > 0 or minutes > 0 or seconds > 0:
duration_string += "T"
if hours > 0:
duration_string += "{}H".format(int(hours))
if minutes > 0:
duration_string += "{}M".format(int(minutes))
if seconds > 0:
duration_string += "{}S".format(int(seconds))
return duration_string
else:
return None
else:
return None
@@ -168,15 +168,14 @@ class EnableEditingWorkSchedule(bpy.types.Operator):
return {"FINISHED"}
class EnableEditingWorkScheduleTasks(bpy.types.Operator):
class EnableEditingWorkScheduleTasks(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_work_schedule_tasks"
bl_label = "Enable Editing Work Schedule Tasks"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
def execute(self, context):
def _execute(self, context):
core.enable_editing_work_schedule_tasks(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule))
return {"FINISHED"}
class LoadTaskProperties(bpy.types.Operator):
@@ -275,13 +274,13 @@ class EditTaskTime(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingTask(bpy.types.Operator):
bl_idname = "bim.enable_editing_task"
bl_label = "Enable Editing Task"
bl_idname = "bim.enable_editing_task_attributes"
bl_label = "Enable Editing Task Attributes"
bl_options = {"REGISTER", "UNDO"}
task: bpy.props.IntProperty()
def execute(self, context):
core.enable_editing_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
core.enable_editing_task_attributes(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
return {"FINISHED"}
@@ -1295,7 +1294,7 @@ class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
class LoadProductTasks(bpy.types.Operator):
bl_idname = "bim.load_product_tasks"
bl_idname = "bim.load_product_related_tasks"
bl_label = "Load Product Tasks"
bl_options = {"REGISTER", "UNDO"}
@@ -1310,7 +1309,7 @@ class LoadProductTasks(bpy.types.Operator):
return True
def execute(self, context):
core.load_product_tasks(
core.load_product_related_tasks(
tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
return {"FINISHED"}
@@ -1327,3 +1326,66 @@ class HighlightTask(bpy.types.Operator):
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
class SelectWorkScheduleProducts(bpy.types.Operator):
bl_idname = "bim.select_work_schedule_products"
bl_label = "Select Work Schedule Products"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
def execute(self, context):
r = core.select_work_schedule_products(
tool.Sequence, tool.Spatial, work_schedule=tool.Ifc.get().by_id(self.work_schedule)
)
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
class SelectUnassignedWorkScheduleProducts(bpy.types.Operator):
bl_idname = "bim.select_unassigned_work_schedule_products"
bl_label = "Select Unassigned Work Schedule Products"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
def execute(self, context):
r = core.select_unassigned_work_schedule_products(tool.Ifc, tool.Sequence, tool.Spatial)
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
class ReorderTask(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.reorder_task_nesting"
bl_label = "Reorder Nesting"
bl_options = {"REGISTER", "UNDO"}
new_index: bpy.props.IntProperty()
task: bpy.props.IntProperty()
def _execute(self, context):
r = core.reorder_task_nesting(
tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task), new_index=self.new_index
)
if isinstance(r, str):
self.report({"WARNING"}, r)
class CreateBaseline(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.create_baseline"
bl_label = "Create Schedule Baseline"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
name: bpy.props.StringProperty()
def _execute(self, context):
core.create_baseline(
tool.Ifc, tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule), name=self.name
)
def draw(self, context):
layout = self.layout
layout.prop(self, "name", text="Baseline Name")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -20,6 +20,8 @@ import bpy
import isodate
import ifcopenshell.api
import ifcopenshell.util.attribute
import blenderbim.tool as tool
import blenderbim.core.sequence as core
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.sequence.data import SequenceData
import blenderbim.bim.module.pset.data
@@ -37,7 +39,6 @@ from bpy.props import (
CollectionProperty,
)
taskcolumns_enum = []
tasktimecolumns_enum = []
@@ -108,7 +109,7 @@ def updateTaskName(self, context):
props = context.scene.BIMWorkScheduleProperties
if not props.is_task_update_enabled or self.name == "Unnamed":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"sequence.edit_task",
self.file,
@@ -124,7 +125,7 @@ def updateTaskIdentification(self, context):
props = context.scene.BIMWorkScheduleProperties
if not props.is_task_update_enabled or self.identification == "XXX":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"sequence.edit_task",
self.file,
@@ -160,7 +161,7 @@ def updateTaskTimeDateTime(self, context, startfinish):
if startfinish_value == "-":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
try:
startfinish_datetime = parser.isoparse(startfinish_value)
@@ -202,32 +203,32 @@ def updateTaskDuration(self, context):
if self.duration == "-":
return
try:
isodate.parse_duration(self.duration),
except:
duration = ifcopenshell.util.date.parse_duration(self.duration)
if not duration:
self.duration = "-"
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
task = self.file.by_id(self.ifc_definition_id)
if task.TaskTime:
task_time = task.TaskTime
else:
task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=task)
SequenceData.load()
ifcopenshell.api.run(
"sequence.edit_task_time",
self.file,
**{"task_time": task_time, "attributes": {"ScheduleDuration": self.duration}},
**{"task_time": task_time, "attributes": {"ScheduleDuration": duration}},
)
SequenceData.load()
if props.active_task_id == self.ifc_definition_id:
attribute = props.task_time_attributes.get("Duration")
if attribute:
attribute.string_value = self.duration
bpy.ops.bim.load_task_properties()
def get_schedule_predefined_types(self, context):
if not SequenceData.is_loaded:
SequenceData.load()
return SequenceData.data["predefined_types"]
def update_visualisation_start(self, context):
update_visualisation_start_finish(self, context, "visualisation_start")
@@ -278,6 +279,14 @@ def update_color_progress(self, context):
color[2] = color_progress.b
def update_sort_reversed(self, context):
if context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
core.load_task_tree(
tool.Sequence,
work_schedule=tool.Ifc.get().by_id(context.scene.BIMWorkScheduleProperties.active_work_schedule_id),
)
class Task(PropertyGroup):
name: StringProperty(name="Name", update=updateTaskName)
identification: StringProperty(name="Identification", update=updateTaskIdentification)
@@ -335,6 +344,9 @@ class ISODuration(PropertyGroup):
class BIMWorkScheduleProperties(PropertyGroup):
work_schedule_predefined_types: EnumProperty(
items=get_schedule_predefined_types, name="Predefined Type", default=None
)
durations_attributes: CollectionProperty(name="Durations Attributes", type=ISODuration)
work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars")
work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute)
@@ -346,12 +358,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_id: IntProperty(name="Active Task Id")
task_attributes: CollectionProperty(name="Task Attributes", type=Attribute)
should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False)
should_show_bar_visual_option: BoolProperty(name="Should Show Settings UI", default=False)
should_show_task_bar_selection: BoolProperty(name="Add to task bar", default=False)
should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False)
should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
columns: CollectionProperty(name="Columns", type=Attribute)
active_column_index: IntProperty(name="Active Column Index")
sort_column: StringProperty(name="Sort Column")
is_sort_reversed: BoolProperty(name="Is Sort Reversed")
is_sort_reversed: BoolProperty(name="Is Sort Reversed", update=update_sort_reversed)
column_types: EnumProperty(
items=[
("IfcTask", "IfcTask", ""),
@@ -404,6 +417,10 @@ class BIMWorkScheduleProperties(PropertyGroup):
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
active_product_input_task_index: IntProperty(name="Active Product Input Task Index")
enable_reorder: BoolProperty(name="Enable Reorder", default=False)
show_task_operators: BoolProperty(name="Show Task Options", default=True)
should_show_schedule_baseline_ui: BoolProperty(name="Baselines", default=False)
filter_by_active_schedule: BoolProperty(name="Filter By Active Schedule", default=False)
class BIMTaskTreeProperties(PropertyGroup):
@@ -501,3 +518,4 @@ class BIMAnimationProperties(PropertyGroup):
description="color picker",
update=update_color_progress,
)
should_show_task_bar_options: BoolProperty(name="Show Task Bar Options", default=False)
@@ -21,7 +21,7 @@ import blenderbim.bim.helper
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from blenderbim.bim.module.sequence.data import WorkPlansData, SequenceData, TaskICOMData
from blenderbim.bim.module.sequence.data import WorkPlansData, WorkScheduleData, SequenceData, TaskICOMData
class BIM_PT_work_plans(Panel):
@@ -113,55 +113,101 @@ class BIM_PT_work_schedules(Panel):
def draw(self, context):
if not SequenceData.is_loaded:
SequenceData.load()
if not WorkScheduleData.is_loaded:
WorkScheduleData.load()
self.props = context.scene.BIMWorkScheduleProperties
self.tprops = context.scene.BIMTaskTreeProperties
self.animation_props = context.scene.BIMAnimationProperties
row = self.layout.row()
if SequenceData.data["has_work_schedules"]:
row.label(
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
icon="TEXT",
)
else:
row.label(text="No Work Schedules found.", icon="TEXT")
row.operator("bim.add_work_schedule", text="", icon="ADD")
if not self.props.active_work_schedule_id:
row = self.layout.row()
if SequenceData.data["has_work_schedules"]:
row.label(
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
icon="TEXT",
)
else:
row.label(text="No Work Schedules found.", icon="TEXT")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.props, "work_schedule_predefined_types")
row.operator("bim.add_work_schedule", text="Add new", icon="ADD")
for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items():
self.draw_work_schedule_ui(work_schedule_id, work_schedule)
def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
row = self.layout.row(align=True)
row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON")
if not work_schedule["PredefinedType"] == "BASELINE":
row = self.layout.row(align=True)
row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON")
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK")
elif self.props.editing_type == "TASKS":
grid = self.layout.grid_flow(columns=2, even_columns=True)
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK")
elif self.props.editing_type == "TASKS":
row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY")
row.prop(self.props, "should_show_visualisation_ui", text="", icon="CAMERA_STEREO")
row.operator("bim.generate_gantt_chart", text="", icon="NLA").work_schedule = work_schedule_id
row.operator("bim.recalculate_schedule", text="", icon="FILE_REFRESH").work_schedule = work_schedule_id
row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id
row.operator("bim.disable_editing_work_schedule", text="", icon="CANCEL")
else:
row.operator(
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
).work_schedule = work_schedule_id
row.operator(
"bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL"
).work_schedule = work_schedule_id
row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
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.generate_gantt_chart", text="Generate Gantt", icon="NLA"
).work_schedule = work_schedule_id
row1.operator(
"bim.recalculate_schedule", text="Re-calculate Schedule", icon="FILE_REFRESH"
).work_schedule = work_schedule_id
row2 = col.row(align=True)
row2.alignment = "RIGHT"
row2.operator(
"bim.select_work_schedule_products", text="Select Assigned", icon="RESTRICT_SELECT_OFF"
).work_schedule = work_schedule_id
row2.operator(
"bim.select_unassigned_work_schedule_products",
text="Select Unassigned",
icon="RESTRICT_SELECT_OFF",
).work_schedule = work_schedule_id
if WorkScheduleData.data["can_have_baselines"]:
row3 = col.row()
row3.alignment = "RIGHT"
row3.prop(self.props, "should_show_schedule_baseline_ui", icon="RESTRICT_INSTANCED_OFF")
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")
row2 = col.row(align=True)
row2.prop(
self.props, "should_show_visualisation_ui", text="Animation Options", icon="CAMERA_STEREO"
)
row2.prop(self.props, "should_show_snapshot_ui", text="Snapshot Options", icon="CAMERA_STEREO")
row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL")
else:
row.operator(
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
).work_schedule = work_schedule_id
row.operator(
"bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL"
).work_schedule = work_schedule_id
row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
self.draw_editable_work_schedule_ui()
elif self.props.editing_type == "TASKS":
if self.props.should_show_column_ui:
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
self.draw_editable_work_schedule_ui()
elif self.props.editing_type == "TASKS":
self.draw_baseline_ui(work_schedule_id)
self.draw_column_ui()
if self.props.should_show_visualisation_ui:
self.draw_visualisation_ui()
self.draw_editable_task_ui(work_schedule_id)
if self.props.should_show_visualisation_ui:
self.draw_visualisation_ui()
if self.props.should_show_snapshot_ui:
self.draw_snapshot_ui()
self.draw_editable_task_ui(work_schedule_id)
else:
self.draw_readonly_work_schedule_ui(work_schedule_id)
def draw_task_operators(self):
row = self.layout.row(align=True)
@@ -178,18 +224,30 @@ class BIM_PT_work_schedules(Panel):
row.operator("bim.edit_task", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_task", text="", icon="CANCEL")
else:
row.prop(self.props, "should_show_bar_visual_option", text="", icon="NLA_PUSHDOWN")
row.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING").task = ifc_definition_id
row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
row.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = ifc_definition_id
row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = ifc_definition_id
row.operator("bim.add_task", text="", icon="ADD").task = ifc_definition_id
row.operator("bim.duplicate_task", text="", icon="DUPLICATE").task = ifc_definition_id
row.operator("bim.remove_task", text="", icon="X").task = ifc_definition_id
row.prop(self.props, "show_task_operators", text="Edit", icon="GREASEPENCIL")
if self.props.show_task_operators:
row2 = self.layout.row(align=True)
row2.alignment = "RIGHT"
row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING").task = ifc_definition_id
row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
row2.operator(
"bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO"
).task = ifc_definition_id
row2.operator(
"bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL"
).task = ifc_definition_id
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
row.operator("bim.duplicate_task", text="Copy", icon="DUPLICATE").task = ifc_definition_id
row.operator("bim.remove_task", text="Delete", icon="X").task = ifc_definition_id
def draw_column_ui(self):
row = self.layout.row(align=True)
row.operator("bim.setup_default_task_columns", text="Show Default Columns", icon="ANCHOR_BOTTOM")
if not self.props.should_show_column_ui:
return
row = self.layout.row()
row.operator("bim.setup_default_task_columns", text="Add Default Columns", icon="ANCHOR_BOTTOM")
row.alignment = "RIGHT"
row = self.layout.row(align=True)
row.prop(self.props, "column_types", text="")
column_type = self.props.column_types
@@ -226,7 +284,7 @@ class BIM_PT_work_schedules(Panel):
op.work_schedule = self.props.active_work_schedule_id
row = self.layout.row(align=True)
row.label(text="Animation Options")
row.label(text="Speed Settings")
row = self.layout.row(align=True)
row.prop(self.props, "speed_types", text="")
if self.props.speed_types == "FRAME_SPEED":
@@ -237,21 +295,50 @@ class BIM_PT_work_schedules(Panel):
row.prop(self.props, "speed_real_duration", text="")
elif self.props.speed_types == "MULTIPLIER_SPEED":
row.prop(self.props, "speed_multiplier", text="")
row = self.layout.row(align=True)
row.label(text="Display Settings")
row = self.layout.row(align=True)
if not self.animation_props.is_editing:
op = row.operator(
"bim.enable_editing_task_animation_colors", text="Customize Animation Colors", icon="SEQUENCE_COLOR_04"
"bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04"
)
else:
op = row.operator(
"bim.disable_editing_task_animation_colors", text="Hide Animation Colors", icon="SEQUENCE_COLOR_01"
"bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01"
)
row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN")
if self.animation_props.should_show_task_bar_options:
row = self.layout.row()
row.label(text="Task Bar Options", icon="NLA_PUSHDOWN")
row.alignment = "LEFT"
row = self.layout.row(align=True)
row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN")
row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN")
grid = self.layout.grid_flow(columns=2, even_columns=True)
# Column1
col = grid.column()
row = col.row(align=True)
row.prop(self.animation_props, "color_progress")
row = col.row(align=True)
row.prop(self.animation_props, "color_full")
if self.animation_props.is_editing:
self.draw_visualisation_settings_ui()
row = self.layout.row()
row = self.layout.row(align=True)
op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA")
op.work_schedule = self.props.active_work_schedule_id
def draw_snapshot_ui(self):
row = self.layout.row(align=True)
row.label(text="Create Construction Snapshot:")
row = self.layout.row(align=True)
op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="REW")
op.target_prop = "BIMWorkScheduleProperties.visualisation_start"
op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="RESTRICT_RENDER_OFF")
op.work_schedule = self.props.active_work_schedule_id
@@ -286,6 +373,13 @@ class BIM_PT_work_schedules(Panel):
draw_attributes(self.props.work_schedule_attributes, self.layout)
def draw_editable_task_ui(self, work_schedule_id):
row = self.layout.row(align=True)
row.label(text="Task Tools")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.add_summary_task", text="Add Summary Task", icon="ADD").work_schedule = work_schedule_id
row.operator("bim.expand_all_tasks", text="Expand All")
row.operator("bim.contract_all_tasks", text="Contract All")
self.draw_task_operators()
self.layout.template_list(
"BIM_UL_tasks",
@@ -295,9 +389,6 @@ class BIM_PT_work_schedules(Panel):
self.props,
"active_task_index",
)
row = self.layout.row()
row.operator("bim.expand_all_tasks", text="Expand All")
row.operator("bim.contract_all_tasks", text="Contract All")
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
self.draw_editable_task_attributes_ui()
elif self.props.active_task_id and self.props.editing_task_type == "CALENDAR":
@@ -381,6 +472,52 @@ class BIM_PT_work_schedules(Panel):
def draw_editable_task_time_attributes_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.task_time_attributes, self.layout)
def draw_baseline_ui(self, work_schedule_id):
if not self.props.should_show_schedule_baseline_ui:
return
row3 = self.layout.row()
row3.alignment = "RIGHT"
row3.operator("bim.create_baseline", text="Add Baseline", icon="ADD").work_schedule = work_schedule_id
if WorkScheduleData.data["active_work_schedule_baselines"]:
for baseline in WorkScheduleData.data["active_work_schedule_baselines"]:
baseline_row = self.layout.row()
baseline_row.alignment = "RIGHT"
baseline_row.label(
text="{} @ {}".format(baseline["name"], baseline["date"]), icon="RESTRICT_INSTANCED_OFF"
)
baseline_row.operator("bim.generate_gantt_chart", text="Compare", icon="NLA").work_schedule = baseline[
"id"
]
baseline_row.operator(
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
).work_schedule = baseline["id"]
baseline_row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = baseline["id"]
def draw_readonly_work_schedule_ui(self, work_schedule_id):
if self.props.active_work_schedule_id == work_schedule_id:
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL")
grid = self.layout.grid_flow(columns=2, even_columns=True)
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")
row2 = col.row(align=True)
if self.props.editing_type == "TASKS":
self.draw_column_ui()
self.layout.template_list(
"BIM_UL_tasks",
"",
self.tprops,
"tasks",
self.props,
"active_task_index",
)
class BIM_PT_task_icom(Panel):
bl_label = "IFC Task ICOM"
@@ -430,7 +567,7 @@ class BIM_PT_task_icom(Panel):
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
op.related_object = input_id
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
op.task = task.ifc_definition_id
row2 = col.row()
@@ -519,7 +656,7 @@ class BIM_UL_task_resources(UIList):
if item:
row = layout.row(align=True)
row.prop(item, "name", emboss=False, text="")
row.prop(item, "schedule_usage", emboss=False, text="")
row.label(text=str(item.schedule_usage))
class BIM_UL_animation_colors(UIList):
@@ -583,7 +720,7 @@ class BIM_UL_tasks(UIList):
text="",
emboss=False,
)
if self.props.should_show_bar_visual_option:
if self.props.should_show_task_bar_selection:
row.prop(
item,
"has_bar_visual",
@@ -591,6 +728,8 @@ class BIM_UL_tasks(UIList):
text="",
emboss=False,
)
if self.props.enable_reorder:
self.draw_order_operator(row, item.ifc_definition_id)
if self.props.active_task_id:
if self.props.editing_task_type == "SEQUENCE" and self.props.active_task_id != item.ifc_definition_id:
if item.is_predecessor:
@@ -605,6 +744,18 @@ class BIM_UL_tasks(UIList):
op = row.operator("bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False)
op.task = item.ifc_definition_id
def draw_order_operator(self, row, ifc_definition_id):
task = SequenceData.data["tasks"][ifc_definition_id]
if task["NestingIndex"] is not None:
if task["NestingIndex"] == 0:
op = row.operator("bim.reorder_task_nesting", icon="TRIA_DOWN", text="")
op.task = ifc_definition_id
op.new_index = task["NestingIndex"] + 1
elif task["NestingIndex"] > 0:
op = row.operator("bim.reorder_task_nesting", icon="TRIA_UP", text="")
op.task = ifc_definition_id
op.new_index = task["NestingIndex"] - 1
def draw_hierarchy(self, row, item):
for i in range(0, item.level_index):
row.label(text="", icon="BLANK1")
@@ -825,8 +976,8 @@ class BIM_PT_4D_Tools(Panel):
def draw(self, context):
self.props = context.scene.BIMWorkScheduleProperties
row = self.layout.row()
row.operator("bim.load_product_tasks", text="Load Tasks", icon="FILE_REFRESH")
row.operator("bim.load_product_related_tasks", text="Load Tasks", icon="FILE_REFRESH")
row.prop(self.props, "filter_by_active_schedule", text="Filter by Active Schedule")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col1 = grid.column()
col1.label(text="Product Input Tasks")
+123
View File
@@ -22,6 +22,9 @@ import json
import time
import logging
import textwrap
import shutil
import platform
import subprocess
import tempfile
import webbrowser
import ifcopenshell
@@ -148,6 +151,126 @@ class SelectSchemaDir(bpy.types.Operator):
return {"RUNNING_MODAL"}
class FileAssociate(bpy.types.Operator):
bl_idname = "bim.file_associate"
bl_label = "Associate BlenderBIM with *.ifc files"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Creates a Desktop launcher and associates it with IFC files"
@classmethod
def poll(cls, context):
if platform.system() == "Linux":
return True
# TODO Windows and Darwin
# https://stackoverflow.com/questions/1082889/how-to-change-filetype-association-in-the-registry
return False
def execute(self, context):
src_dir = os.path.join(os.path.dirname(__file__), "../libs/desktop")
binary_path = bpy.app.binary_path
if platform.system() == "Linux":
destdir = os.path.join(os.environ["HOME"], ".local")
self.install_desktop_linux(src_dir=src_dir, destdir=destdir, binary_path=binary_path)
return {"FINISHED"}
def install_desktop_linux(self, src_dir=None, destdir="/tmp", binary_path="/usr/bin/blender"):
"""Creates linux file assocations and launcher icon"""
for rel_path in (
"bin",
"share/icons/hicolor/128x128/apps",
"share/icons/hicolor/128x128/mimetypes",
"share/applications",
"share/mime/packages",
):
os.makedirs(os.path.join(destdir, rel_path), exist_ok=True)
shutil.copy(
os.path.join(src_dir, "blenderbim.png"),
os.path.join(destdir, "share/icons/hicolor/128x128/apps"),
)
shutil.copy(
os.path.join(src_dir, "blenderbim.desktop"),
os.path.join(destdir, "share/applications"),
)
shutil.copy(
os.path.join(src_dir, "blenderbim.xml"),
os.path.join(destdir, "share/mime/packages"),
)
shutil.copyfile(
os.path.join(src_dir, "x-ifc_128x128.png"),
os.path.join(destdir, "share/icons/hicolor/128x128/mimetypes", "x-ifc.png"),
)
# copy and rewrite wrapper script
with open(os.path.join(src_dir, "blenderbim"), "r") as wrapper_template:
filedata = wrapper_template.read()
filedata = filedata.replace("#BLENDER_EXE=/opt/blender-3.3/blender", 'BLENDER_EXE="' + binary_path + '"')
with open(os.path.join(destdir, "bin", "blenderbim"), "w") as wrapper:
wrapper.write(filedata)
os.chmod(os.path.join(destdir, "bin", "blenderbim"), 0o755)
self.refresh_system_linux(destdir=destdir)
def refresh_system_linux(self, destdir="/tmp"):
"""Attempt to update mime and desktop databases"""
try:
subprocess.call(["update-mime-database", os.path.join(destdir, "share/mime")])
except:
pass
try:
subprocess.call(["update-desktop-database", os.path.join(destdir, "share/applications")])
except:
pass
class FileUnassociate(bpy.types.Operator):
bl_idname = "bim.file_unassociate"
bl_label = "Remove BlenderBIM *.ifc association"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Removes Desktop launcher and unassociates it with IFC files"
@classmethod
def poll(cls, context):
if platform.system() == "Linux":
return True
return False
def execute(self, context):
if platform.system() == "Linux":
destdir = os.path.join(os.environ["HOME"], ".local")
self.uninstall_desktop_linux(destdir=destdir)
return {"FINISHED"}
def uninstall_desktop_linux(self, destdir="/tmp"):
"""Removes linux file assocations and launcher icon"""
for rel_path in (
"share/icons/hicolor/128x128/apps/blenderbim.png",
"share/icons/hicolor/128x128/mimetypes/x-ifc.png",
"share/applications/blenderbim.desktop",
"share/mime/packages/blenderbim.xml",
"bin/blenderbim",
):
try:
os.remove(os.path.join(destdir, rel_path))
except:
pass
self.refresh_system_linux(destdir=destdir)
def refresh_system_linux(self, destdir="/tmp"):
"""Attempt to update mime and desktop databases"""
try:
subprocess.call(["update-mime-database", os.path.join(destdir, "share/mime")])
except:
pass
try:
subprocess.call(["update-desktop-database", os.path.join(destdir, "share/applications")])
except:
pass
class OpenUpstream(bpy.types.Operator):
bl_idname = "bim.open_upstream"
bl_label = "Open Upstream Reference"
+41 -4
View File
@@ -120,6 +120,36 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False
)
lock_grids_on_import: BoolProperty(name="Should Lock Grids By Default", default=True)
decorations_colour: bpy.props.FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
decorator_color_selected: bpy.props.FloatVectorProperty(
name="Selected Elements Color",
subtype="COLOR",
default=(0.545, 0.863, 0, 1), # green
min=0.0,
max=1.0,
size=4,
description="Color of selected verts/edges (used in profile editing mode)",
)
decorator_color_unselected: bpy.props.FloatVectorProperty(
name="Not Selected Elements Color",
subtype="COLOR",
default=(1, 1, 1, 1), # white
min=0.0,
max=1.0,
size=4,
description="Color of not selected verts/edges (used in profile editing mode)",
)
decorator_color_special: bpy.props.FloatVectorProperty(
name="Special Elements Color",
subtype="COLOR",
default=(0.157, 0.565, 1, 1), # blue
min=0.0,
max=1.0,
size=4,
description="Color of special selected verts/edges (openings, preview verts/edges in roof editing, verts with arcs/circles in profile editing)",
)
def draw(self, context):
layout = self.layout
@@ -145,6 +175,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.operator("bim.open_upstream", text="Visit Wiki").page = "wiki"
row.operator("bim.open_upstream", text="Visit Community").page = "community"
row = layout.row()
row.operator("bim.file_associate", icon="LOCKVIEW_ON")
row.operator("bim.file_unassociate", icon="LOCKVIEW_OFF")
row = layout.row()
row.prop(self, "svg2pdf_command")
row = layout.row()
row.prop(self, "svg2dxf_command")
@@ -172,13 +205,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.prop(context.scene.BIMModelProperties, "occurrence_name_function")
row = self.layout.row()
row.prop(context.scene.DocProperties, "decorations_colour")
row.prop(self, "decorations_colour")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_selected")
row.prop(self, "decorator_color_selected")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_unselected")
row.prop(self, "decorator_color_unselected")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_special")
row.prop(self, "decorator_color_special")
row = self.layout.row(align=True)
row.prop(context.scene.BIMProperties, "schema_dir")
@@ -204,6 +237,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.prop(context.scene.DocProperties, "symbols_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "patterns_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "shadingstyles_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "shadingstyle_default")
row = layout.row()
row.operator("bim.configure_visibility")
+21 -3
View File
@@ -20,16 +20,19 @@ def enable_editing_cost_schedule_attributes(cost, cost_schedule):
cost.load_cost_schedule_attributes(cost_schedule)
cost.enable_editing_cost_schedule_attributes(cost_schedule)
def enable_editing_cost_items(cost, cost_schedule):
cost.enable_editing_cost_items(cost_schedule)
cost.load_cost_schedule_tree()
cost.play_sound()
def add_summary_cost_item(ifc, cost, cost_schedule):
ifc.run("cost.add_cost_item", cost_schedule=cost_schedule)
cost.load_cost_schedule_tree()
# cost.play_sound()
def add_cost_item(ifc, cost, cost_item):
ifc.run("cost.add_cost_item", cost_item=cost_item)
cost.load_cost_schedule_tree()
@@ -175,54 +178,68 @@ def enable_editing_cost_item_value(cost, cost_value):
def disable_editing_cost_item_value(cost):
cost.disable_editing_cost_item_value()
def enable_editing_cost_item_value_formula(cost, cost_value):
cost.load_cost_item_value_formula_attributes(cost_value)
cost.enable_editing_cost_item_value_formula(cost_value)
def edit_cost_item_value_formula(ifc, cost, cost_value):
formula = cost.get_cost_item_value_formula()
ifc.run("cost.edit_cost_value_formula", cost_value=cost_value, formula=formula)
cost.disable_editing_cost_item_value()
def edit_cost_value(ifc, cost, cost_value):
attributes = cost.get_cost_value_attributes()
ifc.run("cost.edit_cost_value", cost_value=cost_value, attributes=attributes)
cost.disable_editing_cost_item_value()
#cost.load_cost_item_values(cost.get_active_cost_item())
#cost.load_cost_item_values(cost.get_highlighted_cost_item())
def copy_cost_item_values(ifc, cost, source, destination):
ifc.run("cost.copy_cost_item_values", source=source, destination=destination)
def select_cost_item_products(cost, spatial, cost_item):
is_deep = cost.show_nested_cost_item_elements()
products = cost.get_cost_item_products(cost_item, is_deep)
spatial.select_products(products)
def select_cost_schedule_products(cost, spatial, cost_schedule):
products = cost.get_cost_schedule_products(cost_schedule)
spatial.select_products(products)
def import_cost_schedule_csv(cost, file_path, is_schedule_of_rates):
cost.import_cost_schedule_csv(file_path, is_schedule_of_rates)
def add_cost_column(cost, name):
cost.add_cost_column(name)
def remove_cost_column(cost, name):
cost.remove_cost_column(name)
def expand_cost_item_rate(cost, cost_item):
cost.expand_cost_item_rate(cost_item)
def contract_cost_item_rate(cost, cost_item):
cost.contract_cost_item_rate(cost_item)
def calculate_cost_item_resource_value(ifc, cost_item):
ifc.run("cost.calculate_cost_item_resource_value", cost_item=cost_item)
def export_cost_schedules(cost, format):
cost.export_cost_schedules(format)
def clear_cost_item_assignments(ifc, cost, cost_item, related_object_type):
products = cost.get_cost_item_assignments(cost_item, filter_by_type=related_object_type)
if products:
@@ -230,6 +247,7 @@ def clear_cost_item_assignments(ifc, cost, cost_item, related_object_type):
cost.load_cost_item_quantity_assignments(cost_item, related_object_type=related_object_type)
cost.load_cost_schedule_tree()
def select_unassigned_products(ifc, cost, spatial):
spatial.deselect_all()
products = ifc.get().by_type("IfcElement")
@@ -237,9 +255,11 @@ def select_unassigned_products(ifc, cost, spatial):
selection = [product for product in products if not cost.has_cost_assignments(product, cost_schedule)]
spatial.select_products(selection)
def load_product_cost_items(cost, product):
cost.load_product_cost_items(product)
def highlight_product_cost_item(spatial, cost, cost_item):
cost_schedule = cost.get_cost_schedule(cost_item)
is_cost_schedule_active = cost.is_cost_schedule_active(cost_schedule)
@@ -248,8 +268,6 @@ def highlight_product_cost_item(spatial, cost, cost_item):
else:
return "Cost schedule is not active"
def toggle_cost_item_parent(cost, cost_item):
cost.toggle_cost_item_parent(cost_item)
def change_parent_cost_item(ifc, cost, new_parent):
cost_item = cost.get_active_cost_item()
+15 -12
View File
@@ -28,8 +28,8 @@ def disable_editing_text(drawing, obj=None):
def edit_text(drawing, obj=None):
drawing.synchronise_ifc_and_text_attributes(obj)
drawing.update_text_value(obj)
drawing.update_text_size_pset(obj)
drawing.update_text_value(obj)
drawing.disable_editing_text(obj)
@@ -104,9 +104,7 @@ def remove_sheet(ifc, drawing, sheet=None):
def rename_sheet(ifc, drawing, sheet=None, identification=None, name=None):
ifc.run(
"document.edit_information", information=sheet, attributes={"Identification": identification, "Name": name}
)
ifc.run("document.edit_information", information=sheet, attributes={"Identification": identification, "Name": name})
for reference in drawing.get_document_references(sheet):
description = drawing.get_reference_description(reference)
if description == "SHEET":
@@ -198,7 +196,9 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
human_scale = "1:100"
else:
scale = "1/96"
human_scale = "1/8\"=1'-0\""
human_scale = '1/8"=1\'-0"'
shading_styles_path = drawing.get_default_drawing_resource_path("ShadingStyles")
ifc.run(
"pset.edit_pset",
pset=pset,
@@ -214,8 +214,11 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
"Markers": drawing.get_default_drawing_resource_path("Markers"),
"Symbols": drawing.get_default_drawing_resource_path("Symbols"),
"Patterns": drawing.get_default_drawing_resource_path("Patterns"),
"ShadingStyles": (shading_styles_path := drawing.get_default_drawing_resource_path("ShadingStyles")),
"CurrentShadingStyle": drawing.get_default_shading_style(),
},
)
drawing.setup_shading_styles_path(shading_styles_path)
information = ifc.run("document.add_information")
uri = drawing.get_default_drawing_path(drawing_name)
reference = ifc.run("document.add_reference", information=information)
@@ -312,13 +315,13 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None):
if drawing_tool.does_file_exist(resolved_old_location):
drawing_tool.move_file(resolved_old_location, ifc.resolve_uri(new_location))
for reference in drawing_tool.get_references_with_location(old_location):
ifc.run("document.edit_reference", reference=reference, attributes={"Location": new_location})
sheet = drawing_tool.get_reference_document(reference)
if not sheet:
uri = ifc.resolve_uri(drawing_tool.get_document_uri(sheet, "LAYOUT"))
if drawing_tool.does_file_exist(uri):
drawing_tool.update_embedded_svg_location(uri, old_location, new_location)
for reference in drawing_tool.get_references_with_location(old_location):
ifc.run("document.edit_reference", reference=reference, attributes={"Location": new_location})
sheet = drawing_tool.get_reference_document(reference)
if sheet:
uri = ifc.resolve_uri(drawing_tool.get_document_uri(sheet, "LAYOUT"))
if drawing_tool.does_file_exist(uri):
drawing_tool.update_embedded_svg_location(uri, old_location, new_location)
def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None):
+15 -4
View File
@@ -10,6 +10,15 @@ def add_file(ifcgit, ifc):
ifcgit.add_file_to_repo(repo, path_ifc)
def clone_repo(ifcgit, remote_url, local_folder, operator):
repo = ifcgit.clone_repo(remote_url, local_folder)
if not repo:
operator.report({"ERROR"}, "Clone failed")
return
operator.report({"INFO"}, "Repository cloned")
ifcgit.load_anyifc(repo)
def discard_uncomitted(ifcgit, ifc):
path_ifc = ifc.get_path()
# NOTE this is calling the git binary in a subprocess
@@ -25,11 +34,12 @@ def commit_changes(ifcgit, ifc, repo, context):
ifcgit.create_new_branch()
def add_tag(ifcgit, repo):
ifcgit.add_tag(repo)
def refresh_revision_list(ifcgit, repo, ifc):
ifcgit.clear_commits_list()
path_ifc = ifc.get_path()
lookup = ifcgit.tags_by_hexsha(repo)
ifcgit.get_commits_list(path_ifc, lookup)
ifcgit.refresh_revision_list(ifc.get_path())
def colourise_revision(ifcgit, context):
@@ -54,6 +64,7 @@ def switch_revision(ifcgit, ifc):
path_ifc = ifc.get_path()
ifcgit.switch_to_revision_item()
ifcgit.load_project(path_ifc)
ifcgit.refresh_revision_list(path_ifc)
def merge_branch(ifcgit, ifc, operator):
@@ -170,3 +170,15 @@ def unassign_resource(ifc, spatial, resource=None, products=None):
products = spatial.get_selected_products()
for product in products:
ifc.run("resource.unassign_resource", relating_resource=resource, related_object=product)
def edit_productivity_pset(ifc, resource_tool):
resource = resource_tool.get_highlighted_resource()
if resource is None:
return
productivity = resource_tool.get_productivity(resource)
if productivity:
pset = ifc.get().by_id(productivity["id"])
else:
pset = ifc.run("pset.add_pset", product=resource, name="EPset_Productivity")
ifc.run("pset.edit_pset", pset=pset, properties=resource_tool.get_productivity_attributes())
+5 -1
View File
@@ -21,6 +21,10 @@ def copy_class(ifc, collector, geometry, root, obj=None):
element = ifc.get_entity(obj)
if not element:
return
if root.is_element_a(element, "IfcRelSpaceBoundary"):
new = ifc.run("boundary.copy_boundary", boundary=element)
ifc.link(new, obj)
return new
representation = root.get_object_representation(obj)
new = ifc.run("root.copy_class", product=element)
ifc.link(new, obj)
@@ -38,7 +42,7 @@ def copy_class(ifc, collector, geometry, root, obj=None):
geometry.link(new_representation, data)
root.assign_body_styles(new, obj)
collector.assign(obj)
if root.is_opening_element(new):
if root.is_element_a(new, "IfcOpeningElement"):
root.add_tracked_opening(obj)
return new
+71 -19
View File
@@ -74,47 +74,47 @@ def enable_editing_work_schedule(sequence, work_schedule=None):
def enable_editing_work_schedule_tasks(sequence, work_schedule=None):
sequence.enable_editing_work_schedule_tasks(work_schedule)
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def create_task_tree(sequence, work_schedule):
sequence.create_task_tree(work_schedule)
def load_task_tree(sequence, work_schedule):
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def expand_task(sequence, task=None):
sequence.expand_task(task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def expand_all_tasks(sequence):
sequence.expand_all_tasks()
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def contract_task(sequence, task=None):
sequence.contract_task(task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def contract_all_tasks(sequence):
sequence.contract_all_tasks()
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def remove_task(ifc, sequence, task=None):
ifc.run("sequence.remove_task", task=task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
sequence.disable_selecting_deleted_task()
@@ -129,22 +129,26 @@ def disable_editing_work_schedule(sequence):
def add_summary_task(ifc, sequence, work_schedule=None):
ifc.run("sequence.add_task", work_schedule=work_schedule)
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def add_task(ifc, sequence, parent_task=None):
ifc.run("sequence.add_task", parent_task=parent_task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def enable_editing_task(sequence, task=None):
sequence.load_task_attributes(task)
sequence.enable_editing_task(task)
def enable_editing_task_attributes(sequence, task=None):
sequence.load_task_attributes(task)
sequence.enable_editing_task_attributes(task)
def edit_task(ifc, sequence, task=None):
attributes = sequence.get_task_attributes()
ifc.run("sequence.edit_task", task=task, attributes=attributes)
@@ -165,7 +169,7 @@ def copy_task_attribute(ifc, sequence, attribute_name=None):
def duplicate_task(ifc, sequence, task=None):
ifc.run("sequence.duplicate_task", task=task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
@@ -245,7 +249,10 @@ def unassign_input_products(ifc, sequence, spatial, task=None, products=None):
def assign_resource(ifc, sequence, task=None):
resource = sequence.get_selected_resource()
sub_resource = ifc.run(
"resource.add_resource", parent_resource=resource, ifc_class=resource.is_a(), name=resource.Name
"resource.add_resource",
parent_resource=resource,
ifc_class=resource.is_a(),
name="{}/{}".format(resource.Name or "Unnamed", task.Name or ""),
)
ifc.run("sequence.assign_process", relating_process=task, related_object=sub_resource)
resources = sequence.get_task_resources(task)
@@ -430,13 +437,30 @@ def select_task_inputs(sequence, spatial, task=None):
spatial.select_products(products=sequence.get_task_inputs(task))
def select_work_schedule_products(sequence, spatial, work_schedule=None):
products = sequence.get_work_schedule_products(work_schedule)
spatial.select_products(products)
def select_unassigned_work_schedule_products(ifc, sequence, spatial):
spatial.deselect_all()
products = ifc.get().by_type("IfcElement")
work_schedule = sequence.get_active_work_schedule()
schedule_products = sequence.get_work_schedule_products(work_schedule)
selection = [product for product in products if product not in schedule_products]
spatial.select_products(selection)
def recalculate_schedule(ifc, work_schedule=None):
ifc.run("sequence.recalculate_schedule", work_schedule=work_schedule)
def add_task_column(sequence, column_type=None, name=None, data_type=None):
sequence.add_task_column(column_type, name, data_type)
work_schedule = sequence.get_active_work_schedule()
if work_schedule:
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def remove_task_column(sequence, name=None):
sequence.remove_task_column(name)
@@ -444,13 +468,16 @@ def remove_task_column(sequence, name=None):
def set_task_sort_column(sequence, column=None):
sequence.set_task_sort_column(column)
work_schedule = sequence.get_active_work_schedule()
if work_schedule:
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def calculate_task_duration(ifc, sequence, task=None):
ifc.run("sequence.calculate_task_duration", task=task)
work_schedule = sequence.get_active_work_schedule()
if work_schedule:
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
@@ -459,6 +486,8 @@ def highlight_task(sequence, task=None):
is_work_schedule_active = sequence.is_work_schedule_active(work_schedule)
if is_work_schedule_active:
sequence.highlight_task(task)
else:
return "Work schedule is not active"
def highlight_product_related_task(sequence, spatial, product_type=None):
@@ -520,7 +549,30 @@ def visualise_work_schedule_date(sequence, work_schedule=None):
def generate_gantt_chart(sequence, work_schedule):
json = sequence.create_tasks_json(work_schedule)
sequence.generate_gantt_browser_chart(json)
sequence.generate_gantt_browser_chart(json, work_schedule)
def load_product_tasks(sequence, product=None):
sequence.load_product_tasks(product)
def load_product_related_tasks(sequence, product=None):
filter_by_schedule = sequence.is_filter_by_active_schedule()
if filter_by_schedule:
work_schedule = sequence.get_active_work_schedule()
task_inputs, task_ouputs = sequence.get_tasks_for_product(product, work_schedule)
else:
task_inputs, task_ouputs = sequence.get_tasks_for_product(product)
sequence.load_product_related_tasks(task_inputs, task_ouputs)
def reorder_task_nesting(ifc, sequence, task, new_index):
is_sorting_enabled = sequence.is_sorting_enabled()
is_sort_reversed = sequence.is_sort_reversed()
if is_sorting_enabled or is_sort_reversed:
return "Remove manual sorting"
else:
ifc.run("nest.reorder_nesting", item=task, new_index=new_index)
work_schedule = sequence.get_active_work_schedule()
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def create_baseline(ifc, sequence, work_schedule, name):
ifc.run("sequence.create_baseline", work_schedule=work_schedule, name=name)
+55 -26
View File
@@ -274,6 +274,8 @@ class Drawing:
def get_body_context(cls): pass
def get_default_drawing_path(cls, name): pass
def get_default_drawing_resource_path(cls, resource): pass
def get_default_shading_style(cls): pass
def setup_shading_styles_path(cls, resource_path): pass
def get_default_layout_path(cls, identification, name): pass
def get_default_sheet_path(cls, identification, name): pass
def get_default_sheet_path(cls, identification, name): pass
@@ -292,6 +294,7 @@ class Drawing:
def get_reference_description(cls, reference): pass
def get_reference_document(cls, reference): pass
def get_reference_location(cls, reference): pass
def get_references_with_location(cls, location): pass
def get_text_literal(cls, obj): pass
def get_unit_system(cls): pass
def import_assigned_product(cls, obj): pass
@@ -313,6 +316,7 @@ class Drawing:
def show_decorations(cls): pass
def sync_object_placement(cls, obj): pass
def synchronise_ifc_and_text_attributes(cls, obj): pass
def update_embedded_svg_location(cls, uri, old_location, new_location): pass
def update_text_size_pset(cls, obj): pass
def update_text_value(cls, obj): pass
@@ -562,32 +566,37 @@ class Qto:
@interface
class Resource:
def load_resources(cls): pass
def load_resource_properties(cls): pass
def clear_productivity_data(cls, props): pass
def contract_resource(cls, resource): pass
def disable_editing_resource_cost_value(cls): pass
def disable_editing_resource_quantity(cls): pass
def disable_editing_resource(cls): pass
def disable_resource_editing_ui(cls): pass
def load_resource_attributes(cls, resource): pass
def enable_editing_resource(cls, resource): pass
def get_resource_attributes(cls): pass
def enable_editing_resource_time(cls, resource): pass
def get_resource_time(cls, resource): pass
def load_resource_time_attributes(cls, resource_time): pass
def get_resource_time_attributes(cls): pass
def enable_editing_resource_costs(cls, resource): pass
def disable_editing_resource_cost_value(cls): pass
def enable_editing_resource_cost_value_formula(cls, cost_value): pass
def load_cost_value_attributes(cls, cost_value): pass
def edit_productivity_pset(cls, resource, attributes): pass
def enable_editing_cost_value_attributes(cls, cost_value): pass
def get_resource_cost_value_formula(cls): pass
def get_resource_cost_value_attributes(cls): pass
def enable_editing_resource_base_quantity(cls, resource): pass
def enable_editing_resource_cost_value_formula(cls, cost_value): pass
def enable_editing_resource_costs(cls, resource): pass
def enable_editing_resource_quantity(cls, resource_quantity): pass
def disable_editing_resource_quantity(cls): pass
def get_resource_quantity_attributes(cls): pass
def enable_editing_resource_time(cls, resource): pass
def enable_editing_resource(cls, resource): pass
def expand_resource(cls, resource): pass
def contract_resource(cls, resource): pass
def get_highlighted_resource(cls): pass
def get_productivity_attributes(cls): pass
def get_productivity(cls, resource, should_inherit): pass
def get_resource_attributes(cls): pass
def get_resource_cost_value_attributes(cls): pass
def get_resource_cost_value_formula(cls): pass
def get_resource_quantity_attributes(cls): pass
def get_resource_time_attributes(cls): pass
def get_resource_time(cls, resource): pass
def import_resources(cls, file_path): pass
def load_cost_value_attributes(cls, cost_value): pass
def load_productivity_data(cls): pass
def load_resource_attributes(cls, resource): pass
def load_resource_properties(cls): pass
def load_resource_time_attributes(cls, resource_time): pass
def load_resources(cls): pass
@interface
class Root:
@@ -601,7 +610,7 @@ class Root:
def get_object_name(cls, obj): pass
def get_object_representation(cls, obj): pass
def get_representation_context(cls, representation): pass
def is_opening_element(cls, element): pass
def is_element_a(cls, element, ifc_class): pass
def link_object_data(cls, source_obj, destination_obj): pass
def recreate_decompositions(cls, relationships, old_to_new): pass
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
@@ -619,11 +628,23 @@ class Search:
@interface
class Sequence:
def add_task_column(cls, column_type, name, data_type): pass
def add_text_animation_handler(cls, settings): pass
def animate_consumption(cls, obj, start_frame, product_frame, color, animation_type): pass
def animate_creation(cls, obj, start_frame, product_frame, color): pass
def animate_destruction(cls, obj, start_frame, color, animation_type): pass
def animate_input(cls, obj, start_frame, product_frame, animation_type): pass
def animate_movement_from(cls, obj, start_frame, color, animation_type): pass
def animate_movement_to(cls, obj, start_frame, product_frame, color): pass
def animate_objects(cls, settings, frames, clear_previous, animation_type): pass
def animate_operation(cls, obj, start_frame, product_frame, color): pass
def animate_output(cls, obj, start_frame, product_frame): pass
def clear_object_animation(cls, obj): pass
def clear_objects_animation(cls, include_blender_objects): pass
def contract_all_tasks(cls): pass
def contract_task(cls, task): pass
def create_bars(cls, tasks): pass
def create_bars(cls, tasks):pass
def create_task_tree(cls, work_schedule): pass
def create_new_task_json(cls, task, json, type_map=None): pass
def create_tasks_json(cls, work_schedule=None): pass
def disable_editing_rel_sequence(cls): pass
def disable_editing_task_animation_colors(cls): pass
def disable_editing_task_time(cls): pass
@@ -637,10 +658,10 @@ class Sequence:
def enable_editing_rel_sequence_attributes(cls, rel_sequence): pass
def enable_editing_sequence_lag_time(cls, rel_sequence): pass
def enable_editing_task_animation_colors(cls): pass
def enable_editing_task_attributes(cls, task): pass
def enable_editing_task_calendar(cls, task): pass
def enable_editing_task_sequence(cls, task): pass
def enable_editing_task_time(cls, task): pass
def enable_editing_task(cls, task): pass
def enable_editing_work_calendar_times(cls, work_calendar): pass
def enable_editing_work_calendar(cls, work_calendar): pass
def enable_editing_work_plan_schedules(cls, work_plan): pass
@@ -651,11 +672,13 @@ class Sequence:
def expand_all_tasks(cls): pass
def expand_task(cls, task): pass
def find_related_input_tasks(cls, product): pass
def find_related_output_tasks(cls, column): pass
def find_related_output_tasks(cls, product): pass
def generate_gantt_browser_chart(cls, task_json): pass
def get_active_task(cls): pass
def get_active_work_schedule(cls): pass
def get_animation_bar_tasks(cls): pass
def get_animation_product_frames(cls, work_schedule, settings): pass
def get_animation_settings(cls): pass
def get_checked_tasks(cls): pass
def get_direct_nested_tasks(cls, task):pass
def get_direct_task_outputs(cls, task): pass
@@ -665,7 +688,6 @@ class Sequence:
def get_recurrence_pattern_attributes(cls, recurrence_pattern): pass
def get_recurrence_pattern_times(cls): pass
def get_rel_sequence_attributes(cls): pass
def get_selected_resource(cls): pass
def get_start_date(cls): pass
def get_task_attribute_value(cls, attribute_name): pass
@@ -675,15 +697,20 @@ class Sequence:
def get_task_resources(cls, task):pass
def get_task_time_attributes(cls): pass
def get_task_time(cls, task): pass
def get_tasks_for_product(cls, product, work_schedule): pass
def get_work_calendar_attributes(cls): pass
def get_work_plan_attributes(cls): pass
def get_work_schedule_attributes(cls): pass
def get_work_schedule_products(cls, work_schedule): pass
def get_work_schedule(cls, task): pass
def get_work_time_attributes(cls): pass
def guess_date_range(cls, work_schedule): pass
def has_task_assignments(cls, product, cost_schedule=None): pass
def highlight_task(cls, task): pass
def is_filter_by_active_schedule(cls): pass
def is_work_schedule_active(cls, work_schedule): pass
def load_lag_time_attributes(cls, lag_time): pass
def load_product_related_tasks(cls, product): pass
def load_rel_sequence_attributes(cls, rel_sequence): pass
def load_resources(cls): pass
def load_task_animation_colors(cls): pass
@@ -693,6 +720,7 @@ class Sequence:
def load_task_properties(cls, task): pass
def load_task_resources(cls,resources): pass
def load_task_time_attributes(cls, task_time): pass
def load_task_tree(cls, work_schedule): pass
def load_work_calendar_attributes(cls, work_calendar): pass
def load_work_plan_attributes(cls, work_plan): pass
def load_work_schedule_attributes(cls, work_schedule): pass
@@ -707,6 +735,7 @@ class Sequence:
def show_snapshot(cls, product_states): pass
def update_visualisation_date(cls, start_date, finish_date): pass
@interface
class Spatial:
def can_contain(cls, structure_obj, element_obj): pass
@@ -825,7 +854,7 @@ class Unit:
def enable_editing_units(cls): pass
def export_unit_attributes(cls): pass
def get_scene_unit_name(cls, unit_type): pass
def get_scene_unit_si_prefix(cls): pass
def get_scene_unit_si_prefix(cls, unit_type): pass
def import_unit_attributes(cls, unit): pass
def import_units(cls): pass
def is_scene_unit_metric(cls): pass
+6 -8
View File
@@ -18,19 +18,17 @@
def assign_scene_units(ifc, unit):
length_name = unit.get_scene_unit_name("length")
area_name = unit.get_scene_unit_name("area")
volume_name = unit.get_scene_unit_name("volume")
if unit.is_scene_unit_metric():
prefix = unit.get_scene_unit_si_prefix()
prefix = unit.get_scene_unit_si_prefix("LENGTHUNIT")
lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix=prefix)
prefix = unit.get_scene_unit_si_prefix("AREAUNIT")
areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=prefix)
prefix = unit.get_scene_unit_si_prefix("VOLUMEUNIT")
volumeunit = ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=prefix)
else:
lengthunit = ifc.run("unit.add_conversion_based_unit", name=length_name)
areaunit = ifc.run("unit.add_conversion_based_unit", name=area_name)
volumeunit = ifc.run("unit.add_conversion_based_unit", name=volume_name)
lengthunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("LENGTHUNIT"))
areaunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("AREAUNIT"))
volumeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("VOLUMEUNIT"))
planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree")
@@ -9,8 +9,8 @@ install :
${DESTDIR}/share/icons/hicolor/128x128/mimetypes \
${DESTDIR}/share/applications \
${DESTDIR}/share/mime/packages
cp blenderbim.sh ${DESTDIR}/bin/
chmod +x ${DESTDIR}/bin/blenderbim.sh
cp blenderbim ${DESTDIR}/bin/
chmod +x ${DESTDIR}/bin/blenderbim
cp blenderbim.png ${DESTDIR}/share/icons/hicolor/128x128/apps/
cp blenderbim.desktop ${DESTDIR}/share/applications/
cp blenderbim.xml ${DESTDIR}/share/mime/packages/
@@ -20,7 +20,7 @@ install :
uninstall :
rm -f \
${DESTDIR}/bin/blenderbim.sh \
${DESTDIR}/bin/blenderbim \
${DESTDIR}/share/icons/hicolor/128x128/apps/blenderbim.png \
${DESTDIR}/share/applications/blenderbim.desktop \
${DESTDIR}/share/mime/packages/blenderbim.xml \
@@ -15,7 +15,7 @@ configure associations and icons for a single user by running `make`:
Test by running the wrapper script directly, this should launch BlenderBIM with
an empty IFC project:
blenderbim.sh
blenderbim
This `Makefile` installs files to `~/.local` by default, to install to an
alternative location, set `DESTDIR`:
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# Wrapper script to launch BlenderBIM.
# If the `blender` executable can't be found in your PATH environment, set
# BLENDER_EXE to the location of your preferred `blender` executable.
#BLENDER_EXE=/opt/blender-3.3/blender
: ${BLENDER_EXE:=blender}
if [[ $# -eq 1 && -f "$@" ]]; then
"${BLENDER_EXE}" --python-expr 'import bpy; bpy.data.batch_remove(bpy.data.objects); bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True); bpy.ops.bim.load_project(filepath="'"$@"'")'
else
"${BLENDER_EXE}" --python-expr 'import bpy; bpy.data.batch_remove(bpy.data.objects); bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True); bpy.ops.bim.create_project()'
fi;
@@ -1,7 +1,7 @@
[Desktop Entry]
Type=Application
Terminal=false
Exec=blenderbim.sh %f
Exec=blenderbim %f
Name=BlenderBIM
Icon=blenderbim
X-Desktop-File-Install-Version=0.26

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

+10
View File
@@ -115,6 +115,16 @@ class Blender:
tool.Blender.get_viewport_context()["area"].tag_redraw()
@classmethod
def ensure_unique_name(cls, name, objects, iteration=0):
"""returns a unique name for the given name and dictionary of objects
blender style name with .001, .002, etc. suffix
"""
current_iteration = name if not iteration else f"{name}.{iteration:03d}"
if current_iteration not in objects:
return current_iteration
return cls.ensure_unique_name(name, objects, iteration + 1)
@classmethod
def get_default_selection_keypmap(cls):
"""keymap to replicate default blender selection behaviour with click and box selection"""
@@ -70,3 +70,9 @@ class Boundary(blenderbim.core.tool.Boundary):
translation = obj.matrix_world.translation - space.matrix_world.translation
obj.data.transform(mathutils.Matrix.Translation(translation))
obj.matrix_world = space.matrix_world
@classmethod
def decorate_boundary(cls, obj):
new = bpy.context.scene.BIMBoundaryProperties.boundaries.add()
new.obj = obj
obj.show_in_front = True
+2 -3
View File
@@ -68,8 +68,7 @@ class Cost(blenderbim.core.tool.Cost):
props.is_cost_update_enabled = False
cost_schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
props.cost_items.clear()
if not hasattr(cls, "contracted_cost_items"):
cls.contracted_cost_items = json.loads(props.contracted_cost_items)
cls.contracted_cost_items = json.loads(props.contracted_cost_items)
[
cls.create_new_cost_item_li(props.cost_items, cost_item, 0, type="cost")
for rel in cost_schedule.Controls or []
@@ -614,7 +613,7 @@ class Cost(blenderbim.core.tool.Cost):
return True
@classmethod
def toggle_cost_item_parent(cls, cost_item=None):
def toggle_cost_item_parent_change(cls, cost_item=None):
props = bpy.context.scene.BIMCostProperties
if props.change_cost_item_parent:
props.active_cost_item_id = cost_item.id()
+65 -15
View File
@@ -671,6 +671,11 @@ class Drawing(blenderbim.core.tool.Drawing):
new.name = os.path.basename(reference.Location)
new.reference_type = reference.Description
@classmethod
def get_active_sheet(cls, context):
props = context.scene.DocProperties
return next(s for s in props.sheets[: props.active_sheet_index + 1][::-1] if s.is_sheet)
@classmethod
def import_text_attributes(cls, obj):
props = obj.BIMTextProperties
@@ -754,12 +759,7 @@ class Drawing(blenderbim.core.tool.Drawing):
props = obj.BIMTextProperties
literals = cls.get_text_literal(obj, return_list=True)
if not literals:
props.literals.clear()
return
cls.import_text_attributes(obj)
for i, literal in enumerate(literals):
product = cls.get_assigned_product(tool.Ifc.get_entity(obj))
props.literals[i].value = cls.replace_text_literal_variables(literal.Literal, product)
@@ -872,6 +872,21 @@ class Drawing(blenderbim.core.tool.Drawing):
if resource_path:
return resource_path.replace("\\", "/")
@classmethod
def get_default_shading_style(cls):
dprops = bpy.context.scene.DocProperties
return dprops.shadingstyle_default
@classmethod
def setup_shading_styles_path(cls, resource_path):
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)
@classmethod
def get_potential_reference_elements(cls, drawing):
elements = []
@@ -1347,15 +1362,16 @@ class Drawing(blenderbim.core.tool.Drawing):
"""returns a set of elements that are included in the drawing"""
ifc_file = tool.Ifc.get()
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects)
include = pset.get("Include", None)
if include:
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include))
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
else:
if tool.Ifc.get_schema() == "IFC2X3":
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
else:
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in elements if e.is_a() != "IfcSpace"}
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
elements.update(annotations)
@@ -1370,7 +1386,9 @@ class Drawing(blenderbim.core.tool.Drawing):
ifc_file = tool.Ifc.get()
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
include = pset.get("Include", None)
elements = set(ifc_file.by_type("IfcSpace"))
elements = cls.get_elements_in_camera_view(
tool.Ifc.get_object(drawing), [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSpace")]
)
if include:
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
exclude = pset.get("Exclude", None)
@@ -1449,13 +1467,13 @@ class Drawing(blenderbim.core.tool.Drawing):
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
drawing = tool.Ifc.get_entity(camera)
all_elements = set(tool.Ifc.get().by_type("IfcElement")) - set(tool.Ifc.get().by_type("IfcOpeningElement"))
filtered_elements = cls.get_drawing_elements(drawing)
hidden_elements = list(all_elements - filtered_elements)
hidden_objs = [tool.Ifc.get_object(e) for e in hidden_elements]
# Running operators is much more efficient in this scenario than looping through each element
bpy.ops.object.hide_view_clear()
if not bpy.app.background:
bpy.ops.object.hide_view_clear()
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
hidden_objs = [o for o in bpy.context.visible_objects if tool.Ifc.get_entity(o) not in filtered_elements]
for hidden_obj in hidden_objs:
if bpy.context.view_layer.objects.get(hidden_obj.name):
@@ -1492,6 +1510,38 @@ class Drawing(blenderbim.core.tool.Drawing):
)
break
@classmethod
def get_elements_in_camera_view(cls, camera, objs):
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
x = camera.data.ortho_scale / 2
y = (camera.data.BIMCameraProperties.raster_y / camera.data.BIMCameraProperties.raster_x) * x
else:
y = camera.data.ortho_scale / 2
x = (camera.data.BIMCameraProperties.raster_x / camera.data.BIMCameraProperties.raster_y) * y
camera_inverse_matrix = camera.matrix_world.inverted()
return set(
[
tool.Ifc.get_entity(o)
for o in objs
if cls.is_in_camera_view(o, camera_inverse_matrix, x, y, camera.data.clip_start, camera.data.clip_end)
and tool.Ifc.get_entity(o)
]
)
@classmethod
def is_in_camera_view(cls, obj, camera_inverse_matrix, x, y, clip_start, clip_end):
local_bbox = [camera_inverse_matrix @ obj.matrix_world @ Vector(v) for v in obj.bound_box]
for v in local_bbox:
if v.z < -clip_start and v.z > -clip_end and abs(v.x) < x and abs(v.y) < y:
return True
if any([v.z > -clip_start for v in local_bbox]) and any([v.z < -clip_end for v in local_bbox]):
return True
elif any([v.x < -x for v in local_bbox]) and any([v.x > x for v in local_bbox]):
return True
elif any([v.y < -y for v in local_bbox]) and any([v.y > y for v in local_bbox]):
return True
@classmethod
def is_intersecting_camera(cls, obj, camera):
# Based on separating axis theorem
+6 -2
View File
@@ -71,6 +71,9 @@ class Geometry(blenderbim.core.tool.Geometry):
return
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
return blenderbim.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
if element.is_a("IfcRelSpaceBoundary"):
ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element)
return bpy.data.objects.remove(obj)
if obj.users_collection and obj.users_collection[0].name == obj.name:
parent = ifcopenshell.util.element.get_aggregate(element)
if not parent:
@@ -84,6 +87,7 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.data.collections.remove(obj.users_collection[0])
if getattr(element, "FillsVoids", None):
bpy.ops.bim.remove_filling(filling=element.id())
if element.is_a("IfcOpeningElement"):
if element.HasFillings:
for rel in element.HasFillings:
@@ -97,7 +101,7 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.ops.bim.remove_opening(opening_id=rel.RelatedOpeningElement.id())
for port in ifcopenshell.util.system.get_ports(element):
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
try:
obj.name
bpy.data.objects.remove(obj)
@@ -316,7 +320,7 @@ class Geometry(blenderbim.core.tool.Geometry):
@classmethod
def is_edited(cls, obj):
return list(obj.scale) != [1.0, 1.0, 1.0] or obj in IfcStore.edited_objs
return not all([tool.Cad.is_x(o, 1.0) for o in obj.scale]) or obj in IfcStore.edited_objs
@classmethod
def is_mapped_representation(cls, representation):
+58 -19
View File
@@ -14,6 +14,21 @@ class IfcGit:
def init_repo(cls, path_dir):
IfcGitRepo.repo = git.Repo.init(path_dir)
@classmethod
def clone_repo(cls, remote_url, local_folder):
IfcGitRepo.repo = git.Repo.clone_from(remote_url, local_folder)
return IfcGitRepo.repo
@classmethod
def load_anyifc(cls, repo):
working_dir = repo.working_dir
for item in os.listdir(working_dir):
path = os.path.join(working_dir, item)
if os.path.isfile(path) and re.match(".*\.ifc$", path, re.IGNORECASE):
cls.load_project(path)
return True
return False
@classmethod
def get_path_dir(cls, path_ifc):
return os.path.abspath(os.path.dirname(path_ifc))
@@ -39,7 +54,7 @@ class IfcGit:
if parentdir_path == path_dir:
# root folder
return None
return IfcGit.repo_from_path(parentdir_path)
return cls.repo_from_path(parentdir_path)
if repo:
IfcGitRepo.repo = repo
return repo
@@ -62,6 +77,14 @@ class IfcGit:
repo.index.commit(message=props.commit_message)
props.commit_message = ""
@classmethod
def add_tag(cls, repo):
props = bpy.context.scene.IfcGitProperties
item = props.ifcgit_commits[props.commit_index]
repo.create_tag(props.new_tag_name, ref=item.hexsha, message=props.new_tag_message)
props.new_tag_name = ""
props.new_tag_message = ""
@classmethod
def create_new_branch(cls):
props = bpy.context.scene.IfcGitProperties
@@ -86,15 +109,16 @@ class IfcGit:
def get_commits_list(cls, path_ifc, lookup):
props = bpy.context.scene.IfcGitProperties
repo = cls.repo_from_path(path_ifc)
commits = list(
git.objects.commit.Commit.iter_items(
repo=IfcGitRepo.repo,
repo=repo,
rev=[props.display_branch],
)
)
commits_relevant = list(
git.objects.commit.Commit.iter_items(
repo=IfcGitRepo.repo,
repo=repo,
rev=[props.display_branch],
paths=[path_ifc],
)
@@ -108,9 +132,27 @@ class IfcGit:
continue
props.ifcgit_commits.add()
props.ifcgit_commits[-1].hexsha = commit.hexsha
list_item = props.ifcgit_commits[-1]
list_item.hexsha = commit.hexsha
list_item.message = commit.message
list_item.author_name = commit.author.name
list_item.author_email = commit.author.email
if commit in commits_relevant:
props.ifcgit_commits[-1].relevant = True
list_item.relevant = True
if commit.hexsha in lookup:
for tag in lookup[commit.hexsha]:
list_item.tags.add()
list_item.tags[-1].name = tag.name
if tag.tag:
# a lightweight tag has no message
list_item.tags[-1].message = tag.tag.message
@classmethod
def refresh_revision_list(cls, path_ifc):
repo = cls.repo_from_path(path_ifc)
cls.clear_commits_list()
lookup = cls.tags_by_hexsha(repo)
cls.get_commits_list(path_ifc, lookup)
@classmethod
def is_valid_ref_format(cls, string):
@@ -129,7 +171,7 @@ class IfcGit:
# delete any IfcProject/* collections
for collection in bpy.data.collections:
if re.match("^IfcProject/", collection.name):
IfcGit.delete_collection(collection)
cls.delete_collection(collection)
# delete any Ifc* objects not in IfcProject/ heirarchy
for obj in bpy.data.objects:
if re.match("^Ifc", obj.name):
@@ -138,7 +180,6 @@ class IfcGit:
bpy.data.orphans_purge(do_recursive=True)
bpy.ops.bim.load_project(filepath=path_ifc)
bpy.ops.ifcgit.refresh()
@classmethod
def branches_by_hexsha(cls, repo):
@@ -211,14 +252,14 @@ class IfcGit:
return
if current_revision.committed_date > selected_revision.committed_date:
step_ids = IfcGit.ifc_diff_ids(
step_ids = cls.ifc_diff_ids(
repo,
selected_revision.hexsha,
current_revision.hexsha,
path_ifc,
)
else:
step_ids = IfcGit.ifc_diff_ids(
step_ids = cls.ifc_diff_ids(
repo,
current_revision.hexsha,
selected_revision.hexsha,
@@ -271,7 +312,7 @@ class IfcGit:
repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index]
lookup = IfcGit.branches_by_hexsha(repo)
lookup = cls.branches_by_hexsha(repo)
if item.hexsha in lookup:
for branch in lookup[item.hexsha]:
if branch.name == props.display_branch:
@@ -285,14 +326,11 @@ class IfcGit:
for obj in blender_collection.objects:
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(blender_collection)
for collection in bpy.data.collections:
if not collection.users:
bpy.data.collections.remove(collection)
@classmethod
def is_valid_branch_name(cls, new_branch_name):
"""Check if a branch name is valid and doesn't conflict with existing branches"""
if not IfcGit.is_valid_ref_format(new_branch_name):
if not cls.is_valid_ref_format(new_branch_name):
return False
if new_branch_name in [branch.name for branch in IfcGitRepo.repo.branches]:
return False
@@ -312,7 +350,7 @@ class IfcGit:
props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index]
lookup = IfcGit.branches_by_hexsha(repo)
lookup = cls.branches_by_hexsha(repo)
if item.hexsha in lookup:
for branch in lookup[item.hexsha]:
if branch.name == props.display_branch:
@@ -324,12 +362,12 @@ class IfcGit:
# merge is expected to fail, run ifcmerge
try:
repo.git.mergetool(tool="ifcmerge")
except:
except git.exc.GitCommandError as exc:
message = re.sub("( stderr: '|')", "", exc.stderr)
# ifcmerge failed, rollback
repo.git.merge(abort=True)
# FIXME need to report errors somehow
operator.report({"ERROR"}, "IFC Merge failed")
operator.report({"ERROR"}, "IFC Merge failed:" + message)
return False
except:
@@ -340,7 +378,8 @@ class IfcGit:
props.commit_message = "Merged branch: " + props.display_branch
props.display_branch = repo.active_branch.name
IfcGit.load_project(path_ifc)
cls.load_project(path_ifc)
cls.refresh_revision_list(path_ifc)
class IfcGitRepo:
+92
View File
@@ -115,6 +115,56 @@ class Model(blenderbim.core.tool.Model):
cls.bm.free()
return profile
@classmethod
def export_surface(cls, obj):
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
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)
position = Matrix()
position.col[0][:3] = x_axis
position.col[1][:3] = y_axis
position.col[2][:3] = z_axis
position.col[3][:3] = p1
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
helper = Helper(tool.Ifc.get())
indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data)
if isinstance(indices, tuple) and indices[0] is False: # Ugly
return
cls.bm = bmesh.new()
cls.bm.from_mesh(obj.data)
cls.bm.verts.ensure_lookup_table()
cls.bm.edges.ensure_lookup_table()
surface = tool.Ifc.get().createIfcCurveBoundedPlane()
surface.BasisSurface = tool.Ifc.get().createIfcPlane(tool.Ifc.get().createIfcAxis2Placement3D(
tool.Ifc.get().createIfcCartesianPoint([o / cls.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":
cls.points = cls.export_points(position, indices["points"])
surface.OuterBoundary = cls.export_curve(position, indices["profile"])
results = []
for inner_curve in indices["inner_curves"]:
results.append(cls.export_curve(position, inner_curve))
surface.InnerBoundaries = results
cls.bm.free()
return surface
@classmethod
def generate_occurrence_name(cls, element_type, ifc_class):
props = bpy.context.scene.BIMModelProperties
@@ -218,6 +268,44 @@ class Model(blenderbim.core.tool.Model):
return obj
@classmethod
def import_surface(cls, surface, obj=None):
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
cls.vertices = []
cls.edges = []
cls.arcs = []
cls.circles = []
if surface.is_a("IfcCurveBoundedPlane"):
position = Matrix(ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist())
position[0][3] *= cls.unit_scale
position[1][3] *= cls.unit_scale
position[2][3] *= cls.unit_scale
cls.import_curve(obj, position, surface.OuterBoundary)
for inner_boundary in surface.InnerBoundaries:
cls.import_curve(obj, position, inner_boundary)
mesh = bpy.data.meshes.new("Surface")
mesh.from_pydata(cls.vertices, cls.edges, [])
mesh.BIMMeshProperties.subshape_type = "PROFILE"
if obj is None:
obj = bpy.data.objects.new("Surface", mesh)
else:
obj.data = mesh
for arc in cls.arcs:
group = obj.vertex_groups.new(name="IFCARCINDEX")
group.add(arc, 1, "REPLACE")
for circle in cls.circles:
group = obj.vertex_groups.new(name="IFCCIRCLE")
group.add(circle, 1, "REPLACE")
return obj
@classmethod
def import_curve(cls, obj, position, curve):
offset = len(cls.vertices)
@@ -232,6 +320,10 @@ class Model(blenderbim.core.tool.Model):
cls.vertices.append(global_point)
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices))])
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcCompositeCurve"):
# This is a first pass incomplete implementation only for simple polylines, and misses many details.
for segment in curve.Segments:
cls.import_curve(obj, position, segment.ParentCurve)
elif curve.is_a("IfcIndexedPolyCurve"):
is_arc = False
is_closed = False
+2 -2
View File
@@ -133,8 +133,8 @@ class Project(blenderbim.core.tool.Project):
def set_default_modeling_dimensions(cls):
props = bpy.context.scene.BIMModelProperties
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props.extrusion_depth = 3 / unit_scale
props.length = 1 / unit_scale
props.extrusion_depth = 3
props.length = 1
props.rl1 = 0
props.rl2 = 1 / unit_scale
props.x = 0.5 / unit_scale
-1
View File
@@ -52,7 +52,6 @@ class Pset(blenderbim.core.tool.Pset):
@classmethod
def get_pset_name(cls, obj, obj_type):
pset = cls.get_pset_props(obj, obj_type)
print(pset.pset_name)
return pset.pset_name
@classmethod
+81 -4
View File
@@ -30,6 +30,8 @@ from datetime import datetime
from dateutil import parser
import ifcopenshell.util.date as ifcdateutils
import ifcopenshell.util.cost
import ifcopenshell.util.resource
import blenderbim.bim.schema
class Resource(blenderbim.core.tool.Resource):
@@ -58,6 +60,7 @@ class Resource(blenderbim.core.tool.Resource):
if not resource.HasContext:
continue
create_new_resource_li(resource, 0)
cls.load_productivity_data()
props.is_editing = True
@classmethod
@@ -299,8 +302,82 @@ class Resource(blenderbim.core.tool.Resource):
@classmethod
def get_highlighted_resource(cls):
return tool.Ifc.get().by_id(
bpy.context.scene.BIMResourceTreeProperties.resources[
bpy.context.scene.BIMResourceProperties.active_resource_index
resources = len(bpy.context.scene.BIMResourceTreeProperties.resources)
if resources and resources > bpy.context.scene.BIMResourceProperties.active_resource_index:
return tool.Ifc.get().by_id(
bpy.context.scene.BIMResourceTreeProperties.resources[
bpy.context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
)
)
@classmethod
def clear_productivity_data(cls, props):
for duration_prop in props.quantity_consumed or []:
if duration_prop.name == "BaseQuantityConsumed":
duration_prop.years = 0
duration_prop.months = 0
duration_prop.days = 0
duration_prop.hours = 0
duration_prop.minutes = 0
duration_prop.seconds = 0
props.quantity_produced = 0
props.quantity_produced_name = ""
@classmethod
def load_productivity_data(cls):
duration_props = None
for collection_prop in bpy.context.scene.BIMResourceProductivity.quantity_consumed:
duration_props = collection_prop if collection_prop.name == "BaseQuantityConsumed" else None
break
if not duration_props:
duration_props = bpy.context.scene.BIMResourceProductivity.quantity_consumed.add()
duration_props.name = "BaseQuantityConsumed"
cls.clear_productivity_data(bpy.context.scene.BIMResourceProductivity)
current_resource = tool.Resource.get_highlighted_resource()
if current_resource:
productivity = cls.get_productivity(current_resource)
if productivity:
bpy.context.scene.BIMResourceProductivity.quantity_produced = (
ifcopenshell.util.resource.get_quantity_produced(productivity)
)
bpy.context.scene.BIMResourceProductivity.quantity_produced_name = (
ifcopenshell.util.resource.get_quantity_produced_name(productivity)
)
time_consumed = ifcopenshell.util.resource.get_unit_consumed(productivity)
if time_consumed:
durations_attributes = helper.parse_duration_as_blender_props(time_consumed)
duration_props.years = durations_attributes["years"]
duration_props.months = durations_attributes["months"]
duration_props.days = durations_attributes["days"]
duration_props.hours = durations_attributes["hours"]
duration_props.minutes = durations_attributes["minutes"]
duration_props.seconds = durations_attributes["seconds"]
@classmethod
def get_productivity_attributes(cls):
props = bpy.context.scene.BIMResourceProductivity
productivity = {}
if props.quantity_consumed:
productivity["BaseQuantityConsumed"] = helper.blender_props_to_iso_duration(
props.quantity_consumed, "ELAPSEDTIME", "BaseQuantityConsumed"
)
productivity["BaseQuantityProducedValue"] = props.quantity_produced
productivity["BaseQuantityProducedName"] = props.quantity_produced_name
return productivity
@classmethod
def get_productivity(cls, resource, should_inherit=False):
return ifcopenshell.util.resource.get_productivity(resource, should_inherit=should_inherit)
@classmethod
def edit_productivity_pset(cls, resource, attributes):
productivity = cls.get_productivity(resource)
if productivity:
pset = tool.Ifc.get().by_id(productivity["id"])
else:
pset = tool.Ifc.run("pset.add_pset", product=resource, name="EPset_Productivity")
tool.Ifc.run(
"pset.edit_pset",
pset=pset,
properties=attributes,
)
+2 -2
View File
@@ -119,8 +119,8 @@ class Root(blenderbim.core.tool.Root):
return representation.ContextOfItems
@classmethod
def is_opening_element(cls, element):
return element.is_a("IfcOpeningElement")
def is_element_a(cls, element, ifc_class):
return element.is_a(ifc_class)
@classmethod
def link_object_data(cls, source_obj, destination_obj):
+139 -57
View File
@@ -27,6 +27,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
import json
import blenderbim.core.tool
import blenderbim.core
import blenderbim.tool as tool
import blenderbim.bim.helper
import blenderbim.bim.module.sequence.helper as helper
@@ -132,7 +133,7 @@ class Sequence(blenderbim.core.tool.Sequence):
props.editing_type = "TASKS"
@classmethod
def create_task_tree(cls, work_schedule):
def load_task_tree(cls, work_schedule):
bpy.context.scene.BIMTaskTreeProperties.tasks.clear()
props = bpy.context.scene.BIMWorkScheduleProperties
cls.contracted_tasks = json.loads(props.contracted_tasks)
@@ -143,10 +144,30 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def get_sorted_tasks_ids(cls, tasks):
cls.sort_keys = {task.id(): cls.get_sort_key(task) for task in tasks}
related_object_ids = sorted(cls.sort_keys, key=cls.natural_sort_key)
def get_sort_key(task):
# Sorting only applies to actual tasks, not the WBS
# for rel in task.IsNestedBy:
# for object in rel.RelatedObjects:
# if object.is_a("IfcTask"):
# return "0000000000" + (task.Identification or "")
column_type, name = bpy.context.scene.BIMWorkScheduleProperties.sort_column.split(".")
if column_type == "IfcTask":
return task.get_info(task)[name] or ""
elif column_type == "IfcTaskTime" and task.TaskTime:
return task.TaskTime.get_info(task)[name]
return task.Identification or ""
def natural_sort_key(i, _nsre=re.compile("([0-9]+)")):
s = sort_keys[i]
return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]
if bpy.context.scene.BIMWorkScheduleProperties.sort_column:
sort_keys = {task.id(): get_sort_key(task) for task in tasks}
related_object_ids = sorted(sort_keys, key=natural_sort_key)
else:
related_object_ids = [task.id() for task in tasks]
if bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed:
return related_object_ids.reverse()
related_object_ids.reverse()
return related_object_ids
@classmethod
@@ -162,27 +183,6 @@ class Sequence(blenderbim.core.tool.Sequence):
for related_object_id in cls.get_sorted_tasks_ids(ifcopenshell.util.sequence.get_nested_tasks(task)):
cls.create_new_task_li(related_object_id, level_index + 1)
@classmethod
def natural_sort_key(cls, i, _nsre=re.compile("([0-9]+)")):
s = cls.sort_keys[i]
return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]
@classmethod
def get_sort_key(cls, task):
# Sorting only applies to actual tasks, not the WBS
for rel in task.IsNestedBy:
for object in rel.RelatedObjects:
if object.is_a("IfcTask"):
return "0000000000" + (task.Identification or "")
if not bpy.context.scene.BIMWorkScheduleProperties.sort_column:
return task.Identification or ""
column_type, name = bpy.context.scene.BIMWorkScheduleProperties.sort_column.split(".")
if column_type == "IfcTask":
return task.Name or ""
elif column_type == "IfcTaskTime" and task.TaskTime:
return task.TaskTime.Name or ""
return task.Identification or ""
@classmethod
def load_task_properties(cls, task=None):
def canonicalise_time(time):
@@ -225,7 +225,7 @@ class Sequence(blenderbim.core.tool.Sequence):
else "-"
)
item.duration = (
isodate.duration_isoformat(ifcopenshell.util.date.ifc2datetime(task_time.ScheduleDuration))
str(ifcopenshell.util.date.readable_ifc_duration(task_time.ScheduleDuration))
if task_time.ScheduleDuration
else "-"
)
@@ -243,7 +243,7 @@ class Sequence(blenderbim.core.tool.Sequence):
item.finish = "-"
item.duration = "-"
bpy.context.scene.BIMWorkScheduleProperties.is_task_update_enabled = True
props.is_task_update_enabled = True
@classmethod
def get_active_work_schedule(cls):
@@ -330,7 +330,7 @@ class Sequence(blenderbim.core.tool.Sequence):
blenderbim.bim.helper.import_attributes2(task, props.task_attributes)
@classmethod
def enable_editing_task(cls, task):
def enable_editing_task_attributes(cls, task):
props = bpy.context.scene.BIMWorkScheduleProperties
props.active_task_id = task.id()
props.editing_task_type = "ATTRIBUTES"
@@ -394,7 +394,7 @@ class Sequence(blenderbim.core.tool.Sequence):
return True
else:
duration_type = attributes["DurationType"] if "DurationType" in attributes else None
time_split_iso_duration = helper.simplify_duration(
time_split_iso_duration = helper.blender_props_to_iso_duration(
props.durations_attributes, duration_type, prop.name
)
attributes[prop.name] = time_split_iso_duration
@@ -409,16 +409,15 @@ class Sequence(blenderbim.core.tool.Sequence):
def load_task_resources(cls, resources):
props = bpy.context.scene.BIMWorkScheduleProperties
props.task_resources.clear()
if resources:
for resource in resources:
new = props.task_resources.add()
new.ifc_definition_id = resource.id()
new.name = resource.Name or "Unnamed"
new.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0
for resource in resources or []:
new = props.task_resources.add()
new.ifc_definition_id = resource.id()
new.name = resource.Name or "Unnamed"
new.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0
@classmethod
def load_resources(cls):
bpy.ops.bim.load_resources() # remove and refactor
blenderbim.core.resource.load_resources(tool.Resource)
@classmethod
def get_task_inputs(cls, task):
@@ -430,10 +429,22 @@ class Sequence(blenderbim.core.tool.Sequence):
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
@classmethod
def are_entities_same_class(cls, entities):
if not entities:
return False
if len(entities) == 1:
return True
first = entities[0]
for entity in entities:
if entity.is_a() != first.is_a():
return False
return True
@classmethod
def get_task_resources(cls, task):
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_resources
return ifcopenshell.util.sequence.get_task_resource(task, is_deep)
return ifcopenshell.util.sequence.get_task_resources(task, is_deep)
@classmethod
def load_task_inputs(cls, inputs):
@@ -734,6 +745,8 @@ class Sequence(blenderbim.core.tool.Sequence):
def remove_task_column(cls, name):
props = bpy.context.scene.BIMWorkScheduleProperties
props.columns.remove(props.columns.find(name))
if props.sort_column == name:
props.sort_column = ""
@classmethod
def set_task_sort_column(cls, column):
@@ -781,7 +794,7 @@ class Sequence(blenderbim.core.tool.Sequence):
bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks = json.dumps(contracted_tasks)
expand_ancestors(parent_task)
work_schedule = cls.get_active_work_schedule()
cls.create_task_tree(work_schedule)
cls.load_task_tree(work_schedule)
cls.load_task_properties()
task_props = bpy.context.scene.BIMTaskTreeProperties
@@ -1158,7 +1171,6 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def show_snapshot(cls, product_states):
print(product_states)
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 2
for obj in bpy.data.objects:
@@ -1299,7 +1311,6 @@ class Sequence(blenderbim.core.tool.Sequence):
def clear_object_animation(cls, obj):
if obj.animation_data:
obj.animation_data_clear()
print("Cleared animation for", obj.name)
@classmethod
def clear_objects_animation(cls, include_blender_objects=True):
@@ -1462,32 +1473,69 @@ class Sequence(blenderbim.core.tool.Sequence):
"USERDEFINED": "FS",
"NOTDEFINED": "FS",
}
is_baseline = False
if work_schedule.PredefinedType == "BASELINE":
is_baseline = True
relating_work_schedule = work_schedule.IsDeclaredBy[0].RelatingObject
work_schedule = relating_work_schedule
tasks_json = []
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
cls.create_new_task_json(task, tasks_json, sequence_type_map)
if is_baseline:
cls.create_new_task_json(task, tasks_json, sequence_type_map, baseline_schedule=work_schedule)
else:
cls.create_new_task_json(task, tasks_json, sequence_type_map)
return tasks_json
@classmethod
def create_new_task_json(cls, task, json, type_map=None):
def create_new_task_json(cls, task, json, type_map=None, baseline_schedule=None):
task_time = task.TaskTime
resources = ifcopenshell.util.sequence.get_task_resources(task, is_deep=False)
string_resources = ""
resources_usage = ""
for resource in resources:
string_resources += resource.Name + ", "
resources_usage += str(resource.Usage.ScheduleUsage) + ", " if resource.Usage else "-, "
schedule_start = task_time.ScheduleStart if task_time else ""
schedule_finish = task_time.ScheduleFinish if task_time else ""
baseline_task = None
if baseline_schedule:
for rel in task.Declares:
for baseline_task in rel.RelatedObjects:
if baseline_schedule.id() == ifcopenshell.util.sequence.get_task_work_schedule(baseline_task).id():
baseline_task = task
break
if baseline_task and baseline_task.TaskTime:
compare_start = baseline_task.TaskTime.ScheduleStart
compare_finish = baseline_task.TaskTime.ScheduleFinish
else:
compare_start = schedule_start
compare_finish = schedule_finish
task_name = task.Name or "Unnamed"
task_name = task_name.replace('\n', "")
data = {
"pID": task.id(),
"pName": task.Name,
"pCaption": task.Name,
"pStart": task_time.ScheduleStart if task_time else "",
"pEnd": task_time.ScheduleFinish if task_time else "",
"pPlanStart": task_time.ScheduleStart if task_time else "",
"pPlanEnd": task_time.ScheduleFinish if task_time else "",
"pName": task_name,
"pCaption": task_name,
"pStart": schedule_start,
"pEnd": schedule_finish ,
"pPlanStart": compare_start,
"pPlanEnd": compare_finish,
"pMile": 1 if task.IsMilestone else 0,
"pRes": string_resources,
"pComp": 0,
"pGroup": 1 if task.IsNestedBy else 0,
"pParent": task.Nests[0].RelatingObject.id() if task.Nests else 0,
"pOpen": 1,
"pCost": 1,
"ifcduration": task_time.ScheduleDuration if task_time else "",
"ifcduration": str(ifcopenshell.util.date.ifc2datetime(task_time.ScheduleDuration))
if (task_time and task_time.ScheduleDuration)
else "",
"resourceUsage": resources_usage,
}
if task_time and task_time.IsCritical:
data["pClass"] = "gtaskred"
elif data["pGroup"]:
@@ -1502,22 +1550,32 @@ class Sequence(blenderbim.core.tool.Sequence):
)
json.append(data)
for nested_task in ifcopenshell.util.sequence.get_nested_tasks(task):
cls.create_new_task_json(nested_task, json, type_map)
cls.create_new_task_json(nested_task, json, type_map, baseline_schedule)
@classmethod
def generate_gantt_browser_chart(cls, task_json):
def generate_gantt_browser_chart(cls, task_json, work_schedule):
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f:
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t:
f.write(pystache.render(t.read(), {"json_data": json.dumps(task_json)}))
f.write(
pystache.render(
t.read(), {"json_data": json.dumps(task_json), "data": json.dumps(work_schedule.get_info())}
)
)
webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"))
@classmethod
def load_product_tasks(cls, product):
def is_filter_by_active_schedule(cls):
return bpy.context.scene.BIMWorkScheduleProperties.filter_by_active_schedule
@classmethod
def get_tasks_for_product(cls, product, work_schedule=None):
return ifcopenshell.util.sequence.get_tasks_for_product(product, work_schedule)
@classmethod
def load_product_related_tasks(cls, task_inputs, task_ouputs):
props = bpy.context.scene.BIMWorkScheduleProperties
props.is_task_update_enabled = False
props.product_input_tasks.clear()
props.product_output_tasks.clear()
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
for task in task_inputs or []:
new = props.product_input_tasks.add()
new.name = task.Name or "Unnamed"
@@ -1526,3 +1584,27 @@ class Sequence(blenderbim.core.tool.Sequence):
new = props.product_output_tasks.add()
new.name = task.Name or "Unnamed"
new.ifc_definition_id = task.id()
@classmethod
def get_work_schedule_products(cls, work_schedule):
products = []
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_deep=True))
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True))
return products
@classmethod
def has_task_assignments(cls, product, work_schedule=None):
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
if work_schedule:
task_inputs = [task for task in task_inputs or [] if cls.get_work_schedule(task) == work_schedule]
task_ouputs = [task for task in task_ouputs or [] if cls.get_work_schedule(task) == work_schedule]
return bool(task_inputs or task_ouputs)
@classmethod
def is_sorting_enabled(cls):
return bpy.context.scene.BIMWorkScheduleProperties.sort_column
@classmethod
def is_sort_reversed(cls):
return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed
+27 -29
View File
@@ -51,37 +51,35 @@ class Unit(blenderbim.core.tool.Unit):
@classmethod
def get_scene_unit_name(cls, unit_type):
props = bpy.context.scene.unit_settings
is_metric = props.system == "METRIC" or props.system == "NONE"
if is_metric:
name = "METRE"
elif props.length_unit == "MILES":
name = "mile"
elif props.length_unit == "FEET" or props.length_unit == "ADAPTIVE":
name = "foot"
elif props.length_unit == "INCHES":
name = "inch"
elif props.length_unit == "THOU":
name = "thou"
if unit_type == "length":
return name
elif unit_type == "area":
if is_metric:
return f"SQUARE_{name}"
return f"square {name}"
elif unit_type == "volume":
if is_metric:
return f"CUBIC_{name}"
return f"cubic {name}"
if unit_type == "LENGTHUNIT":
props = bpy.context.scene.unit_settings
if props.length_unit == "MILES":
return "mile"
elif props.length_unit == "FEET" or props.length_unit == "ADAPTIVE":
return "foot"
elif props.length_unit == "INCHES":
return "inch"
elif props.length_unit == "THOU":
return "thou"
return "foot"
elif unit_type == "AREAUNIT":
return bpy.context.scene.BIMProperties.area_unit
elif unit_type == "VOLUMEUNIT":
return bpy.context.scene.BIMProperties.volume_unit
@classmethod
def get_scene_unit_si_prefix(cls):
props = bpy.context.scene.unit_settings
if props.length_unit == "ADAPTIVE" or props.length_unit == "METERS":
return
return props.length_unit.replace("METERS", "")
def get_scene_unit_si_prefix(cls, unit_type):
if unit_type == "LENGTHUNIT":
props = bpy.context.scene.unit_settings
if props.length_unit == "ADAPTIVE" or props.length_unit == "METERS":
return
return props.length_unit.replace("METERS", "")
elif unit_type == "AREAUNIT":
unit = bpy.context.scene.BIMProperties.area_unit
elif unit_type == "VOLUMEUNIT":
unit = bpy.context.scene.BIMProperties.volume_unit
if "/" in unit:
return unit.split("/")[0]
@classmethod
def import_unit_attributes(cls, unit):
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
# Wrapper script to launch BlenderBIM.
# If the `blender` executable can't be found in your PATH environment, set
# BLENDER_EXE to the location of your preferred `blender` executable.
#BLENDER_EXE=/opt/blender-3.3/blender
: ${BLENDER_EXE:=blender}
if [[ $# -eq 1 && -f "$@" ]]; then
"${BLENDER_EXE}" --python-expr 'import bpy; bpy.data.collections.remove(bpy.data.collections[0]); bpy.ops.bim.load_project(filepath="'"$@"'")'
else
"${BLENDER_EXE}" --python-expr 'import bpy; bpy.data.collections.remove(bpy.data.collections[0]); bpy.ops.bim.create_project()'
fi;
+8 -2
View File
@@ -21,7 +21,8 @@ Add-on compatibility
The BlenderBIM Add-on is a non-trivial add-on. By turning Blender into a
graphical front-end to a native IFC authoring platform, some fundamental Blender
features (such as object deletion and object duplication) have been patched.
features (such as hotkeys for basic functionality like object deletion or
duplication) have been patched and many dependencies have been introduced.
Other add-ons may no longer work as intended when the BlenderBIM Add-on is
enabled, or vice versa, the BlenderBIM Add-on may no longer work as intended
@@ -29,7 +30,12 @@ when other add-ons are enabled.
Known scenarios which will lead to add-on incompatibility include:
- The add-on overrides object deletion or duplication
- The add-on also overrides the same hotkeys. For example, if an add-on
overrides the "X" key to delete an object, you will need to manually trigger
(either via menu or custom hotkey) the BlenderBIM Add-on equivalent operator
(e.g. IFC Delete).
- The add-on uses object deletion or duplication macros with dictionary
override. Note that this is also deprecated in Blender, so the other add-on
should be updated to fix this.
- The add-on requires a conflicting dependency, or a conflicting version of the
same dependency. Neither add-on may work simultaneously.

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