Compare commits

...

234 Commits

Author SHA1 Message Date
Jesusbill 07745aa6bc Fix #2998 2023-04-27 13:26:47 +02:00
Jesusbill 45afaba63e Add face creation when adding representation for surface structural items #2999 2023-04-27 09:54:51 +02:00
Andrej730 ee2f18349f WALL_MOUNTED_HANDRAIL type for Railing modifier
Added WALL_MOUNTED_HANDRAIL type for railing modifier. It's still a bit work in progress - still need to add support for different railing termination types.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The main differences are 2d representation (https://i.imgur.com/VyEH569.png) and the door panel being placed before the lining, not after
2023-03-30 12:41:34 +05:00
173 changed files with 32188 additions and 28098 deletions
@@ -4,6 +4,7 @@ on:
push:
paths:
- .github/workflows/ci-ifcopenshell-conda-daily.yml
- conda/**
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
@@ -53,11 +54,13 @@ jobs:
sudo mv -v MacOSX10.13.sdk /opt/ && \
ls /opt/
- uses: seanmiddleditch/gha-setup-ninja@master
- uses: conda-incubator/setup-miniconda@v2 # https://github.com/conda-incubator/setup-miniconda
- uses: mamba-org/provision-with-micromamba@main # https://github.com/mamba-org/provision-with-micromamba
with:
activate-environment: conda-build
python-version: ${{ matrix.pyver.distver }}
channels: conda-forge
channel-priority: strict
environment-file: conda/environment.yml
extra-specs: |
python=${{ matrix.pyver.distver }}
- name: build, test and upload ifcopenshell
if: ${{ matrix.platform.upload == 'true' }}
run: |
+1 -2
View File
@@ -10,5 +10,4 @@ dependencies:
- ripgrep
- pip
- pip:
- --extra-index-url https://lief.s3-website.fr-par.scw.cloud/latest
- lief==0.13.0.dev0
- lief==0.13
+1 -1
View File
@@ -36,7 +36,7 @@ requirements:
run:
- python
- boost-cpp
- occt
- {{ pin_compatible('occt', max_pin='x.x.x') }}
- libxml2
- cgal-cpp
- hdf5
+4
View File
@@ -59,6 +59,10 @@ import multiprocessing
import platform
import sysconfig
# @todo temporary for expired mpfr.org certificate on 2023-04-08
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
from urllib.request import urlretrieve
+5
View File
@@ -236,6 +236,11 @@ endif
cp -r dist/working/IfcOpenShell-0.7.0/src/ifc5d/ifc5d dist/blenderbim/libs/site/packages/
# Provides IFCCityJSON functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccityjson dist/blenderbim/libs/site/packages/
# Provides IfcGit functionality
cd dist/working && . env/bin/activate && $(PIP) install GitPython --target=./site-packages
cp -r dist/working/site-packages/smmap dist/blenderbim/libs/site/packages/
cp -r dist/working/site-packages/gitdb dist/blenderbim/libs/site/packages/
cp -r dist/working/site-packages/git dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides Mustache templating in construction documentation
+7 -1
View File
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim
import importlib
from . import handler, ui, prop, operator, helper
@@ -70,6 +71,7 @@ modules = {
"covetool": None,
"augin": None,
"debug": None,
"ifcgit": None,
# Uncomment this line to enable loading of the demo module. Happy hacking!
# The name "demo" must correlate to a folder name in `bim/module/`.
# "demo": None,
@@ -171,7 +173,11 @@ def register():
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(handler.setDefaultProperties)
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
bpy.app.handlers.save_post.remove(handler.ensureIfcExported)
@@ -22,6 +22,7 @@
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; font-size: 4.13px; }
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }
@@ -96,4 +96,12 @@
<path d="M 7.5 10 L 10 0 L 15 10" class="annotation" style="stroke-width:1; fill: white;" />
</g>
</marker>
<filter x="0" y="0" width="1" height="1" id="fill-background">
<feFlood flood-color="white" result="bg" />
<feMerge>
<feMergeNode in="bg"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

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

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 69 KiB

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="rectangle-tag">
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
</g>
<g id="triangle-tag">
<path style="fill: white; stroke: black; stroke-width: 0.25;" d="M 3.1409425e-7,-7.1069207 5.7650623,2.8784603 l -11.5301246,-6e-7 z" />
</g>
<g id="hexagon-tag">
<path style="fill: white; stroke: black; stroke-width: 0.25;" d="m 0.0161133,-6.0583333 5.2339915,3.8366023 -2.031433,6.1633977 c 0,0 -6.4894866,-0.027414 -6.4894866,-0.027414 l -1.97929,-6.1803395 5.2662181,-3.7922467" />
</g>
<g id="capsule-tag">
<rect x="-6" y="-2.5" width="12" height="5" rx="2.5" ry="2.5" fill="white" stroke="black" style="stroke-width: 0.25;" />
</g>
<g id="circle-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
</g>
<g id="door-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<text y="-1.75" class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<line x1="-5" x2="5" style="stroke: black; stroke-width: 0.25;" />
<text y="2.5" class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
</g>
<g id="space-tag">
<text y="-5" class="large" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<text class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<text y="5" class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
</g>
<g id="elevation-arrow">
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
</g>
<g id="elevation-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
</g>
<g id="section-arrow">
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
</g>
<g id="section-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,20 +5,24 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#6=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#14=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#15=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#16=IFCSIMPLEPROPERTYTEMPLATE('1$xfo9EVb26QLqmPll2_RK',$,'MetricPrecision','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
#17=IFCSIMPLEPROPERTYTEMPLATE('38uAtrp9nD_901NO42zd$7',$,'ImperialPrecision','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#18=IFCSIMPLEPROPERTYTEMPLATE('1MX0uffTL6TOvtvEJpxFmk',$,'DecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#19=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -5,18 +5,30 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#6,#7));
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#6,#7));
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#9,#10,#11,#12,#13));
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#9,#10,#11,#12,#13));
#9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('2$6U0mLI9AiPRWeBabdY3u',$,'EndArrowSymbol','Custom symbol for the end of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('1naFqntIL7igCY7hCaE7kq',$,'HasConnectedSectionLine','Connect or disconnect section markers with line (by default = True).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22));
#15=IFCSIMPLEPROPERTYTEMPLATE('0ulAhgk3v9qfGlDILauR6J',$,'SheetsDir','Default sheets directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#16=IFCSIMPLEPROPERTYTEMPLATE('2yvlVKiQXASucfH40deCvu',$,'LayoutsDir','Default layouts directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#17=IFCSIMPLEPROPERTYTEMPLATE('2kXZqXicL3jRwsOnLM_0ho',$,'TitleblocksDir','Default titleblocks directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#18=IFCSIMPLEPROPERTYTEMPLATE('1lmOfo9Bf9beMBJkCZKf5Y',$,'DrawingsDir','Default drawings directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#19=IFCSIMPLEPROPERTYTEMPLATE('30cOKpb4D9CADKM04$N9X3',$,'StylesheetPath','Default stylesheet CSS',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#20=IFCSIMPLEPROPERTYTEMPLATE('23tejOrxj859R_eCRp1AFP',$,'MarkersPath','Default markers SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=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.);
ENDSEC;
END-ISO-10303-21;
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="rectangle-tag">
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
</g>
<g id="door-tag">
<circle cy="1.75" r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<text text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<line x1="-5" y1="1.75" x2="5" y2="1.75" style="stroke: black; stroke-width: 0.25;" />
<text y="5" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
</g>
<g id="elevation-arrow">
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
</g>
<g id="elevation-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
</g>
<g id="section-arrow">
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
</g>
<g id="section-tag">
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

+1 -19
View File
@@ -45,7 +45,6 @@ class IfcExporter:
self.set_header()
IfcStore.update_cache()
if bpy.context.scene.BIMProjectProperties.is_authoring:
self.sync_deletions()
self.sync_all_objects()
self.sync_edited_objects()
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
@@ -87,19 +86,6 @@ class IfcExporter:
self.get_application_name(), self.get_application_version()
)
def sync_deletions(self):
results = []
for ifc_definition_id in IfcStore.deleted_ids:
try:
product = self.file.by_id(ifc_definition_id)
if hasattr(product, "GlobalId"):
results.append(product.GlobalId)
except:
continue
ifcopenshell.api.run("root.remove_product", self.file, **{"product": product})
IfcStore.deleted_ids.clear()
return results
def sync_all_objects(self):
results = []
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -126,7 +112,7 @@ class IfcExporter:
continue
try:
if isinstance(obj, bpy.types.Material):
if self.has_changed_shading(obj):
if tool.Ifc.has_changed_shading(obj):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj)
else:
element = tool.Ifc.get_entity(obj)
@@ -149,10 +135,6 @@ class IfcExporter:
checksum = obj.data.BIMMeshProperties.material_checksum
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
def has_changed_shading(self, obj):
checksum = obj.BIMMaterialProperties.shading_checksum
return checksum != repr(np.array(obj.diffuse_color).tobytes())
def sync_object_placement(self, obj):
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.is_moved(obj):
+9 -8
View File
@@ -105,16 +105,16 @@ def active_material_index_callback(obj, data):
refresh_ui_data()
def subscribe_to(object, data_path, callback):
def subscribe_to(obj, data_path, callback):
try:
subscribe_to = object.path_resolve(data_path, False)
subscribe_to = obj.path_resolve(data_path, False)
except:
return
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=object,
owner=obj,
args=(
object,
obj,
data_path,
),
notify=callback,
@@ -235,6 +235,11 @@ def setDefaultProperties(scene):
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"
@@ -297,8 +302,4 @@ def setDefaultProperties(scene):
RasterStyleProperty.OVERLAY_SHOW_RELATIONSHIP_LINES.value: False,
}
)
drawing_style = bpy.context.scene.DocProperties.drawing_styles.add()
drawing_style.name = "Blender Default"
drawing_style.render_type = "DEFAULT"
bpy.ops.bim.save_drawing_style(index="2")
AuthoringData.type_thumbnails = {}
+2 -20
View File
@@ -24,6 +24,7 @@ import zipfile
import tempfile
import ifcopenshell
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
@@ -35,7 +36,6 @@ class IfcStore:
cache_path = None
id_map = {}
guid_map = {}
deleted_ids = set()
edited_objs = set()
pset_template_path = ""
pset_template_file = None
@@ -62,7 +62,6 @@ class IfcStore:
IfcStore.cache_path = None
IfcStore.id_map = {}
IfcStore.guid_map = {}
IfcStore.deleted_ids = set()
IfcStore.edited_objs = set()
IfcStore.pset_template_path = ""
IfcStore.pset_template_file = None
@@ -277,23 +276,6 @@ class IfcStore:
data = {"id": element.id(), "obj": obj.name}
IfcStore.commit_link_element(data)
@staticmethod
def delete_element(element):
IfcStore.deleted_ids.add(element.id())
if IfcStore.history:
data = {"id": element.id()}
IfcStore.history[-1]["operations"].append(
{"rollback": IfcStore.rollback_delete_element, "commit": IfcStore.commit_delete_element, "data": data}
)
@staticmethod
def rollback_delete_element(data):
IfcStore.deleted_ids.remove(data["id"])
@staticmethod
def commit_delete_element(data):
IfcStore.deleted_ids.add(data["id"])
@staticmethod
def link_element(element, obj):
existing_obj = IfcStore.id_map.get(element.id(), None)
@@ -371,7 +353,7 @@ class IfcStore:
def unlink_element(element=None, obj=None):
if element is None:
try:
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
element = tool.Ifc.get_entity(obj)
except:
pass
+11 -74
View File
@@ -60,7 +60,6 @@ class MaterialCreator:
def create(self, element, obj, mesh):
self.mesh = mesh
self.obj = obj
if (hasattr(element, "Representation") and not element.Representation) or (
hasattr(element, "RepresentationMaps") and not element.RepresentationMaps
):
@@ -140,11 +139,11 @@ class MaterialCreator:
def assign_material_slots_to_faces(self):
if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]:
return
if len(self.obj.material_slots) == 1:
if len(self.mesh.materials) == 1:
return
material_to_slot = {}
for i, material in enumerate(self.mesh["ios_materials"]):
slot_index = self.obj.material_slots.find(self.styles[material].name)
slot_index = self.mesh.materials.find(self.styles[material].name)
material_to_slot[i] = slot_index
if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
@@ -242,6 +241,8 @@ class IfcImporter:
self.profile_code("Create native elements")
self.create_elements()
self.profile_code("Create elements")
self.create_generic_elements(self.annotations)
self.profile_code("Create annotations")
self.create_grids()
self.profile_code("Create grids")
self.create_spatial_elements()
@@ -321,10 +322,13 @@ class IfcImporter:
if isinstance(self.elements, set):
self.elements = list(self.elements)
# TODO: enable filtering for annotations
self.annotations = set(self.file.by_type("IfcAnnotation"))
self.annotations = set([a for a in self.file.by_type("IfcAnnotation") if not a.HasAssignments])
else:
self.elements = self.file.by_type("IfcElement")
self.annotations = set(self.file.by_type("IfcAnnotation"))
if self.file.schema in ("IFC2X3", "IFC4"):
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
else:
self.elements = self.file.by_type("IfcElement")
self.annotations = set([a for a in self.file.by_type("IfcAnnotation") if not a.HasAssignments])
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
if self.ifc_import_settings.is_coordinating:
@@ -335,23 +339,7 @@ class IfcImporter:
if self.ifc_import_settings.has_filter or offset or offset_limit < len(self.elements):
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
if self.file.schema == "IFC2X3":
self.element_types = set(
self.file.by_type("IfcElementType")
+ self.file.by_type("IfcDoorStyle")
+ self.file.by_type("IfcWindowStyle")
)
elif self.file.schema == "IFC4":
self.element_types = set(
self.file.by_type("IfcElementType")
+ self.file.by_type("IfcDoorStyle")
+ self.file.by_type("IfcWindowStyle")
+ self.file.by_type("IfcSpatialElementType")
)
else:
self.element_types = set(
self.file.by_type("IfcElementType") + self.file.by_type("IfcSpatialElementType")
)
self.element_types = set(self.file.by_type("IfcTypeProduct"))
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
@@ -921,9 +909,6 @@ class IfcImporter:
if mesh:
pass
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
mesh = self.create_camera(element, shape)
tool.Loader.link_mesh(shape, mesh)
elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element) and shape:
mesh = self.create_curve(element, shape)
tool.Loader.link_mesh(shape, mesh)
@@ -1834,54 +1819,6 @@ class IfcImporter:
):
return representation.Items[0].MappingTarget
def create_camera(self, element, shape):
if hasattr(shape, "geometry"):
geometry = shape.geometry
else:
geometry = shape
v = geometry.verts
x = [v[i] for i in range(0, len(v), 3)]
y = [v[i + 1] for i in range(0, len(v), 3)]
z = [v[i + 2] for i in range(0, len(v), 3)]
width = max(x) - min(x)
height = max(y) - min(y)
depth = max(z) - min(z)
camera = bpy.data.cameras.new(tool.Loader.get_mesh_name(geometry))
camera.type = "ORTHO"
camera.ortho_scale = width if width > height else height
camera.clip_end = depth
if width > height:
camera.BIMCameraProperties.raster_x = 1000
camera.BIMCameraProperties.raster_y = round(1000 * (height / width))
else:
camera.BIMCameraProperties.raster_x = round(1000 * (width / height))
camera.BIMCameraProperties.raster_y = 1000
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("EPset_Drawing")
if pset:
if "TargetView" in pset:
camera.BIMCameraProperties.target_view = pset["TargetView"]
if "Scale" in pset:
valid_scales = [
i[0] for i in get_diagram_scales(None, bpy.context) if pset["Scale"] == i[0].split("|")[-1]
]
if valid_scales:
camera.BIMCameraProperties.diagram_scale = valid_scales[0]
else:
camera.BIMCameraProperties.diagram_scale = "CUSTOM"
camera.BIMCameraProperties.custom_diagram_scale = pset["Scale"]
if "HasUnderlay" in pset:
camera.BIMCameraProperties.has_underlay = pset["HasUnderlay"]
if "HasLinework" in pset:
camera.BIMCameraProperties.has_linework = pset["HasLinework"]
if "HasAnnotation" in pset:
camera.BIMCameraProperties.has_annotation = pset["HasAnnotation"]
return camera
def create_curve(self, element, shape):
if hasattr(shape, "geometry"):
geometry = shape.geometry
@@ -59,9 +59,13 @@ class MaterialAttributesData:
@classmethod
def load(cls):
cls.data = {"attributes": cls.attributes()}
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def ifc_definition_id(cls):
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
@classmethod
def attributes(cls):
results = []
@@ -50,7 +50,20 @@ class EnableEditingAttributes(bpy.types.Operator):
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
props.attributes.clear()
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes)
def callback(name, prop, data):
if name in ("RefLatitude", "RefLongitude"):
new = props.attributes.add()
new.name = name
new.is_null = data[name] is None
new.is_optional = True
new.data_type = "string"
new.ifc_class = data["type"]
new.string_value = "" if new.is_null else json.dumps(data[name])
blenderbim.bim.helper.import_attributes2(
tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback
)
props.is_editing_attributes = True
return {"FINISHED"}
@@ -87,7 +100,19 @@ class EditAttributes(bpy.types.Operator, Operator):
obj = bpy.data.materials.get(self.obj)
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
attributes = blenderbim.bim.helper.export_attributes(props.attributes)
def callback(attributes, prop):
if prop.name in ("RefLatitude", "RefLongitude"):
if prop.is_null:
attributes[prop.name] = None
else:
try:
attributes[prop.name] = json.loads(prop.string_value)
except:
attributes[prop.name] = None
return True
attributes = blenderbim.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
return {"FINISHED"}
@@ -94,4 +94,10 @@ class BIM_PT_material_attributes(Panel):
def draw(self, context):
if not MaterialAttributesData.is_loaded:
MaterialAttributesData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialAttributesData.data["ifc_definition_id"]
):
MaterialAttributesData.load()
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
@@ -218,7 +218,7 @@ class EditBcfTopicName(bpy.types.Operator):
props = context.scene.BCFProperties
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = bcfxml.topics[blender_topic.name]
topic = bcfxml.topics[blender_topic.name].topic
topic.title = blender_topic.title
return {"FINISHED"}
@@ -233,7 +233,7 @@ class EditBcfTopic(bpy.types.Operator):
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = bcfxml.topics[blender_topic.name]
topic = bcfxml.topics[blender_topic.name].topic
topic.title = blender_topic.title or None
topic.priority = blender_topic.priority or None
topic.due_date = blender_topic.due_date or None
@@ -940,7 +940,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
context.area.type = old
for global_id in exception_global_ids:
obj = IfcStore.get_element(global_id)
if obj:
if obj and bpy.context.view_layer.objects.get(obj.name):
obj.hide_set(True)
else:
objs = []
@@ -119,7 +119,7 @@ class Loader:
class LoadProjectSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.load_project_space_boundaries"
bl_label = "Load all project space boundaries"
bl_label = "Load All Project Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -131,7 +131,7 @@ class LoadProjectSpaceBoundaries(bpy.types.Operator):
class LoadBoundary(bpy.types.Operator):
bl_idname = "bim.load_boundary"
bl_label = "Load boundary"
bl_label = "Load Boundary"
bl_options = {"REGISTER", "UNDO"}
boundary_id: bpy.props.IntProperty()
@@ -147,7 +147,7 @@ class LoadBoundary(bpy.types.Operator):
class LoadSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.load_space_boundaries"
bl_label = "Load selected space boundaries"
bl_label = "Load Selected Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -168,7 +168,7 @@ def get_element_boundaries(element):
class SelectRelatedElementBoundaries(bpy.types.Operator):
bl_idname = "bim.select_related_element_boundaries"
bl_label = "Select related element space boundaries"
bl_label = "Select Related Element Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
related_element: bpy.props.IntProperty()
@@ -185,7 +185,7 @@ class SelectRelatedElementBoundaries(bpy.types.Operator):
class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
bl_idname = "bim.select_related_element_type_boundaries"
bl_label = "Select related element type space boundaries"
bl_label = "Select Related Element Type Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
related_element: bpy.props.IntProperty()
@@ -208,7 +208,7 @@ class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
class SelectSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.select_space_boundaries"
bl_label = "Select all space boundaries"
bl_label = "Select All Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -224,7 +224,7 @@ class SelectSpaceBoundaries(bpy.types.Operator):
class SelectProjectBoundaries(bpy.types.Operator):
bl_idname = "bim.select_project_space_boundaries"
bl_label = "Select all project space boundaries"
bl_label = "Select All Project Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -260,7 +260,7 @@ def get_colour(ifc_boundary):
class ColourByRelatedBuildingElement(bpy.types.Operator):
bl_idname = "bim.colour_by_related_building_element"
bl_label = "Apply colour based on related building elements"
bl_label = "Apply Colour Based on Related Building Elements"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -308,7 +308,7 @@ EDITABLE_ATTRIBUTES = {
class EnableEditingBoundary(bpy.types.Operator):
bl_idname = "bim.enable_editing_boundary"
bl_label = "Edit boundary relations"
bl_label = "Edit Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -327,7 +327,7 @@ class EnableEditingBoundary(bpy.types.Operator):
class DisableEditingBoundary(bpy.types.Operator):
bl_idname = "bim.disable_editing_boundary"
bl_label = "Disable editing boundary relations"
bl_label = "Disable Editing Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -340,7 +340,7 @@ class DisableEditingBoundary(bpy.types.Operator):
class EditBoundaryAttributes(bpy.types.Operator):
bl_idname = "bim.edit_boundary_attributes"
bl_label = "Disable editing boundary relations"
bl_label = "Disable Editing Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -361,7 +361,7 @@ class EditBoundaryAttributes(bpy.types.Operator):
class UpdateBoundaryGeometry(bpy.types.Operator):
bl_idname = "bim.update_boundary_geometry"
bl_label = "Update boundary geometry"
bl_label = "Update Boundary Geometry"
bl_description = """
Update boundary connection geometry from mesh.
Mesh must lie on a single plane. It should look like a face or a face with holes.
@@ -18,7 +18,6 @@
import bpy
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
from bpy.types import PropertyGroup
from math import pi
@@ -32,3 +31,4 @@ class BIMCadProperties(PropertyGroup):
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
gable_roof_separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
@@ -156,7 +156,7 @@ class CadTool(WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Set gable roof angle").hotkey = "S_R"
row.operator("bim.cad_hotkey", text="Set Gable Roof Angle").hotkey = "S_R"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -229,6 +229,7 @@ class CadHotkey(bpy.types.Operator):
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
self.layout.row().prop(props, "gable_roof_separate_verts")
elif self.hotkey == "S_V":
if not self.is_profile():
@@ -285,7 +286,9 @@ class CadHotkey(bpy.types.Operator):
and RoofData.data["parameters"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
bpy.ops.bim.set_gable_roof_edge_angle(
angle=self.props.gable_roof_edge_angle, separate_verts=self.props.gable_roof_separate_verts
)
def hotkey_S_T(self):
bpy.ops.bim.cad_mitre()
@@ -149,7 +149,7 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator):
class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_cost_item"
bl_label = "Remove Cost item"
bl_label = "Remove Cost Item"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
@@ -17,11 +17,12 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from . import ui, prop, operator, handler, gizmos
from . import ui, prop, operator, handler, gizmos, workspace
classes = (
operator.ActivateDrawing,
operator.ActivateDrawingStyle,
operator.ActivateView,
operator.ActivateModel,
operator.AddAnnotation,
operator.AddDrawing,
operator.AddDrawingStyle,
@@ -30,6 +31,7 @@ classes = (
operator.AddSchedule,
operator.AddScheduleToSheet,
operator.AddSheet,
operator.AddTextLiteral,
operator.BuildSchedule,
operator.CleanWireframes,
operator.ContractSheet,
@@ -40,32 +42,32 @@ classes = (
operator.DisableEditingSchedules,
operator.DisableEditingSheets,
operator.DisableEditingText,
operator.AddTextLiteral,
operator.RemoveTextLiteral,
operator.DuplicateDrawing,
operator.EditAssignedProduct,
operator.EditSheet,
operator.EditText,
operator.EditTextPopup,
operator.EditVectorStyle,
operator.EnableEditingAssignedProduct,
operator.EnableEditingText,
operator.ExpandSheet,
operator.LoadDrawings,
operator.LoadSchedules,
operator.LoadSheets,
operator.OpenDrawing,
operator.OpenSchedule,
operator.OpenSheet,
operator.OpenView,
operator.RemoveDrawing,
operator.RemoveDrawingFromSheet,
operator.RemoveDrawingStyle,
operator.RemoveDrawingStyleAttribute,
operator.RemoveSchedule,
operator.RemoveSheet,
operator.RemoveTextLiteral,
operator.SelectAllDrawings,
operator.ResizeText,
operator.SaveDrawingStyle,
operator.SelectDocIfcFile,
operator.SelectAssignedProduct,
operator.SelectDocIfcFile,
prop.Variable,
prop.Drawing,
prop.Schedule,
@@ -76,6 +78,7 @@ classes = (
prop.Literal,
prop.BIMTextProperties,
prop.BIMAssignedProductProperties,
prop.BIMAnnotationProperties,
ui.BIM_PT_camera,
ui.BIM_PT_drawing_underlay,
ui.BIM_PT_annotation_utilities,
@@ -91,11 +94,15 @@ classes = (
gizmos.DimensionLabelGizmo,
gizmos.ExtrusionGuidesGizmo,
gizmos.ExtrusionWidget,
workspace.Hotkey,
)
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=True)
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
bpy.types.Scene.BIMAnnotationProperties = bpy.props.PointerProperty(type=prop.BIMAnnotationProperties)
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
@@ -105,7 +112,10 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.AnnotationTool)
del bpy.types.Scene.DocProperties
del bpy.types.Scene.BIMAnnotationProperties
del bpy.types.Camera.BIMCameraProperties
del bpy.types.Object.BIMAssignedProductProperties
del bpy.types.Object.BIMTextProperties
@@ -77,66 +77,35 @@ class Annotator:
co1, co2, _, _ = Annotator.get_placeholder_coords()
co1 = obj.matrix_world.inverted() @ co1
co2 = obj.matrix_world.inverted() @ co2
if isinstance(obj.data, bpy.types.Mesh):
obj.data.vertices.add(2)
obj.data.vertices[-2].co = co1
obj.data.vertices[-1].co = co2
obj.data.edges.add(1)
obj.data.edges[-1].vertices = (obj.data.vertices[-2].index, obj.data.vertices[-1].index)
if isinstance(obj.data, bpy.types.Curve):
polyline = obj.data.splines.new("POLY")
polyline.points.add(1)
polyline.points[-2].co = list(co1) + [1]
polyline.points[-1].co = list(co2) + [1]
return obj
@staticmethod
def add_plane_to_annotation(obj):
co1, co2, co3, co4 = Annotator.get_placeholder_coords()
co1 = obj.matrix_world.inverted() @ co1 # bot left
co2 = obj.matrix_world.inverted() @ co2 # top left
co3 = obj.matrix_world.inverted() @ co3 # bot right
co4 = obj.matrix_world.inverted() @ co4 # top right
# default order = bot left, top left, bot right, top right
# therefore we redefine the order
face_verts = [0, 2, 3, 1]
obj.data.vertices.add(4)
v1 = obj.data.vertices[-4]
v2 = obj.data.vertices[-3]
v3 = obj.data.vertices[-2]
v4 = obj.data.vertices[-1]
v1.co = co4
v2.co = co2
v3.co = co1
v4.co = co3
verts_world_space = Annotator.get_placeholder_coords()
verts_local = [obj.matrix_world.inverted() @ v for v in verts_world_space]
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
new_verts = [bm.verts.new(v) for v in verts_local]
obj.data.edges.add(4)
e1 = obj.data.edges[-4]
e2 = obj.data.edges[-3]
e3 = obj.data.edges[-2]
e4 = obj.data.edges[-1]
e1.vertices = (v1.index, v2.index)
e2.vertices = (v2.index, v3.index)
e3.vertices = (v3.index, v4.index)
e4.vertices = (v4.index, v1.index)
obj.data.loops.add(4)
l1 = obj.data.loops[-4]
l2 = obj.data.loops[-3]
l3 = obj.data.loops[-2]
l4 = obj.data.loops[-1]
l1.vertex_index = v1.index
l1.edge_index = e1.index
l2.vertex_index = v2.index
l2.edge_index = e2.index
l3.vertex_index = v3.index
l3.edge_index = e3.index
l4.vertex_index = v4.index
l4.edge_index = e4.index
obj.data.polygons.add(1)
p1 = obj.data.polygons[-1]
p1.vertices = (v1.index, v2.index, v3.index, v4.index)
p1.loop_start = l1.index
p1.loop_total = 4
bm.faces.new([new_verts[i] for i in face_verts])
tool.Blender.apply_bmesh(obj.data, bm, obj)
return obj
@staticmethod
@@ -144,15 +113,15 @@ class Annotator:
camera = tool.Ifc.get_object(drawing)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = camera.matrix_world.copy()
matrix_world[0][3] = co1.x
matrix_world[1][3] = co1.y
matrix_world[2][3] = co1.z
matrix_world.translation = co1
collection = camera.users_collection[0]
if object_type == "TEXT":
obj = bpy.data.objects.new(object_type, None)
obj.matrix_world = matrix_world
collection.objects.link(obj)
return obj
elif object_type in ("TEXT_LEADER", "SECTION_LEVEL"):
data = bpy.data.curves.new(object_type, type="CURVE")
data.dimensions = "3D"
@@ -161,17 +130,20 @@ class Annotator:
obj.matrix_world = matrix_world
collection.objects.link(obj)
return obj
if object_type != "ANGLE":
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if element and element.ObjectType == object_type and obj.type == object_type.upper():
return obj
if data_type == "mesh":
data = bpy.data.meshes.new(object_type)
elif data_type == "curve":
data = bpy.data.curves.new(object_type, type="CURVE")
data.dimensions = "3D"
data.resolution_u = 2
obj = bpy.data.objects.new(object_type, data)
obj.matrix_world = matrix_world
collection.objects.link(obj)
@@ -182,14 +154,13 @@ class Annotator:
if not camera:
camera = bpy.context.scene.camera
z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
y = (
camera.data.ortho_scale
* (bpy.context.scene.render.resolution_y / bpy.context.scene.render.resolution_x)
/ 4
)
else:
y = camera.data.ortho_scale / 4
y = camera.data.ortho_scale / 4
res_x = bpy.context.scene.render.resolution_x
res_y = bpy.context.scene.render.resolution_y
if res_x > res_y:
y *= res_y / res_x
y_offset = camera.matrix_world.to_quaternion() @ Vector((0, y, 0))
x_offset = camera.matrix_world.to_quaternion() @ Vector((y / 2, 0, 0))
return (
@@ -16,10 +16,12 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import ifcopenshell.util.element
import ifcopenshell.util.representation
import blenderbim.tool as tool
from pathlib import Path
def refresh():
@@ -27,7 +29,9 @@ def refresh():
SheetsData.is_loaded = False
SchedulesData.is_loaded = False
DrawingsData.is_loaded = False
AnnotationData.is_loaded = False
DecoratorData.data = {}
DecoratorData.cut_cache = {}
class ProductAssignmentsData:
@@ -56,12 +60,36 @@ class SheetsData:
@classmethod
def load(cls):
cls.data = {"total_sheets": cls.total_sheets()}
cls.data = {
"has_saved_ifc": cls.has_saved_ifc(),
"total_sheets": cls.total_sheets(),
"titleblocks": cls.titleblocks(),
}
cls.is_loaded = True
@classmethod
def has_saved_ifc(cls):
return os.path.isfile(tool.Ifc.get_path())
@classmethod
def total_sheets(cls):
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"])
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"])
@classmethod
def titleblocks(cls):
files = Path(os.path.join(bpy.context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
files = [str(f.stem) for f in files]
if tool.Ifc.get():
project = tool.Ifc.get().by_type("IfcProject")[0]
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
if not titleblocks_dir:
titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
if os.path.exists(titleblocks_dir):
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
return [(f, f, "") for f in sorted(list(set(files)))]
class DrawingsData:
@@ -70,9 +98,17 @@ class DrawingsData:
@classmethod
def load(cls):
cls.data = {"total_drawings": cls.total_drawings(), "location_hint": cls.location_hint()}
cls.data = {
"has_saved_ifc": cls.has_saved_ifc(),
"total_drawings": cls.total_drawings(),
"location_hint": cls.location_hint(),
}
cls.is_loaded = True
@classmethod
def has_saved_ifc(cls):
return os.path.isfile(tool.Ifc.get_path())
@classmethod
def total_drawings(cls):
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
@@ -94,9 +130,13 @@ class SchedulesData:
@classmethod
def load(cls):
cls.data = {"total_schedules": cls.total_schedules()}
cls.data = {"has_saved_ifc": cls.has_saved_ifc(), "total_schedules": cls.total_schedules()}
cls.is_loaded = True
@classmethod
def has_saved_ifc(cls):
return os.path.isfile(tool.Ifc.get_path())
@classmethod
def total_schedules(cls):
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SCHEDULE"])
@@ -114,6 +154,7 @@ FONT_SIZES = {
class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
# used by Ifc Annotations with ObjectType = "BATTING"
@classmethod
@@ -171,7 +212,7 @@ class DecoratorData:
cls.data[obj.name] = display_data
return display_data
# used by Ifc Annotations with ObjectType = "TEXT"
# used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"
@classmethod
def get_ifc_text_data(cls, obj):
"""returns font size in mm for current ifc text object"""
@@ -180,7 +221,7 @@ class DecoratorData:
return result
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
return None
props = obj.BIMTextProperties
@@ -218,15 +259,16 @@ class DecoratorData:
cls.data[obj.name] = text_data
return text_data
# used by Ifc Annotations with ObjectType = "DIMENSION"
# used by Ifc Annotations with ObjectType = "DIMENSION" / "DIAMETER"
@classmethod
def get_dimension_style(cls, obj):
def get_dimension_data(cls, obj):
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcAnnotation") or element.ObjectType != "DIMENSION":
supported_object_types = ("DIMENSION", "DIAMETER")
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types:
return None
dimension_style = "arrow"
@@ -234,5 +276,39 @@ class DecoratorData:
if classes and "oblique" in classes.lower().split():
dimension_style = "oblique"
cls.data[obj.name] = dimension_style
return dimension_style
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension") or {}
show_description_only = pset_data.get("ShowDescriptionOnly", False)
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
dimension_data = {
"dimension_style": dimension_style,
"show_description_only": show_description_only,
"suppress_zero_inches": suppress_zero_inches,
}
cls.data[obj.name] = dimension_data
return dimension_data
class AnnotationData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.props = bpy.context.scene.BIMAnnotationProperties
cls.data["relating_types"] = cls.get_relating_types()
@classmethod
def get_relating_types(cls):
object_type = cls.props.object_type
relating_types = []
for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"):
if tool.Drawing.is_annotation_object_type(relating_type, object_type):
relating_types.append(relating_type)
enum_items = [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in relating_types]
# item to create anootations without relating types
enum_items.insert(0, ("0", "-", ""))
return enum_items
File diff suppressed because it is too large Load Diff
@@ -18,10 +18,9 @@
import bpy
import blf
import math
import gpu, bgl
import gpu
from bpy import types
from mathutils import Vector, Matrix
from mathutils import Vector
from mathutils import geometry
from bpy_extras import view3d_utils
from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader, BaseLinesShader
@@ -273,7 +272,7 @@ class CustomGizmo:
gpu.matrix.multiply_matrix(self.matrix_world)
batch.draw()
bgl.glDisable(bgl.GL_BLEND)
gpu.state.blend_set("NONE")
class OffsetHandle:
@@ -500,7 +499,7 @@ class ExtrusionWidget(types.GizmoGroup):
gz = self.guides = self.gizmos.new("BIM_GT_extrusion_guides")
gz.matrix_basis = basis
gz.color = gz.color_highlight = tuple(theme.gizmo_secondary)
gz.alpha = gz.alpha_highlight = 0.5
gz.alpha = gz.alpha_highlight = 0.75
gz.use_draw_modal = True
gz.target_set_prop("depth", prop, "value")
gz.scale_value = scale_value
@@ -23,8 +23,7 @@ from bpy.app.handlers import persistent
@persistent
def toggleDecorationsOnLoad(*args):
toggle = bpy.context.scene.DocProperties.should_draw_decorations
if toggle:
if bpy.context.scene.DocProperties.should_draw_decorations:
decoration.DecorationsHandler.install(bpy.context)
else:
decoration.DecorationsHandler.uninstall()
@@ -120,9 +120,9 @@ class BoundingBox:
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
# MeasureIt-ARCH is GPL-v3
# In the future I will need to rewrite this to allow the user to have custom
# settings for each annotation object, not read from Blender.
def format_distance(value, isArea=False, hide_units=True):
def format_distance(
value, isArea=False, hide_units=True, precision=None, decimal_places=None, suppress_zero_inches=False
):
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
# Get Scene Unit Settings
@@ -131,7 +131,7 @@ def format_distance(value, isArea=False, hide_units=True):
unit_length = bpy.context.scene.unit_settings.length_unit
toInches = 39.3700787401574887
inPerFoot = 11.999
inPerFoot = 11.9999
if isArea:
toInches = 1550
@@ -141,8 +141,7 @@ def format_distance(value, isArea=False, hide_units=True):
# Imperial Formatting
if unit_system == "IMPERIAL":
precision = bpy.context.scene.BIMProperties.imperial_precision
if precision == "NONE":
if not precision:
precision = 256
elif precision == "1":
precision = 1
@@ -186,44 +185,50 @@ def format_distance(value, isArea=False, hide_units=True):
inches = 0
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
if feet:
tx_dist += str(feet) + "'"
if feet and inches:
if feet and add_inches:
tx_dist += " - "
if inches:
if add_inches:
tx_dist += str(inches)
if inches and frac:
if add_inches and frac:
tx_dist += " "
if frac:
tx_dist += str(frac) + "/" + str(base)
if inches or frac:
if add_inches or frac:
tx_dist += '"'
else:
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
# METRIC FORMATTING
elif unit_system == "METRIC":
precision = bpy.context.scene.BIMProperties.metric_precision
if precision != 0:
if precision:
value = precision * round(float(value) / precision)
if decimal_places:
fmt = "%1." + str(decimal_places) + "f"
# Meters
if unit_length == "METERS":
fmt = "%1.3f"
if not decimal_places:
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
# Centimeters
elif unit_length == "CENTIMETERS":
fmt = "%1.1f"
if not decimal_places:
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
# Millimeters
elif unit_length == "MILLIMETERS":
fmt = "%1.0f"
if not decimal_places:
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
d_mm = value * (1000)
@@ -231,20 +236,21 @@ def format_distance(value, isArea=False, hide_units=True):
# Otherwise Use Adaptive Units
else:
if round(value, 2) >= 1.0:
if round(value, 2) >= 1.0 and not decimal_places:
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
else:
if round(value, 2) >= 0.01:
if round(value, 2) >= 0.01 and not decimal_places:
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
else:
fmt = "%1.0f"
if not decimal_places:
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
d_mm = value * (1000)
File diff suppressed because it is too large Load Diff
@@ -25,7 +25,7 @@ import blenderbim.tool as tool
import blenderbim.core.drawing as core
import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.decoration as decoration
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
from blenderbim.bim.module.drawing.data import refresh as refresh_drawing_data
from pathlib import Path
from blenderbim.bim.prop import Attribute, StrProperty
@@ -44,20 +44,14 @@ from bpy.props import (
diagram_scales_enum = []
titleblocks_enum = []
sheets_enum = []
vector_styles_enum = []
def purge():
global diagram_scales_enum
global titleblocks_enum
global sheets_enum
global vector_styles_enum
diagram_scales_enum = []
titleblocks_enum = []
sheets_enum = []
vector_styles_enum = []
def update_target_view(self, context):
@@ -86,7 +80,7 @@ def update_diagram_scale(self, context):
)
except:
return
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
@@ -191,20 +185,14 @@ def update_layer(self, context, name, value):
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={name: value})
def getTitleblocks(self, context):
global titleblocks_enum
if len(titleblocks_enum) < 1:
titleblocks_enum.clear()
files = Path(os.path.join(context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
files = sorted([str(f.stem) for f in files])
titleblocks_enum.extend([(f, f, "") for f in files])
return titleblocks_enum
def get_titleblocks(self, context):
if not SheetsData.is_loaded:
SheetsData.load()
return SheetsData.data["titleblocks"]
def refreshTitleblocks(self, context):
global titleblocks_enum
titleblocks_enum.clear()
getTitleblocks(self, context)
def update_titleblocks(self, context):
SheetsData.data["titleblocks"] = SheetsData.titleblocks()
def toggleDecorations(self, context):
@@ -220,16 +208,6 @@ def toggleDecorations(self, context):
decoration.DecorationsHandler.uninstall()
def getVectorStyles(self, context):
global vector_styles_enum
if len(vector_styles_enum) < 1:
sheets_enum.clear()
for filename in Path(os.path.join(context.scene.BIMProperties.data_dir, "styles")).glob("*.css"):
f = str(filename.stem)
vector_styles_enum.append((f, f, ""))
return vector_styles_enum
class Variable(PropertyGroup):
name: StringProperty(name="Name")
prop_key: StringProperty(name="Property Key")
@@ -239,6 +217,7 @@ class Drawing(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
name: StringProperty(name="Name", update=update_drawing_name)
target_view: StringProperty(name="Target View")
is_selected: BoolProperty(name="Is Selected", default=True)
class Schedule(PropertyGroup):
@@ -248,19 +227,9 @@ class Schedule(PropertyGroup):
class Sheet(PropertyGroup):
def set_name(self, new):
old = self.get("name")
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets")
if old and os.path.isfile(os.path.join(path, old + ".svg")):
os.rename(os.path.join(path, old + ".svg"), os.path.join(path, new + ".svg"))
self["name"] = new
def get_name(self):
return self.get("name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
identification: StringProperty(name="Identification")
name: StringProperty(name="Name", get=get_name, set=set_name)
name: StringProperty(name="Name")
is_sheet: BoolProperty(name="Is Sheet", default=False)
reference_type: StringProperty(name="Reference Type")
is_expanded: BoolProperty(name="Is Expanded", default=False)
@@ -278,7 +247,6 @@ class DrawingStyle(PropertyGroup):
name="Render Type",
default="VIEWPORT",
)
vector_style: EnumProperty(items=getVectorStyles, name="Vector Style")
include_query: StringProperty(name="Include Query")
exclude_query: StringProperty(name="Exclude Query")
attributes: CollectionProperty(name="Attributes", type=StrProperty)
@@ -337,7 +305,7 @@ class DocProperties(PropertyGroup):
current_drawing_index: IntProperty(name="Current Drawing Index")
schedules: CollectionProperty(name="Schedules", type=Schedule)
active_schedule_index: IntProperty(name="Active Schedule Index")
titleblock: EnumProperty(items=getTitleblocks, name="Titleblock", update=refreshTitleblocks)
titleblock: EnumProperty(items=get_titleblocks, name="Titleblock", update=update_titleblocks)
is_editing_sheets: BoolProperty(name="Is Editing Sheets", default=False)
sheets: CollectionProperty(name="Sheets", type=Sheet)
active_sheet_index: IntProperty(name="Active Sheet Index")
@@ -347,9 +315,23 @@ class DocProperties(PropertyGroup):
decorations_colour: FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
titleblocks_dir: StringProperty(
default=os.path.join("layouts", "titleblocks") + os.path.sep, name="Default Titleblocks Directory"
)
drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory")
stylesheet_path: StringProperty(
default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet"
)
markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers")
symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols")
patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns")
class BIMCameraProperties(PropertyGroup):
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
@@ -471,3 +453,57 @@ class BIMTextProperties(PropertyGroup):
class BIMAssignedProductProperties(PropertyGroup):
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object)
# ObjectType: annotation_name, description, icon, data_type
# fmt: off
ANNOTATION_TYPES_DATA = {
"DIMENSION": ("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"),
"ANGLE": ("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"),
"RADIUS": ("Radius", "", "FORWARD", "curve"),
"DIAMETER": ("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"),
"TEXT": ("Text", "", "SMALL_CAPS", "empty"),
"TEXT_LEADER": ("Leader", "", "TRACKING_BACKWARDS", "curve"),
"STAIR_ARROW": ("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"),
"HIDDEN_LINE": ("Hidden", "", "CON_TRACKTO", "mesh"),
"PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"),
"SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"),
"BREAKLINE": ("Breakline", "", "FCURVE", "mesh"),
"LINEWORK": ("Line", "", "MESH_MONKEY", "mesh"),
"BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"),
"FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"),
"FALL": ("Fall", "", "SORT_ASC", "curve"),
}
# fmt: on
annotation_classes = [(x, *ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(ANNOTATION_TYPES_DATA)]
def get_annotation_data_prop(prop_name):
def function(self, context):
if not AnnotationData.is_loaded:
AnnotationData.load()
return AnnotationData.data[prop_name]
return function
def update_annotation_object_type(self, context):
self.relating_type_id = "0"
# changing enum doesn't trigger refresh by itself
AnnotationData.is_loaded = False
class BIMAnnotationProperties(PropertyGroup):
object_type: bpy.props.EnumProperty(
name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type
)
relating_type_id: bpy.props.EnumProperty(
name="Relating Annotation Type", items=get_annotation_data_prop("relating_types")
)
create_representation_for_type: bpy.props.BoolProperty(
name="Create Representation For Type",
default=False,
description='Whether "Add type" should define a representation for the type \n'
"or allow occurences to have their own",
)
@@ -16,10 +16,177 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bgl
from mathutils import Matrix
from gpu.types import GPUShader
from gpu_extras.batch import batch_for_shader
import gpu
BASE_DEF_GLSL = """
#define PI 3.141592653589793
#define MAX_POINTS 64
#define CIRCLE_SEGS 12
#define SMOOTH_WIDTH 1.0
#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;
uniform float lineWidth;
#define half_winsize (winsize / 2)
// convert camera to window
#define C2W(v) vec4(v.x * half_winsize.x / v.w, v.y * half_winsize.y / v.w, v.z / v.w, 1)
// convert window to camera
#define W2C(v) vec4(v.x * v.w / half_winsize.x, v.y * v.w / half_winsize.y, v.z * v.w, 1)
void emitSegment(vec4 p0, vec4 p1) {
gl_Position = p0;
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
}
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
gl_Position = p1;
EmitVertex();
gl_Position = p2;
EmitVertex();
gl_Position = p3;
EmitVertex();
EndPrimitive();
}
#define matCLIP2WIN() vec4(winsize.x/2, winsize.y/2, 1, 1)
#define CLIP2WIN(v) (clip2win * (v) / (v).w)
#define matWIN2CLIP() vec4(2/winsize.x, 2/winsize.y, 1, 1)
#define WIN2CLIP(v) (win2clip * (v) * (v).w)
#define DEFAULT_SETUP() vec4 clip2win = matCLIP2WIN(), win2clip = matWIN2CLIP(); vec2 EDGE_DIR
out float smoothline;
void arrow_head(in vec4 dir, in float size, in float angle, out vec4 head[3]) {
vec4 nose = dir * size;
float c = cos(angle), s = sin(angle);
head[0] = nose;
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
}
void circle_head(in float size, out vec4 head[CIRCLE_SEGS]) {
float angle_d = PI * 2 / CIRCLE_SEGS;
for(int i = 0; i<CIRCLE_SEGS; i++) {
float angle = angle_d * i;
head[i] = vec4(cos(angle), sin(angle), 0, 0) * size;
}
}
bool check_counterclockwise(in vec4 A, in vec4 B, in vec4 C) {
return (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x);
}
void angle_circle_head(
in vec4 circle_start, in float circle_angle,
in bool counterclockwise,
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
// 1 added to CIRCLE_SEGS because we're number of vertices
// for n segments is n+1
float angle_d;
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
angle_segs = max(1, ceil(circle_angle / angle_d));
angle_d = circle_angle / angle_segs;
for(int i = 0; i < (angle_segs + 1); i++) {
float angle = angle_d * i;
if (counterclockwise) {
head[i] = vec4(
circle_start.x * cos(-angle) + circle_start.y * sin(-angle),
circle_start.x * -sin(-angle) + circle_start.y * cos(-angle),
0, 0
);
} else {
head[i] = vec4(
circle_start.x * cos(angle) + circle_start.y * sin(angle),
circle_start.x * -sin(angle) + circle_start.y * cos(angle),
0, 0
);
}
}
}
void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
vec4 nose = dir * size;
float c = cos(PI/2), s = sin(PI/2);
head[0] = nose;
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
}
// do_vertex_util is custom EmitVertex method to support line smoothing
// `e` - edge xy direction in win/clip space (directions are invariant in both spaces)
#define do_vertex(pos, e) (do_vertex_util(pos, vec2(-(e).y, (e).x) / winsize.xy))
#define do_vertex_win(pos, e) ( do_vertex( WIN2CLIP( pos ), e ) )
// if vertex is shared by two segments of the line still need to emit it twice
// to avoid smoothing artifacts
// don't forget to initialize `vec2 EDGE_DIR` for macro to work
// `pos0` / `pos1` - vertex position in clip space
#define do_edge_verts(pos0, pos1) ( EDGE_DIR = normalize( ( (pos1) - (pos0) ).xy ), do_vertex( pos0, EDGE_DIR ), do_vertex( pos1, EDGE_DIR) )
#define do_edge_verts_win(pos0, pos1) ( do_edge_verts( WIN2CLIP(pos0), WIN2CLIP(pos1) ) )
void do_vertex_util(vec4 pos, vec2 ofs)
{
float final_line_width = lineWidth + SMOOTH_WIDTH * float(lineSmooth);
ofs *= final_line_width;
smoothline = final_line_width * 0.5;
gl_Position = pos;
gl_Position.xy += ofs * pos.w;
EmitVertex();
smoothline = -final_line_width * 0.5;
gl_Position = pos;
gl_Position.xy -= ofs * pos.w;
EmitVertex();
}
// geometry utils
void triangle_head(in vec4 side, in vec4 dir, in float length, in float width, in float radius, out vec4 head[5]) {
vec4 nose = side * length;
vec4 ear = dir * width;
head[0] = side * -radius;
head[1] = nose * -.5;
head[2] = vec4(0) + ear;
head[3] = nose * .5;
head[4] = side * radius;
}
void do_triangle_head(vec4 pos_w, vec4 head[5]) {
DEFAULT_SETUP();
do_edge_verts_win(pos_w + head[0], pos_w + head[1]);
do_edge_verts_win(pos_w + head[1], pos_w + head[2]);
do_edge_verts_win(pos_w + head[2], pos_w + head[3]);
do_edge_verts_win(pos_w + head[3], pos_w + head[4]);
}
void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) {
DEFAULT_SETUP();
for(int i=0; i<CIRCLE_SEGS-1; i++) {
do_edge_verts_win(pos_w + head[i], pos_w + head[i+1]);
EmitVertex();
}
do_edge_verts_win( pos_w + head[CIRCLE_SEGS-1], pos_w + head[0] );
}
"""
class BaseShader:
@@ -28,7 +195,7 @@ class BaseShader:
The Geometry shader works in clipping coords (aftre projecting before division and window scaling).
To make window-scale geometry, vectors should be calculated in window space and than back-projected to clipping spae.
Provide `winSize` uniform vector with halfsize of window, and use W2C and C2W macros.
Provide `half_winsize` uniform vector with halfsize of window, and use W2C and C2W macros.
Replace glsl code in derived classes.
Beware of unused attributes and uniforms: glsl compiler will optimize them out and blender fail with an exception.
@@ -43,9 +210,7 @@ class BaseShader:
TYPE = None # should be LINES|POINTS|etc
DEF_GLSL = """
#define PI 3.141592653589793
"""
DEF_GLSL = BASE_DEF_GLSL
VERT_GLSL = """
uniform mat4 viewMatrix;
@@ -63,46 +228,45 @@ class BaseShader:
"""
# prepended to geom_glsl
LIB_GLSL = """
uniform vec2 winSize;
// convert camera to window
#define C2W(v) vec4(v.x * winSize.x / v.w, v.y * winSize.y / v.w, v.z / v.w, 1)
// convert window to camera
#define W2C(v) vec4(v.x * v.w / winSize.x, v.y * v.w / winSize.y, v.z * v.w, 1)
void emitSegment(vec4 p0, vec4 p1) {
gl_Position = p0;
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
}
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
gl_Position = p1;
EmitVertex();
gl_Position = p2;
EmitVertex();
gl_Position = p3;
EmitVertex();
EndPrimitive();
}
"""
LIB_GLSL = BASE_LIB_GLSL
GEOM_GLSL = """
uniform float viewportDrawingScale;
layout(lines) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
// default setup for macro to work
vec4 clip2win = matCLIP2WIN();
vec4 win2clip = matWIN2CLIP();
vec2 EDGE_DIR;
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
do_edge_verts(p0, p1);
EndPrimitive();
}
"""
FRAG_GLSL = """
uniform vec4 color;
uniform float lineWidth;
in float smoothline;
out vec4 fragColor;
void main() {
vec2 co = gl_FragCoord.xy;
fragColor = color;
if (lineSmooth) {
fragColor.a *= clamp((lineWidth + SMOOTH_WIDTH) * 0.5 - abs(smoothline), 0.0, 1.0);
}
}
"""
def __init__(self):
# NB: libcode arg doesn't work
# TODO: rename to .shader
self.prog = GPUShader(
vertexcode=self.VERT_GLSL,
fragcode=self.FRAG_GLSL,
@@ -120,27 +284,29 @@ class BaseShader:
self.prog.bind()
def glenable(self):
bgl.glEnable(bgl.GL_BLEND)
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
bgl.glBlendEquation(bgl.GL_FUNC_ADD)
# bgl.glEnable(bgl.GL_DEPTH_TEST)
# bgl.glDepthFunc(bgl.GL_LEQUAL)
# bgl.glDepthMask(True)
gpu.state.blend_set("ALPHA")
gpu.state.depth_test_set("LESS_EQUAL")
def uniform_region(self, ctx):
region = ctx.region
region3d = ctx.region_data
try:
self.prog.uniform_float("viewMatrix", region3d.perspective_matrix)
except ValueError: # unused uniform
pass
try:
self.prog.uniform_float("winSize", (region.width / 2, region.height / 2))
except ValueError: # unused uniform
pass
uniform_floats = {
"viewMatrix": region3d.perspective_matrix,
"winsize": (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
# TODO: add smoothing if this shades is going to be used
class BaseLinesShader(BaseShader):
"""Draws line segments with gaps around vertices at endpoints"""
@@ -185,7 +351,6 @@ class BaseLinesShader(BaseShader):
def glenable(self):
super().glenable()
bgl.glEnable(bgl.GL_LINE_SMOOTH)
class GizmoShader(BaseShader):
@@ -207,6 +372,7 @@ class GizmoShader(BaseShader):
"""
# TODO: add smoothing if this shader is going to be used
class DotsGizmoShader(GizmoShader):
"""Draws circles of radius 1 around points"""
@@ -274,24 +440,26 @@ class ExtrusionGuidesShader(GizmoShader):
uniform mat4 ModelViewProjectionMatrix;
layout(lines) in;
layout(line_strip, max_vertices=10) out;
layout(triangle_strip, max_vertices=MAX_POINTS) out;
void main() {
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
vec4 p0w = C2W(p0), p1w = C2W(p1);
// default setup for macro to work
vec2 EDGE_DIR;
emitSegment(p0, p1);
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;
emitSegment(p0 - bx, p0 + bx);
emitSegment(p0 - by, p0 + by);
emitSegment(p1 - bx, p1 + bx);
emitSegment(p1 - by, p1 + by);
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();
}
"""
def glenable(self):
super().glenable()
bgl.glEnable(bgl.GL_LINE_SMOOTH)
@@ -17,14 +17,15 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import uuid
import shutil
import ntpath
import pystache
import urllib.parse
import xml.etree.ElementTree as ET
import blenderbim.tool as tool
import ifcopenshell.util.geolocation
from shutil import copy
from xml.dom import minidom
@@ -33,22 +34,29 @@ class SheetBuilder:
self.data_dir = None
self.scale = "NTS"
def create(self, sheet_path, titleblock_name):
def create(self, layout_path, titleblock_name):
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
root.attrib["id"] = "root"
root.attrib["version"] = "1.1"
view_root = ET.parse(
os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
).getroot()
sheet_dir = os.path.dirname(layout_path)
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
os.makedirs(sheet_dir, exist_ok=True)
os.makedirs(os.path.dirname(titleblock_path), exist_ok=True)
if not os.path.exists(titleblock_path):
shutil.copy(ootb_titleblock_path, titleblock_path)
view_root = ET.parse(titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
view = ET.SubElement(root, "g")
view.attrib["data-type"] = "titleblock"
titleblock = ET.SubElement(view, "image")
titleblock.attrib["xlink:href"] = "../templates/titleblocks/" + titleblock_name + ".svg"
titleblock.attrib["xlink:href"] = os.path.relpath(titleblock_path, sheet_dir)
titleblock.attrib["x"] = "0"
titleblock.attrib["y"] = "0"
titleblock.attrib["width"] = str(view_width)
@@ -58,26 +66,25 @@ class SheetBuilder:
root.attrib["height"] = "{}mm".format(view_height)
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
with open(sheet_path, "w") as f:
with open(layout_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
def add_drawing(self, reference, drawing, sheet):
filename = drawing.Name
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_dir = os.path.join(self.data_dir, "sheets")
drawing_dir = os.path.join(self.data_dir, "diagrams")
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
drawing_path = os.path.join(drawing_dir, filename + ".svg")
underlay_path = os.path.join(drawing_dir, filename + "-underlay.png")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
if not os.path.isfile(sheet_path):
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
if not os.path.exists(layout_path) or not os.path.exists(drawing_path):
raise FileNotFoundError
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
sheet_tree = ET.parse(sheet_path)
sheet_root = sheet_tree.getroot()
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
view_tree = ET.parse(drawing_path)
view_root = view_tree.getroot()
@@ -85,7 +92,7 @@ class SheetBuilder:
# The view is placed into a group with a background image element.
# Although the foreground SVG already has a background, it is duplicated
# here to accommodate browsers which do not nest images.
view = ET.SubElement(sheet_root, "g")
view = ET.SubElement(layout_root, "g")
view.attrib["data-type"] = "drawing"
view.attrib["data-id"] = str(reference.id())
view.attrib["data-drawing"] = drawing.GlobalId
@@ -95,7 +102,7 @@ class SheetBuilder:
if os.path.isfile(underlay_path):
background = ET.SubElement(view, "image")
background.attrib["data-type"] = "background"
background.attrib["xlink:href"] = os.path.relpath(underlay_path, sheet_dir)
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
background.attrib["x"] = "30"
background.attrib["y"] = "30"
background.attrib["width"] = str(view_width)
@@ -104,84 +111,96 @@ class SheetBuilder:
if os.path.isfile(drawing_path):
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "foreground"
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, sheet_dir)
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir)
foreground.attrib["x"] = "30"
foreground.attrib["y"] = "30"
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(30, view_height + 35, view)
sheet_tree.write(sheet_path)
self.add_view_title(30, view_height + 35, view, layout_dir)
layout_tree.write(layout_path)
def remove_drawing(self, reference, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
sheet_path = tool.Drawing.get_document_uri(sheet)
sheet_tree = ET.parse(sheet_path)
sheet_root = sheet_tree.getroot()
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
for g in sheet_root.findall("{http://www.w3.org/2000/svg}g"):
for g in layout_root.findall("{http://www.w3.org/2000/svg}g"):
if g.attrib.get("data-id") == str(reference.id()):
sheet_root.remove(g)
layout_root.remove(g)
break
sheet_tree.write(sheet_path)
layout_tree.write(layout_path)
def add_schedule(self, reference, schedule, sheet):
view_path = tool.Drawing.get_document_uri(schedule)
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_schedule(schedule)
schedule_name = os.path.splitext(os.path.basename(view_path))[0]
sheet_path = tool.Drawing.get_document_uri(sheet)
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
sheet_tree = ET.parse(sheet_path)
sheet_root = sheet_tree.getroot()
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
view_tree = ET.parse(view_path)
view_root = view_tree.getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
view = ET.SubElement(sheet_root, "g")
view = ET.SubElement(layout_root, "g")
view.attrib["data-type"] = "schedule"
view.attrib["data-id"] = str(reference.id())
view.attrib["data-schedule"] = str(schedule.id())
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "table"
foreground.attrib["xlink:href"] = "../schedules/{}.svg".format(schedule_name)
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
foreground.attrib["x"] = "30"
foreground.attrib["y"] = "30"
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(30, view_height + 35, view)
sheet_tree.write(sheet_path)
self.add_view_title(30, view_height + 35, view, layout_dir)
layout_tree.write(layout_path)
def add_view_title(self, x, y, parent):
title_tree = ET.parse(os.path.join(self.data_dir, "templates", "view-title.svg"))
def add_view_title(self, x, y, parent, layout_dir):
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
ootb_title = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "view-title.svg")
shutil.copy(ootb_title, title_path)
title_tree = ET.parse(title_path)
title_root = title_tree.getroot()
title = ET.SubElement(parent, "image")
title.attrib["data-type"] = "view-title"
title.attrib["xlink:href"] = "../templates/view-title.svg"
title.attrib["xlink:href"] = os.path.relpath(title_path, layout_dir)
title.attrib["x"] = str(x)
title.attrib["y"] = str(y)
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet):
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
self.references = {"SHEET": None, "RASTER": []}
sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
self.layout_dir = os.path.dirname(layout_path)
sheet_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_sheet_path(sheet[0], sheet.Name))
self.sheets_dir = os.path.dirname(sheet_path)
os.makedirs(self.sheets_dir, exist_ok=True)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
tree = ET.parse(sheet_path)
tree = ET.parse(layout_path)
root = tree.getroot()
self.defs = ET.Element("defs")
@@ -189,11 +208,15 @@ class SheetBuilder:
self.build_titleblock(root, sheet)
self.build_drawings(root, sheet)
self.build_schedules(root)
self.build_schedules(root, sheet)
with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
with open(sheet_path, "wb") as output:
tree.write(output)
self.references["SHEET"] = sheet_path
return self.references
def build_titleblock(self, root, sheet):
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
@@ -208,8 +231,6 @@ class SheetBuilder:
titleblock.remove(image)
def build_drawings(self, root, sheet):
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
@@ -232,12 +253,15 @@ class SheetBuilder:
view.append(self.parse_embedded_svg(foreground, {}))
if background is not None:
background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
background_path = os.path.join(self.layout_dir, self.get_href(background))
raster_path = os.path.join(self.sheets_dir, os.path.basename(background_path))
shutil.copy(background_path, raster_path)
self.references["RASTER"].append(raster_path)
if view_title is not None:
foreground_path = self.get_href(foreground)
data = reference.get_info()
data.update({"Sheet" + k: v for k, v in sheet.get_info().items()})
if not data["Name"]:
data["Name"] = ntpath.basename(foreground_path)[0:-4]
data["Scale"] = tool.Drawing.get_drawing_human_scale(drawing)
@@ -246,7 +270,7 @@ class SheetBuilder:
for image in images:
view.remove(image)
def build_schedules(self, root):
def build_schedules(self, root, sheet):
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'):
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"]))
@@ -268,6 +292,7 @@ class SheetBuilder:
if view_title is not None:
path = self.get_href(table)
data = reference.get_info()
data.update({"Sheet" + k: v for k, v in sheet.get_info().items()})
if not data["Name"]:
data["Name"] = schedule.Name or "Unnamed"
view.append(self.parse_embedded_svg(view_title, data))
@@ -298,7 +323,7 @@ class SheetBuilder:
self.defs.append(clip_path)
svg_path = self.get_href(image)
with open(os.path.join(self.data_dir, "sheets", svg_path), "r") as template:
with open(os.path.join(self.layout_dir, svg_path), "r") as template:
embedded = ET.fromstring(pystache.render(template.read(), data))
# viewBox should not be nested
embedded.attrib["viewBox"] = ""
@@ -314,6 +339,39 @@ class SheetBuilder:
group.append(child)
return group
def change_titleblock(self, sheet, titleblock_name):
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
sheet_dir = os.path.dirname(sheet_path)
os.makedirs(sheet_dir, exist_ok=True)
os.makedirs(os.path.dirname(titleblock_path), exist_ok=True)
if not os.path.exists(titleblock_path):
shutil.copy(ootb_titleblock_path, titleblock_path)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
view_root = ET.parse(ootb_titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
sheet_tree = ET.parse(sheet_path)
root = sheet_tree.getroot()
titleblock = sheet_tree.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image[@{http://www.w3.org/1999/xlink}href]")[0]
image.attrib["{http://www.w3.org/1999/xlink}href"] = os.path.relpath(titleblock_path, sheet_dir)
image.attrib["width"] = str(view_width)
image.attrib["height"] = str(view_height)
root.attrib["width"] = "{}mm".format(view_width)
root.attrib["height"] = "{}mm".format(view_height)
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
sheet_tree.write(sheet_path)
def convert_to_mm(self, value):
# CSS is what defines these possibilities
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
@@ -21,6 +21,7 @@ import re
import bpy
import math
import bmesh
import shutil
import pystache
import mathutils
import xml.etree.ElementTree as ET
@@ -65,10 +66,9 @@ class SvgWriter:
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
self.related_paths = None
self.resource_paths = {}
def create_blank_svg(self, output_path):
self.define_related_paths() # making sure all paths are defined
self.calculate_scale()
self.svg = svgwrite.Drawing(
output_path,
@@ -85,35 +85,24 @@ class SvgWriter:
self.svg.save(pretty=True)
def draw_underlay(self, image):
self.svg.add(
self.svg.image(
os.path.join("..", "diagrams", os.path.basename(image)),
width=self.width,
height=self.height,
)
)
self.svg.add(self.svg.image(os.path.basename(image), width=self.width, height=self.height))
return self
def define_related_paths(self, **related_paths):
if not self.related_paths:
self.related_paths = {}
if not related_paths:
related_paths = {
"Stylesheet": os.path.join(self.data_dir, "styles", f"default.css"),
"Markers": os.path.join(self.data_dir, "templates", "markers.svg"),
"Symbols": os.path.join(self.data_dir, "templates", "symbols.svg"),
"Patterns": os.path.join(self.data_dir, "templates", "patterns.svg"),
}
for path_name in list(related_paths.keys()):
if path_name in self.related_paths:
del related_paths[path_name]
for path_name in related_paths:
uri = related_paths[path_name]
custom_path = tool.Ifc.resolve_uri(uri)
if custom_path:
self.related_paths[path_name] = custom_path
def setup_drawing_resource_paths(self, element):
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"):
resource_path = pset.get(resource)
if not resource_path:
self.resource_paths[resource] = None
continue
resource_path = tool.Ifc.resolve_uri(resource_path)
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
if not os.path.exists(resource_path):
resource_basename = os.path.basename(resource_path)
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
if os.path.exists(ootb_resource):
shutil.copy(ootb_resource, resource_path)
self.resource_paths[resource] = resource_path
def define_boilerplate(self):
self.add_stylesheet()
@@ -132,33 +121,43 @@ class SvgWriter:
self.height = self.raw_height * self.svg_scale
def add_stylesheet(self):
with open(self.related_paths["Stylesheet"], "r") as stylesheet:
if not self.resource_paths["Stylesheet"] or not os.path.exists(self.resource_paths["Stylesheet"]):
return
with open(self.resource_paths["Stylesheet"], "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self):
tree = ET.parse(self.related_paths["Markers"])
if not self.resource_paths["Markers"] or not os.path.exists(self.resource_paths["Markers"]):
return
tree = ET.parse(self.resource_paths["Markers"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_symbols(self):
tree = ET.parse(self.related_paths["Symbols"])
if not self.resource_paths["Symbols"] or not os.path.exists(self.resource_paths["Symbols"]):
return
tree = ET.parse(self.resource_paths["Symbols"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def find_xml_symbol_by_id(self, id):
tree = ET.parse(self.related_paths["Symbols"])
tree = ET.parse(self.resource_paths["Symbols"])
xml_symbol = tree.find(f'.//*[@id="{id}"]')
return External(xml_symbol) if xml_symbol else None
def add_patterns(self):
tree = ET.parse(self.related_paths["Patterns"])
if not self.resource_paths["Patterns"] or not os.path.exists(self.resource_paths["Patterns"]):
return
tree = ET.parse(self.resource_paths["Patterns"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def draw_annotations(self, annotations):
def draw_annotations(self, annotations, precision, decimal_places):
self.precision = precision
self.decimal_places = decimal_places
for element in annotations:
obj = tool.Ifc.get_object(element)
if not obj or element.ObjectType == "DRAWING":
@@ -228,18 +227,14 @@ class SvgWriter:
# TODO: allow metric to be configurable
rl = (matrix_world @ points[0].co.xyz).z
if bpy.context.scene.unit_settings.system == "IMPERIAL":
rl = helper.format_distance(rl)
rl = helper.format_distance(rl, precision=self.precision, decimal_places=self.decimal_places)
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rl /= unit_scale
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
rl *= unit_scale
rl = "{:.3f}m".format(rl)
text_style = {
"text-anchor": "start",
"alignment-baseline": "baseline",
"dominant-baseline": "baseline",
}
text_style = self.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))
@@ -268,11 +263,7 @@ class SvgWriter:
"UP",
insert=tuple(text_position),
class_="STAIR",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
},
**self.get_box_alignment_parameters("center"),
)
)
@@ -296,16 +287,13 @@ 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")
self.svg.add(
self.svg.text(
axis_tag,
insert=tuple(start * self.svg_scale),
class_="GRID",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
},
**text_style,
)
)
self.svg.add(
@@ -313,11 +301,7 @@ class SvgWriter:
axis_tag,
insert=tuple(end * self.svg_scale),
class_="GRID",
**{
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
},
**text_style,
)
)
@@ -356,7 +340,7 @@ class SvgWriter:
def get_attribute_classes(self, obj):
element = tool.Ifc.get_entity(obj)
global_id = "GlobalId-{}".format(element.GlobalId)
predefined_type = "PredefinedType-" + self.canonicalise_class_name(
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
str(ifcopenshell.util.element.get_predefined_type(element))
)
classes = [global_id, element.is_a(), predefined_type]
@@ -366,12 +350,11 @@ class SvgWriter:
for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key)
if value:
classes.append(self.canonicalise_class_name(key) + "-" + self.canonicalise_class_name(str(value)))
classes.append(
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
)
return classes
def canonicalise_class_name(self, name):
return re.sub("[^0-9a-zA-Z]+", "", name)
def draw_line_annotation(self, obj):
# TODO: properly scope these offsets
x_offset = self.raw_width / 2
@@ -574,11 +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 = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
text_style = self.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
reference_id,
@@ -611,11 +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 = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
text_style = self.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
reference_id, insert=(text_position[0], text_position[1] - 2.5), class_="ELEVATION", **text_style
@@ -642,6 +617,43 @@ class SvgWriter:
return (reference_id, sheet_id)
return ("-", "-")
def get_box_alignment_parameters(self, box_alignment):
"""Convenience method to get svg parameters for text alignment
in a readable way.
Metehod expecting values like:
`top-left`, `top-middle`, `top-right`,
`middle-left`, `center`, `middle-right`,
`bottom-left`, `bottom-middle`, `bottom-right`
"""
# reference for alignment values:
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/alignment-baseline
vertical_alignment = {
"top": "hanging",
"bottom": "baseline",
"center": "middle",
"middle": "middle",
}
alignment_baseline = vertical_alignment[next(align for align in vertical_alignment if align in box_alignment)]
horizontal_alignment = {
"left": "start",
"right": "end",
"center": "middle",
"middle": "middle",
}
text_anchor = horizontal_alignment[next(align for align in horizontal_alignment if align in box_alignment)]
# using dominant-baseline because we plan to use <tspan> subtags
# otherwise alignment-baseline would be sufficient
return {
"dominant-baseline": alignment_baseline,
"text-anchor": text_anchor,
}
def draw_text_annotation(self, text_obj, position):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -653,16 +665,17 @@ class SvgWriter:
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
text_position_svg = text_position * self.svg_scale
local_x_axis = text_obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
projected_x_axis = self.project_point_onto_camera(position + local_x_axis)
angle = math.degrees(
(Vector((x_offset + projected_x_axis.x, y_offset - projected_x_axis.y)) - text_position).angle_signed(
Vector((1, 0))
)
)
def get_basis_vector(matrix, i=0):
"""returns basis vector for i in world space, unaffected by object scale"""
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world)
text_dir = (self.camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
angle = math.degrees(-text_dir.angle_signed(Vector((1, 0))))
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
classes_str = " ".join(self.get_attribute_classes(text_obj))
classes = self.get_attribute_classes(text_obj)
classes_str = " ".join(classes)
symbol = tool.Drawing.get_annotation_symbol(element)
template_text_fields = []
@@ -686,34 +699,6 @@ class SvgWriter:
if not symbol_svg or not template_text_fields:
self.svg.add(self.svg.use(f"#{symbol}", insert=text_position_svg))
def get_box_alignment_parameters(box_alignment):
# reference for alignment values:
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/alignment-baseline
vertical_alignment = {
"top": "hanging",
"bottom": "baseline",
"center": "middle",
"middle": "middle",
}
alignment_baseline = vertical_alignment[
next(align for align in vertical_alignment if align in box_alignment)
]
horizontal_alignment = {
"left": "start",
"right": "end",
"center": "middle",
"middle": "middle",
}
text_anchor = horizontal_alignment[next(align for align in horizontal_alignment if align in box_alignment)]
# using dominant-baseline because we plan to use <tspan> subtags
# otherwise alignment-baseline would be sufficient
return {
"dominant-baseline": alignment_baseline,
"text-anchor": text_anchor,
}
for text_literal in text_literals:
# after pretty indentation some redundant spaces can occur in svg tags
# this is why we apply "font-size: 0;" to the text tag to remove those spaces
@@ -721,22 +706,32 @@ class SvgWriter:
# ref: https://github.com/IfcOpenShell/IfcOpenShell/issues/2833#issuecomment-1471584960
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
text_tag = self.svg.text(
"",
**{
"transform": transform,
"style": "font-size: 0;",
},
**get_box_alignment_parameters(text_literal.BoxAlignment),
)
self.svg.add(text_tag)
attribs = {
"transform": transform,
"style": "font-size: 0;",
}
def add_text_tag(add_fill_bg):
text_tag = self.svg.text(
"",
**(attribs | {"filter": "url(#fill-background)"}) if add_fill_bg else attribs,
**self.get_box_alignment_parameters(text_literal.BoxAlignment),
)
self.svg.add(text_tag)
text_lines = text.replace("\\n", "\n").split("\n")
for line_number, text_line in enumerate(text_lines):
# position has to be inserted at tspan to avoid x offset between tspans
tspan = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
tspan.update({"dy": f"{line_number}em"})
text_tag.add(tspan)
if "fill-bg" in classes:
add_text_tag(True)
add_text_tag(False)
for line_number, text_line in enumerate(text.replace("\\n", "\n").split("\n")):
# position has to be inserted at tspan to avoid x offset between tspans
t_span = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
t_span.update({"dy": f"{line_number}em"})
text_tag.add(t_span)
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
@@ -784,29 +779,24 @@ class SvgWriter:
)
)
# TODO: allow metric to be configurable
rl = ((matrix_world @ points[0].co).xyz + obj.location).z
rl = (matrix_world @ points[0].co).z
if bpy.context.scene.unit_settings.system == "IMPERIAL":
rl = helper.format_distance(rl)
rl = helper.format_distance(rl, precision=self.precision, decimal_places=self.decimal_places)
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rl /= unit_scale
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
rl *= unit_scale
rl = "{:.3f}m".format(rl)
if projected_points[0].x > projected_points[-1].x:
text_anchor = "end"
else:
text_anchor = "start"
box_alignment = "bottom-left" if projected_points[0].x <= projected_points[-1].x else "bottom-right"
text_style = self.get_box_alignment_parameters(box_alignment)
self.svg.add(
self.svg.text(
"RL +{}".format(rl),
insert=tuple(text_position),
class_="PLANLEVEL",
**{
"text-anchor": text_anchor,
"alignment-baseline": "baseline",
"dominant-baseline": "baseline",
},
**text_style,
)
)
@@ -922,12 +912,7 @@ class SvgWriter:
text_offset = (text_position - center_position).xy.normalized() * 5
text_position += text_offset
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
text_style = self.get_box_alignment_parameters("center")
angle_text = abs(round(math.degrees(angle), 3))
if is_reflex:
angle_text = 360 - angle_text
@@ -988,14 +973,9 @@ class SvgWriter:
)
text_position += text_offset
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
text_style = self.get_box_alignment_parameters("center")
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius)
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
tag = element.Description or f"R{radius}"
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
@@ -1051,7 +1031,7 @@ class SvgWriter:
elif element.ObjectType == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{helper.format_distance(rise)} / {helper.format_distance(run)}"
return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}"
elif element.ObjectType == "SLOPE_PERCENT":
if angle == 90:
return "-"
@@ -1066,19 +1046,16 @@ class SvgWriter:
)
text_position += text_offset
text_style = {
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
text_style = self.get_box_alignment_parameters("center")
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
def draw_diameter_annotations(self, obj):
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
element = tool.Ifc.get_entity(obj)
text_override = element.Description
dimension_text = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i, p in enumerate(points):
@@ -1087,22 +1064,35 @@ class SvgWriter:
v0_global = matrix_world @ points[i].co.xyz
v1_global = matrix_world @ points[i + 1].co.xyz
self.draw_dimension_annotation(
v0_global, v1_global, classes, text_override, text_format=lambda x: "D" + x
v0_global,
v1_global,
classes,
dimension_text,
text_format=lambda x: "D" + x,
show_description_only=dimension_data["show_description_only"],
suppress_zero_inches=dimension_data["suppress_zero_inches"],
)
def draw_dimension_annotations(self, obj):
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
element = tool.Ifc.get_entity(obj)
text_override = element.Description
dimension_text = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i, p in enumerate(points):
if i + 1 >= len(points):
continue
for i in range(len(points) - 1):
v0_global = matrix_world @ points[i].co.xyz
v1_global = matrix_world @ points[i + 1].co.xyz
self.draw_dimension_annotation(v0_global, v1_global, classes, text_override)
self.draw_dimension_annotation(
v0_global,
v1_global,
classes,
dimension_text=dimension_text,
show_description_only=dimension_data["show_description_only"],
suppress_zero_inches=dimension_data["suppress_zero_inches"],
)
def draw_measureit_arch_dimension_annotations(self):
try:
@@ -1116,46 +1106,56 @@ class SvgWriter:
Vector(coord[0]), Vector(coord[1]), ["IfcAnnotation", "PredefinedType-DIMENSION"]
)
def draw_dimension_annotation(self, v0_global, v1_global, classes, text_override=None, text_format=lambda x: x):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
def draw_dimension_annotation(
self,
v0_global,
v1_global,
classes,
dimension_text=None,
text_format=lambda x: x,
show_description_only=False,
suppress_zero_inches=False,
):
offset = Vector([self.raw_width, self.raw_height]) / 2
v0 = self.project_point_onto_camera(v0_global)
v1 = self.project_point_onto_camera(v1_global)
start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
start = (offset + v0.xy * Vector((1, -1))) * self.svg_scale
end = (offset + v1.xy * Vector((1, -1))) * self.svg_scale
mid = ((end - start) / 2) + start
vector = end - start
perpendicular = Vector((vector.y, -vector.x)).normalized()
dimension = (v1_global - v0_global).length
dimension = helper.format_distance(dimension)
sheet_dimension = ((end * self.svg_scale) - (start * self.svg_scale)).length
if sheet_dimension < 5: # annotation can't fit
# offset text to right of marker
text_position = (end * self.svg_scale) + perpendicular + (3 * vector.normalized())
else:
text_position = (mid * self.svg_scale) + perpendicular
dimension = helper.format_distance(
dimension,
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
)
sheet_dimension = (end - start).length
# if annotation can't fit offset text to the right of marker
text_position = mid if sheet_dimension > 5 else (end + (3 * vector.normalized()))
rotation = math.degrees(vector.angle_signed(Vector((1, 0))))
line = self.svg.add(
self.svg.line(
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
)
)
if text_override is not None:
text = text_override
else:
text = str(dimension)
text = text_format(text)
self.svg.add(
self.svg.text(
text,
insert=tuple(text_position),
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)}
return self.svg.text(
text_format(text),
insert=text_position,
class_="DIMENSION",
**{
"transform": "rotate({} {} {})".format(rotation, text_position.x, text_position.y),
"text-anchor": "middle",
},
**(text_kwargs | self.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"))
elif show_description_only and dimension_text:
self.svg.add(create_text_tag(dimension_text, text_position + perpendicular, "bottom-middle"))
def project_point_onto_camera(self, point):
# TODO is this needlessly complex?
@@ -62,6 +62,11 @@ class BIM_PT_camera(Panel):
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = layout.row()
row.prop(props, "calculate_shapely_surfaces")
row = layout.row()
row.prop(props, "calculate_svgfill_surfaces")
row = layout.row()
row.prop(dprops, "should_extract")
@@ -84,7 +89,7 @@ class BIM_PT_camera(Panel):
row = layout.row(align=True)
row.operator("bim.create_drawing", text="Create Drawing", icon="OUTPUT")
op = row.operator("bim.open_view", icon="URL", text="")
op = row.operator("bim.open_drawing", icon="URL", text="")
op.view = context.active_object.name.split("/")[1]
@@ -123,9 +128,6 @@ class BIM_PT_drawing_underlay(Panel):
row = layout.row()
row.prop(drawing_style, "render_type")
row = layout.row(align=True)
row.prop(drawing_style, "vector_style")
row.operator("bim.edit_vector_style", text="", icon="GREASEPENCIL")
row = layout.row(align=True)
row.prop(drawing_style, "include_query")
row = layout.row(align=True)
row.prop(drawing_style, "exclude_query")
@@ -158,6 +160,14 @@ class BIM_PT_drawings(Panel):
if not DrawingsData.is_loaded:
DrawingsData.load()
if not DrawingsData.data["has_saved_ifc"]:
row = self.layout.row()
row.label(text="Project Not Yet Saved", icon="ERROR")
row = self.layout.row()
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
op.should_save_as = False
return
self.props = context.scene.DocProperties
if not self.props.is_editing_drawings:
@@ -185,9 +195,11 @@ class BIM_PT_drawings(Panel):
).drawing = active_drawing.ifc_definition_id
col = row.column()
col.alignment = "RIGHT"
op = row.operator("bim.open_view", icon="URL", text="")
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
op = row.operator("bim.open_drawing", icon="URL", text="")
op.view = active_drawing.name
op = row.operator("bim.activate_view", icon="OUTLINER_OB_CAMERA", text="")
row.operator("bim.activate_model", icon="VIEW3D", text="")
op = row.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="")
op.drawing = active_drawing.ifc_definition_id
row.operator("bim.create_drawing", text="", icon="OUTPUT")
self.layout.template_list(
@@ -220,6 +232,14 @@ class BIM_PT_schedules(Panel):
if not SchedulesData.is_loaded:
SchedulesData.load()
if not SchedulesData.data["has_saved_ifc"]:
row = self.layout.row()
row.label(text="Project Not Yet Saved", icon="ERROR")
row = self.layout.row()
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
op.should_save_as = False
return
self.props = context.scene.DocProperties
if not self.props.is_editing_schedules:
@@ -263,6 +283,14 @@ class BIM_PT_sheets(Panel):
if not SheetsData.is_loaded:
SheetsData.load()
if not SheetsData.data["has_saved_ifc"]:
row = self.layout.row()
row.label(text="Project Not Yet Saved", icon="ERROR")
row = self.layout.row()
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
op.should_save_as = False
return
self.props = context.scene.DocProperties
if not self.props.is_editing_sheets:
@@ -280,6 +308,7 @@ class BIM_PT_sheets(Panel):
active_sheet = self.props.sheets[self.props.active_sheet_index]
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.edit_sheet", icon="GREASEPENCIL", text="")
row.operator("bim.open_sheet", icon="URL", text="")
row.operator("bim.add_drawing_to_sheet", icon="IMAGE_PLANE", text="")
row.operator("bim.add_schedule_to_sheet", icon="PRESET_NEW", text="")
@@ -343,7 +372,7 @@ class BIM_PT_text(Panel):
element = tool.Ifc.get_entity(context.active_object)
if not element:
return
return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"]
return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
def draw(self, context):
obj = context.active_object
@@ -470,6 +499,7 @@ class BIM_PT_annotation_utilities(Panel):
op = row.operator("bim.add_annotation", text="Batting", icon="FORCE_FORCE")
op.object_type = "BATTING"
op.data_type = "mesh"
op.description = "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set"
op = row.operator("bim.add_annotation", text="Fill Area", icon="NODE_TEXTURE")
op.object_type = "FILL_AREA"
@@ -487,6 +517,8 @@ class BIM_UL_drawinglist(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
row.prop(item, "is_selected", text="", icon=selected_icon)
icon = "UV_FACESEL"
if item.target_view == "ELEVATION_VIEW":
icon = "UV_VERTEXSEL"
@@ -503,26 +535,35 @@ class BIM_UL_drawinglist(bpy.types.UIList):
class BIM_UL_sheets(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
if item.is_sheet:
if item.is_expanded:
row.operator(
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).sheet = item.ifc_definition_id
else:
row.operator(
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).sheet = item.ifc_definition_id
else:
row.label(text="", icon="BLANK1")
if item.reference_type == "DRAWING":
row.label(text="", icon="IMAGE_DATA")
elif item.reference_type == "SCHEDULE":
row.label(text="", icon="LONGDISPLAY")
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
row.label(text=name)
else:
if not item:
layout.label(text="", translate=False)
return
row = layout.row(align=True)
if item.is_sheet:
if item.is_expanded:
row.operator(
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).sheet = item.ifc_definition_id
else:
row.operator(
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).sheet = item.ifc_definition_id
row.label(text=f"{item.identification} - {item.name}")
else:
row.label(text="", icon="BLANK1")
if item.reference_type == "DRAWING":
row.label(text="", icon="IMAGE_DATA")
elif item.reference_type == "SCHEDULE":
row.label(text="", icon="LONGDISPLAY")
elif item.reference_type == "TITLEBLOCK":
row.label(text="", icon="MENU_PANEL")
elif item.reference_type == "REVISION":
row.label(text="", icon="RECOVER_LAST")
if item.identification:
name = f"{item.identification} - {item.name or 'Unnamed'}"
else:
name = item.name or "Unnamed"
row.label(text=name)
@@ -0,0 +1,296 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 @Andrej730
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import blenderbim.tool as tool
from blenderbim.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
# from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
from blenderbim.bim.module.drawing.data import DecoratorData, AnnotationData
from blenderbim.bim.ifc import IfcStore
import blenderbim.bim.handler
# declaring it here to avoid circular import problems
class Operator:
def execute(self, context):
IfcStore.execute_ifc_operator(self, context)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class AnnotationTool(WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.annotation_tool"
bl_label = "Annotation Tool"
bl_description = "Gives you Annotation related superpowers"
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
bl_widget = None
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
("bim.annotation_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.annotation_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.annotation_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.annotation_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
("bim.annotation_hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
("bim.annotation_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
("bim.annotation_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
("bim.annotation_hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
("bim.annotation_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.annotation_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.annotation_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("bim.annotation_hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
("bim.annotation_hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
)
def draw_settings(context, layout, ws_tool):
# Unlike operators, Blender doesn't treat workspace tools as a class, so we'll create our own.
AnnotationToolUI.draw(context, layout)
def add_layout_hotkey_operator(layout, text, hotkey, description):
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.annotation_hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op, row
# TODO: move to operator
def create_annotation_type(context):
# just empty to store parameters
props = context.scene.BIMAnnotationProperties
object_type = props.object_type
create_representation = props.create_representation_for_type
drawing = tool.Ifc.get_entity(bpy.context.scene.camera)
if props.create_representation_for_type:
obj = tool.Drawing.create_annotation_object(drawing, object_type)
else:
obj = bpy.data.objects.new(object_type, None)
obj.name = f"{object_type}_TYPE"
obj.location = context.scene.cursor.location
tool.Drawing.ensure_annotation_in_drawing_plane(obj)
drawing = tool.Ifc.get_entity(context.scene.camera)
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing))
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcTypeProduct",
predefined_type=object_type,
should_add_representation=create_representation,
context=ifc_context,
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
tool.Blender.select_and_activate_single_object(context, obj)
# TODO: move to operator
def create_annotation_occurence(context):
# object_type = context.scene.BIMAnnotationProperties.object_type
props = context.scene.BIMAnnotationProperties
relating_type = tool.Ifc.get().by_id(int(props.relating_type_id))
object_type = props.object_type
drawing = tool.Ifc.get_entity(context.scene.camera)
obj = tool.Drawing.create_annotation_object(drawing, object_type)
obj.name = relating_type.Name
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing))
relating_type_repr = tool.Drawing.get_annotation_representation(relating_type)
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type=object_type,
should_add_representation=not bool(relating_type_repr),
context=ifc_context,
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
tool.Ifc.run("group.assign_group", group=tool.Drawing.get_drawing_group(drawing), products=[element])
tool.Collector.assign(obj)
tool.Blender.select_and_activate_single_object(context, obj)
def create_annotation():
props = bpy.context.scene.BIMAnnotationProperties
create_type_occurence = props.relating_type_id != "0"
if create_type_occurence:
create_annotation_occurence(bpy.context)
else:
object_type = props.object_type
bpy.ops.bim.add_annotation(object_type=object_type, data_type=ANNOTATION_TYPES_DATA[object_type][-1])
class AnnotationToolUI:
@classmethod
def draw(cls, context, layout):
cls.layout = layout
cls.props = context.scene.BIMAnnotationProperties
row = cls.layout.row(align=True)
if not tool.Ifc.get():
row.label(text="No IFC Project", icon="ERROR")
return
if not AnnotationData.is_loaded:
AnnotationData.load()
cls.draw_type_selection_interface()
if context.active_object and context.selected_objects:
cls.draw_edit_object_interface(context)
elif not context.selected_objects:
cls.draw_create_object_interface()
@classmethod
def draw_create_object_interface(cls):
row = cls.layout.row(align=True)
op, row = add_layout_hotkey_operator(cls.layout, "Add Type", "S_C", "Create a new annotation type")
selected_icon = "CHECKBOX_HLT" if cls.props.create_representation_for_type else "CHECKBOX_DEHLT"
row.prop(cls.props, "create_representation_for_type", text="", icon=selected_icon)
@classmethod
def draw_edit_object_interface(cls, context):
if DecoratorData.get_ifc_text_data(bpy.context.object):
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
@classmethod
def draw_header_interface(cls):
cls.draw_type_selection_interface()
@classmethod
def draw_basic_annotation_tool_interface(cls):
cls.draw_type_selection_interface()
@classmethod
def draw_type_selection_interface(cls):
# shared by both sidebar and header
object_type = cls.props.object_type
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "object_type", text="")
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
create_type_occurence = cls.props.relating_type_id != "0"
label = "Add Type Occurence" if create_type_occurence else "Add Annotation"
add_layout_hotkey_operator(cls.layout, label, "S_A", "Create a new annotation")
if object_type in ("TEXT", "STAIR_ARROW"):
add_layout_hotkey_operator(
cls.layout,
"Bulk Tag",
"S_T",
"Create new annotations and automatically adjust them to the selected objects",
)
add_layout_hotkey_operator(
cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to"
)
class Hotkey(bpy.types.Operator, Operator):
bl_idname = "bim.annotation_hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return tool.Ifc.get()
@classmethod
def description(cls, context, operator):
return operator.description or ""
def _execute(self, context):
self.props = context.scene.BIMAnnotationProperties
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
self.props = context.scene.BIMAnnotationProperties
return self.execute(context)
def draw(self, context):
pass
def hotkey_S_T(self):
props = bpy.context.scene.BIMAnnotationProperties
object_type = props.object_type
related_objects = bpy.context.selected_objects
for related_object in related_objects:
create_annotation()
obj = bpy.context.active_object
bpy.ops.object.mode_set(mode="OBJECT")
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
def hotkey_S_A(self):
create_annotation()
def hotkey_S_E(self):
if not bpy.context.object:
return
if DecoratorData.get_ifc_text_data(bpy.context.object):
bpy.ops.bim.edit_text_popup()
def hotkey_S_G(self):
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcAnnotation"):
continue
related_product = tool.Drawing.get_assigned_product(element)
if not related_product:
continue
related_object = tool.Ifc.get_object(related_product)
tool.Drawing.setup_annotation_object(obj, element.ObjectType, related_object)
def hotkey_S_C(self):
create_annotation_type(bpy.context)
@@ -27,6 +27,8 @@ classes = (
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
operator.OverrideDuplicateMoveLinkedMacro,
operator.OverrideDuplicateMoveMacro,
operator.OverrideJoin,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
@@ -52,6 +54,11 @@ addon_keymaps = []
def register():
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
bpy.types.OBJECT_PT_transform.append(ui.BIM_PT_transform)
@@ -60,9 +67,8 @@ def register():
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_object_join", "J", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.override_object_duplicate_move", "D", "PRESS", shift=True)
kmi.properties.is_interactive = True
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked", "D", "PRESS", alt=True)
kmi = km.keymap_items.new("bim.override_object_duplicate_move_macro", "D", "PRESS", shift=True)
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
@@ -72,6 +78,9 @@ def register():
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
km = wm.keyconfigs.addon.keymaps.new(name="Curve", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS")
@@ -189,7 +189,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
return
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.is_moved(obj):
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
# These objects are parametrically based on an axis and should not be modified as a mesh
@@ -250,6 +251,20 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id())
obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}"
# TODO: In simple scenarios, a type has a ShapeRepresentation of ID
# 123. This is then mapped through mapped representations by
# occurrences, with no cartesian transformation. In this case, the mesh
# data is 100% shared and therefore all have the same mesh name
# referencing ID 123. (i.e. the local origins are shared). However, in
# complex scenarios, occurrences may have their own cartesian
# transformation (via MappingTarget). This will mean that occurrences
# will not share the same mesh data and will instead reference a
# different ShapeRepresentation ID. In this scenario, we have to
# propagate the obj.data back to the type itself and all sibling
# occurrences and accommodate their individual cartesian
# transformations.
core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation)
if obj.data.BIMMeshProperties.ifc_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
@@ -345,52 +360,7 @@ class CopyRepresentation(bpy.types.Operator, Operator):
return r.MappedRepresentation
class OverrideDeleteTrait:
def delete_ifc_object(self, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
return blenderbim.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
IfcStore.delete_element(element)
if obj.users_collection and obj.users_collection[0].name == obj.name:
parent = ifcopenshell.util.element.get_aggregate(element)
if not parent:
parent = ifcopenshell.util.element.get_container(element)
if parent:
parent_obj = tool.Ifc.get_object(parent)
if parent_obj:
parent_collection = bpy.data.collections.get(parent_obj.name)
for child in obj.users_collection[0].children:
parent_collection.children.link(child)
bpy.data.collections.remove(obj.users_collection[0])
if getattr(element, "FillsVoids", None):
self.remove_filling(element)
if element.is_a("IfcOpeningElement"):
if element.HasFillings:
for rel in element.HasFillings:
self.remove_filling(rel.RelatedBuildingElement)
else:
if element.VoidsElements:
self.delete_opening_element(element)
else:
if getattr(element, "HasOpenings", None):
for rel in element.HasOpenings:
self.delete_opening_element(rel.RelatedOpeningElement)
for port in ifcopenshell.util.system.get_ports(element):
self.remove_port(port)
def delete_opening_element(self, element):
bpy.ops.bim.remove_opening(opening_id=element.id())
def remove_filling(self, element):
bpy.ops.bim.remove_filling(filling=element.id())
def remove_port(self, port):
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
class OverrideDelete(bpy.types.Operator):
bl_idname = "bim.override_object_delete"
bl_label = "IFC Delete"
bl_options = {"REGISTER", "UNDO"}
@@ -419,18 +389,16 @@ class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
def _execute(self, context):
for obj in context.selected_objects:
self.delete_ifc_object(obj)
try:
obj.name
if tool.Ifc.get_entity(obj):
tool.Geometry.delete_ifc_object(obj)
else:
bpy.data.objects.remove(obj)
except:
pass
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
return {"FINISHED"}
class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
class OverrideOutlinerDelete(bpy.types.Operator):
bl_idname = "bim.override_outliner_delete"
bl_label = "IFC Delete"
bl_options = {"REGISTER", "UNDO"}
@@ -481,8 +449,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
objects_to_delete.add(bpy.data.objects.get(item.name))
for obj in objects_to_delete:
# This is the only difference
self.delete_ifc_object(obj)
bpy.data.objects.remove(obj)
tool.Geometry.delete_ifc_object(obj)
return {"FINISHED"}
def get_collection_objects_and_children(self, collection):
@@ -498,6 +465,12 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
return {"objects": objects, "children": children}
class OverrideDuplicateMoveMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_macro"
bl_label = "IFC Duplicate Objects"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMove(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move"
bl_label = "IFC Duplicate Objects"
@@ -529,8 +502,6 @@ class OverrideDuplicateMove(bpy.types.Operator):
new_obj.select_set(True)
if new_active_obj:
context.view_layer.objects.active = new_active_obj
if self.is_interactive:
bpy.ops.transform.translate("INVOKE_DEFAULT")
return {"FINISHED"}
def _execute(self, context):
@@ -551,17 +522,26 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
if array_pset:
array_pset = tool.Ifc.get().by_id(array_pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
old_to_new[tool.Ifc.get_entity(obj)] = [new]
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
if self.is_interactive:
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_linked_macro"
bl_label = "IFC Duplicate Linked"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMoveLinked(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move_linked"
bl_label = "IFC Duplicate Linked"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
@@ -586,7 +566,6 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
new_obj.select_set(True)
if new_active_obj:
context.view_layer.objects.active = new_active_obj
bpy.ops.transform.translate("INVOKE_DEFAULT")
return {"FINISHED"}
def _execute(self, context):
@@ -596,6 +575,8 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
old_to_new = {}
for obj in context.selected_objects:
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
if obj == context.active_object:
self.new_active_obj = new_obj
for collection in obj.users_collection:
@@ -605,10 +586,13 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
if array_pset:
array_pset = tool.Ifc.get().by_id(array_pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
old_to_new[tool.Ifc.get_entity(obj)] = new
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
@@ -616,6 +600,12 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class OverrideJoin(bpy.types.Operator, Operator):
bl_idname = "bim.override_object_join"
bl_label = "IFC Join"
bl_options = {"REGISTER", "UNDO"}
def invoke(self, context, event):
if not tool.Ifc.get():
return bpy.ops.object.join()
return self.execute(context)
def _execute(self, context):
if not tool.Ifc.get():
@@ -638,7 +628,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
continue
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
bpy.ops.object.join()
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
elif representation.RepresentationType == "SweptSolid":
@@ -649,7 +639,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
continue
element = tool.Ifc.get_entity(obj)
# Non IFC elements cannot be join since we cannot guarantee SweptSolid compliance
# Non IFC elements cannot be joined since we cannot guarantee SweptSolid compliance
if not element:
obj.select_set(False)
continue
@@ -664,6 +654,9 @@ class OverrideJoin(bpy.types.Operator, Operator):
for item in obj_rep.Items:
copied_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
for style in item.StyledByItem:
copied_style = ifcopenshell.util.element.copy(tool.Ifc.get(), style)
copied_style.Item = copied_item
if copied_item.Position:
position = ifcopenshell.util.placement.get_axis2placement(copied_item.Position)
else:
@@ -676,7 +669,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 0][:3]]),
)
items.append(copied_item)
tool.Ifc.delete(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
representation.Items = items
bpy.ops.object.join()
core.switch_representation(
@@ -696,13 +689,14 @@ class OverrideJoin(bpy.types.Operator, Operator):
continue
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
bpy.ops.object.join()
class OverridePasteBuffer(bpy.types.Operator):
bl_idname = "bim.override_paste_buffer"
bl_label = "IFC Paste BIM Objects"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.view3d.pastebuffer()
@@ -721,10 +715,11 @@ class OverrideModeSetEdit(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
objs = context.selected_objects or [context.active_object]
objs = context.selected_objects or ([context.active_object] if context.active_object else [])
active_obj = context.active_object
if context.active_object:
context.active_object.select_set(True)
edited_objs = []
for obj in objs:
if not obj:
continue
@@ -742,26 +737,43 @@ class OverrideModeSetEdit(bpy.types.Operator):
obj.select_set(False)
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in ("Tessellation", "Brep"):
if element.HasOpenings:
# Mesh elements with openings must disable openings
# so that you can edit the original topology.
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=False,
)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
if not context.selected_objects:
return {"FINISHED"}
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
representation = tool.Geometry.get_active_representation(obj)
if not representation:
continue
if tool.Geometry.is_meshlike(representation):
if getattr(element, "HasOpenings", None):
# Mesh elements with openings must disable openings
# so that you can edit the original topology.
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=False,
)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
else:
obj.select_set(False)
continue
if not context.selected_objects or len(context.selected_objects) != len(objs):
# We are trying to edit at least one non-mesh-like object : Display a hint to the user
self.report({"INFO"}, "Only mesh-compatible representations may be edited in edit mode.")
if context.active_object not in context.selected_objects:
# The active object is non-mesh-like. Set a valid object (or None) as active
context.view_layer.objects.active = context.selected_objects[0] if context.selected_objects else None
if context.active_object:
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
else:
# restore the selection if nothing worked
for obj in objs:
obj.select_set(True)
context.view_layer.objects.active = active_obj
return {"FINISHED"}
def invoke(self, context, event):
@@ -783,7 +795,7 @@ class OverrideModeSetObject(bpy.types.Operator):
for obj in self.edited_objs:
if self.should_save:
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
if tool.Ifc.get_entity(obj).HasOpenings:
if getattr(tool.Ifc.get_entity(obj), "HasOpenings", False):
self.reload_representation(obj)
else:
self.reload_representation(obj)
@@ -806,10 +818,17 @@ class OverrideModeSetObject(bpy.types.Operator):
)
def draw(self, context):
row = self.layout.row(align=True)
row.prop(self, "should_save")
if self.is_valid:
row = self.layout.row()
row.prop(self, "should_save")
else:
row = self.layout.row()
row.label(text="No Geometry Found: Object will revert to previous state.")
def invoke(self, context, event):
self.is_valid = True
self.should_save = True
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
if not tool.Ifc.get():
@@ -829,13 +848,15 @@ class OverrideModeSetObject(bpy.types.Operator):
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
if not tool.Geometry.has_geometric_data(obj):
self.is_valid = False
self.should_save = False
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in (
"Tessellation",
"Brep",
if tool.Geometry.is_meshlike(
representation
) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
elif element.HasOpenings:
elif getattr(element, "HasOpenings", None):
self.unchanged_objs_with_openings.append(obj)
if self.edited_objs:
@@ -49,7 +49,7 @@ class BIM_OT_cityjson2ifc(Operator):
class BIM_OT_find_cityjson_lod(Operator):
bl_idname = "bim.find_cityjson_lod"
bl_label = "Find LODs in CityJSON file"
bl_label = "Find LODs in CityJSON File"
bl_context = "scene"
def execute(self, context):
@@ -0,0 +1,44 @@
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# 2023 Bruno Postle <bruno@postle.net>, Bruno Perdigão <brunoperdigao@tutanota.com>,
# Massimo Fabbro <maxfb87@yahoo.it>
import bpy
from . import ui, prop, operator
classes = (
operator.AddFileToRepo,
operator.CommitChanges,
operator.CreateRepo,
operator.DiscardUncommitted,
operator.DisplayRevision,
operator.DisplayUncommitted,
operator.Merge,
operator.RefreshGit,
operator.SwitchRevision,
prop.IfcGitListItem,
prop.IfcGitProperties,
ui.IFCGIT_PT_panel,
ui.COMMIT_UL_List,
)
def register():
bpy.types.Scene.IfcGitProperties = bpy.props.PointerProperty(type=prop.IfcGitProperties)
def unregister():
del bpy.types.Scene.IfcGitProperties
@@ -0,0 +1,123 @@
import bpy
import os
import shutil
# import tool
import blenderbim.tool as tool
def refresh():
IfcGitData.is_loaded = False
class IfcGitData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {
"repo": cls.repo(),
"branch_names": cls.branch_names(),
"path_ifc": cls.path_ifc(),
"branches_by_hexsha": cls.branches_by_hexsha(),
"tags_by_hexsha": cls.tags_by_hexsha(),
"name_ifc": cls.name_ifc(),
"dir_name": cls.dir_name(),
"base_name": cls.base_name(),
"is_dirty": cls.is_dirty(),
"commit": cls.commit(),
"current_revision": cls.current_revision(),
"git_exe": cls.git_exe(),
"ifcmerge_exe": cls.ifcmerge_exe(),
}
cls.is_loaded = True
@classmethod
def repo(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return tool.IfcGit.repo_from_path(path_ifc)
return None
@classmethod
def branch_names(cls):
return []
@classmethod
def path_ifc(cls):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return tool.Ifc.get_path()
return None
@classmethod
def branches_by_hexsha(cls):
try:
if tool.IfcGitRepo.repo.branches:
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
except:
return {}
@classmethod
def tags_by_hexsha(cls):
if tool.IfcGitRepo.repo:
return tool.IfcGit.tags_by_hexsha(tool.IfcGitRepo.repo)
return {}
@classmethod
def name_ifc(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if tool.IfcGitRepo.repo and os.path.isfile(path_ifc):
working_dir = tool.IfcGitRepo.repo.working_dir
return os.path.relpath(path_ifc, working_dir)
return None
@classmethod
def dir_name(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return os.path.dirname(path_ifc)
return None
@classmethod
def base_name(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return os.path.basename(path_ifc)
return None
@classmethod
def is_dirty(cls):
if cls.repo() and cls.git_exe():
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return cls.repo().is_dirty(path=path_ifc)
return False
@classmethod
def commit(cls):
props = bpy.context.scene.IfcGitProperties
if len(props.ifcgit_commits) > 0:
item = props.ifcgit_commits[props.commit_index]
if cls.repo():
return cls.repo().commit(rev=item.hexsha)
@classmethod
def current_revision(cls):
props = bpy.context.scene.IfcGitProperties
if len(props.ifcgit_commits) > 0:
return tool.IfcGitRepo.repo.commit()
@classmethod
def git_exe(cls):
return shutil.which("git")
@classmethod
def ifcmerge_exe(cls):
return shutil.which("ifcmerge")
@@ -0,0 +1,194 @@
import os
import re
import bpy
import blenderbim.core.ifcgit as core
import blenderbim.tool as tool
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
class CreateRepo(bpy.types.Operator):
"""Initialise a Git repository"""
bl_label = "Create Git repository"
bl_idname = "ifcgit.createrepo"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
path_ifc = IfcGitData.data["path_ifc"]
if not os.path.isfile(path_ifc):
return False
if IfcGitData.data["repo"]:
# repo already exists
return False
if re.match("^/home/[^/]+/?$", os.path.dirname(path_ifc)):
# don't make ${HOME} a repo
return False
return True
def execute(self, context):
core.create_repo(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
class AddFileToRepo(bpy.types.Operator):
"""Add a file to a repository"""
bl_label = "Add file to repository"
bl_idname = "ifcgit.addfile"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
path_ifc = IfcGitData.data["path_ifc"]
if not os.path.isfile(path_ifc):
return False
if not IfcGitData.data["repo"]:
# repo doesn't exist
return False
return True
def execute(self, context):
core.add_file(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
class DiscardUncommitted(bpy.types.Operator):
"""Discard saved changes and update to HEAD"""
bl_label = "Discard uncommitted changes"
bl_idname = "ifcgit.discard"
bl_options = {"REGISTER"}
def execute(self, context):
core.discard_uncomitted(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
class CommitChanges(bpy.types.Operator):
"""Commit current saved changes"""
bl_label = "Commit changes"
bl_idname = "ifcgit.commit_changes"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
repo = IfcGitData.data["repo"]
if props.commit_message == "":
return False
if (
repo
and repo.head.is_detached
and (
not tool.is_valid_ref_format(props.new_branch_name)
or props.new_branch_name in [branch.name for branch in repo.branches]
)
):
cls.poll_message_set(
"The new branch name is invalid, please insert a valid branch name (eg. with no spaces, ...)"
)
return False
return True
def execute(self, context):
repo = IfcGitData.data["repo"]
core.commit_changes(tool.IfcGit, tool.Ifc, repo, context)
bpy.ops.ifcgit.refresh()
refresh()
return {"FINISHED"}
class RefreshGit(bpy.types.Operator):
"""Refresh revision list"""
bl_label = ""
bl_idname = "ifcgit.refresh"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
repo = IfcGitData.data["repo"]
if repo != None and repo.heads:
return True
return False
def execute(self, context):
repo = IfcGitData.data["repo"]
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
refresh()
return {"FINISHED"}
class DisplayRevision(bpy.types.Operator):
"""Colourise objects by selected revision"""
bl_label = ""
bl_idname = "ifcgit.display_revision"
bl_options = {"REGISTER"}
def execute(self, context):
core.colourise_revision(tool.IfcGit, context)
refresh()
return {"FINISHED"}
class DisplayUncommitted(bpy.types.Operator):
"""Colourise uncommitted objects"""
bl_label = "Show uncommitted changes"
bl_idname = "ifcgit.display_uncommitted"
bl_options = {"REGISTER"}
def execute(self, context):
repo = IfcGitData.data["repo"]
core.colourise_uncommitted(tool.IfcGit, tool.Ifc, repo)
refresh()
return {"FINISHED"}
class SwitchRevision(bpy.types.Operator):
"""Switches the repository to the selected revision and reloads the IFC file"""
bl_label = ""
bl_idname = "ifcgit.switch_revision"
bl_options = {"REGISTER"}
def execute(self, context):
core.switch_revision(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
class Merge(bpy.types.Operator):
"""Merges the selected branch into working branch"""
bl_label = "Merge this branch"
bl_idname = "ifcgit.merge"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
if IfcGitData.data["ifcmerge_exe"]:
return True
return False
def execute(self, context):
if core.merge_branch(tool.IfcGit, tool.Ifc, self):
refresh()
return {"FINISHED"}
else:
return {"CANCELLED"}
@@ -0,0 +1,72 @@
import bpy
from bpy.types import PropertyGroup
from bpy.props import (
StringProperty,
BoolProperty,
CollectionProperty,
IntProperty,
EnumProperty,
)
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
def git_branches(self, context):
"""branches enum"""
# NOTE "Python must keep a reference to the strings returned by
# the callback or Blender will misbehave or even crash"
IfcGitData.data["branch_names"] = sorted([branch.name for branch in IfcGitData.data["repo"].heads])
if "main" in IfcGitData.data["branch_names"]:
IfcGitData.data["branch_names"].remove("main")
IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"]
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
def update_revlist(self, context):
"""wrapper to trigger update of the revision list"""
bpy.ops.ifcgit.refresh()
props = context.scene.IfcGitProperties
props.commit_index = 0
class IfcGitListItem(PropertyGroup):
"""Group of properties representing an item in the list."""
hexsha: StringProperty(
name="Git hash",
description="checksum for this commit",
default="Uncommitted data!",
)
relevant: BoolProperty(
name="Is relevant",
description="does this commit reference our ifc file",
default=False,
)
class IfcGitProperties(PropertyGroup):
ifcgit_commits: CollectionProperty(type=IfcGitListItem, name="List of git items")
commit_index: IntProperty(name="Index for my_list", default=0)
commit_message: StringProperty(
name="Commit message",
description="A human readable description of these changes",
default="",
)
new_branch_name: StringProperty(
name="New branch name",
description="A short name used to refer to this branch",
default="",
)
display_branch: EnumProperty(items=git_branches, update=update_revlist)
ifcgit_filter: EnumProperty(
items=[
("all", "All", "All revisions"),
("tagged", "Tagged", "Tagged revisions"),
("relevant", "Relevant", "Revisions for this project"),
],
update=update_revlist,
)
@@ -0,0 +1,166 @@
import bpy
import time
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
class IFCGIT_PT_panel(bpy.types.Panel):
"""Scene Properties panel to interact with IFC repository data"""
bl_label = "IFC Git"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_project_info"
def draw(self, context):
if not IfcGitData.is_loaded:
IfcGitData.load()
layout = self.layout
path_ifc = IfcGitData.data["path_ifc"]
if not IfcGitData.data["git_exe"]:
row = layout.row()
row.label(text="Git is not installed", icon="ERROR")
return
props = context.scene.IfcGitProperties
# TODO if file isn't saved, offer to save to disk
row = layout.row()
if path_ifc:
if IfcGitData.data["repo"]:
name_ifc = IfcGitData.data["name_ifc"]
row.label(text=IfcGitData.data["repo"].working_dir, icon="SYSTEM")
if name_ifc in IfcGitData.data["repo"].untracked_files:
row.operator(
"ifcgit.addfile",
text="Add '" + name_ifc + "' to repository",
icon="FILE",
)
else:
row.label(text=name_ifc, icon="FILE")
else:
row.operator(
"ifcgit.createrepo",
text="Create '" + IfcGitData.data["dir_name"] + "' repository",
icon="SYSTEM",
)
row.label(text=IfcGitData.data["base_name"], icon="FILE")
return
else:
row.label(text="No Git repository found", icon="SYSTEM")
row.label(text="No IFC project saved", icon="FILE")
return
is_dirty = IfcGitData.data["is_dirty"]
if is_dirty:
row = layout.row()
row.label(text="Saved changes have not been committed", icon="ERROR")
row = layout.row()
row.operator("ifcgit.display_uncommitted", icon="SELECT_DIFFERENCE")
row.operator("ifcgit.discard", icon="TRASH")
row = layout.row()
row.prop(props, "commit_message")
if IfcGitData.data["repo"].head.is_detached:
row = layout.row()
row.label(text="HEAD is detached, commit will create a branch", icon="ERROR")
row.prop(props, "new_branch_name")
row = layout.row()
row.operator("ifcgit.commit_changes", icon="GREASEPENCIL")
row = layout.row()
if IfcGitData.data["repo"].head.is_detached:
row.label(text="Working branch: Detached HEAD")
else:
row.label(text="Working branch: " + IfcGitData.data["repo"].active_branch.name)
grouped = layout.row()
column = grouped.column()
row = column.row()
row.prop(props, "display_branch", text="Browse branch")
row.prop(props, "ifcgit_filter", text="Filter revisions")
row = column.row()
row.template_list(
"COMMIT_UL_List",
"The_List",
props,
"ifcgit_commits",
props,
"commit_index",
)
column = grouped.column()
row = column.row()
row.operator("ifcgit.refresh", icon="FILE_REFRESH")
if not is_dirty:
row = column.row()
row.operator("ifcgit.display_revision", icon="SELECT_DIFFERENCE")
row = column.row()
row.operator("ifcgit.switch_revision", icon="CURRENT_FILE")
# TODO operator to tag selected
row = column.row()
row.operator("ifcgit.merge", icon="EXPERIMENTAL", text="")
if not props.ifcgit_commits:
return
item = props.ifcgit_commits[props.commit_index]
commit = IfcGitData.data["commit"]
if not item.relevant:
row = layout.row()
row.label(text="Revision unrelated to current IFC project", icon="ERROR")
box = layout.box()
column = box.column(align=True)
row = column.row()
row.label(text=commit.hexsha)
row = column.row()
row.label(text=commit.author.name + " <" + commit.author.email + ">")
row = column.row()
row.label(text=commit.message)
class COMMIT_UL_List(bpy.types.UIList):
"""List of Git commits"""
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
props = context.scene.IfcGitProperties
current_revision = IfcGitData.data["current_revision"]
# TODO Figure how this "item" can be acesse in "data.py"
# so it's possible to move the ".commit"
commit = IfcGitData.data["repo"].commit(rev=item.hexsha)
lookup = IfcGitData.data["branches_by_hexsha"]
refs = ""
if item.hexsha in lookup:
for branch in lookup[item.hexsha]:
if branch.name == props.display_branch:
refs = "[" + branch.name + "] "
lookup = IfcGitData.data["tags_by_hexsha"]
if item.hexsha in lookup:
for tag in lookup[item.hexsha]:
refs += "{" + tag.name + "} "
if commit == current_revision:
layout.label(text="[HEAD] " + refs + commit.message, icon="DECORATE_KEYFRAME")
else:
layout.label(text=refs + commit.message, icon="DECORATE_ANIMATE")
layout.label(text=time.strftime("%c", time.localtime(commit.committed_date)))
@@ -24,6 +24,7 @@ import ifcopenshell.util.attribute
import ifcopenshell.util.representation
import blenderbim.bim.helper
import blenderbim.tool as tool
import blenderbim.core.style
import blenderbim.core.material as core
import blenderbim.bim.module.model.profile as model_profile
from blenderbim.bim.module.material.prop import purge as material_prop_purge
@@ -515,7 +516,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_set_item_profile"
bl_label = "Enable Editing Material Set Item"
bl_label = "Enable Editing Material Set Item Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
@@ -531,7 +532,7 @@ class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
class DisableEditingMaterialSetItemProfile(bpy.types.Operator):
bl_idname = "bim.disable_editing_material_set_item_profile"
bl_label = "Disable Editing Material Set Item"
bl_label = "Disable Editing Material Set Item Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
@@ -666,8 +667,12 @@ class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
blender_material = context.active_object.active_material
material = tool.Ifc.get_entity(blender_material)
if tool.Ifc.has_changed_shading(blender_material):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=blender_material)
copied_material = ifcopenshell.api.run("material.copy_material", tool.Ifc.get(), material=material)
copied_blender_material = bpy.data.materials.new(blender_material.name)
copied_blender_material = blender_material.copy()
copied_style = self.get_style(copied_material)
tool.Ifc.link(copied_material, copied_blender_material)
if copied_style:
@@ -196,7 +196,7 @@ class BIM_PT_object_material(Panel):
else:
row = self.layout.row(align=True)
if ObjectMaterialData.data["set_item_name"] == "profile":
row.prop(self.mprops, "profiles", icon="ITALIC", text="")
prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="")
prop_with_search(row, self.props, "material", icon="MATERIAL", text="")
op = row.operator(f"bim.add_{ObjectMaterialData.data['set_item_name']}", icon="ADD", text="")
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_set", ObjectMaterialData.data["set"]["id"])
@@ -230,11 +230,11 @@ class BIM_PT_object_material(Panel):
draw_attributes(self.props.material_set_item_attributes, box)
row = box.row()
row.prop(self.props, "material_set_item_material", icon="MATERIAL", text="Material")
prop_with_search(row, self.props, "material_set_item_material", icon="MATERIAL", text="Material")
if ObjectMaterialData.data["set_item_name"] == "profile":
row = box.row()
row.prop(self.mprops, "profiles", icon="ITALIC", text="Profile")
prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="Profile")
def draw_read_only_set_item_ui(self, set_item, index, is_first=False, is_last=False):
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
@@ -47,6 +47,9 @@ classes = (
array.EnableEditingArray,
array.RemoveArray,
array.SelectArrayParent,
array.Input3DCursorXArray,
array.Input3DCursorYArray,
array.Input3DCursorZArray,
product.AddConstrTypeInstance,
product.AddEmptyType,
product.AlignProduct,
@@ -93,9 +96,6 @@ classes = (
slab.ResetVertex,
slab.SetArcIndex,
space.GenerateSpace,
prop.ConstrTypeInfo,
prop.ConstrClassInfo,
prop.ConstrBrowserState,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -113,7 +113,6 @@ classes = (
ui.BIM_PT_railing,
ui.BIM_PT_roof,
ui.LaunchTypeManager,
ui.HelpConstrTypes,
ui.BIM_MT_model,
grid.BIM_OT_add_object,
stair.BIM_OT_add_object,
@@ -149,6 +148,7 @@ classes = (
railing.AddRailing,
railing.CancelEditingRailing,
railing.FinishEditingRailing,
railing.FlipRailingPathOrder,
railing.EnableEditingRailing,
railing.CancelEditingRailingPath,
railing.FinishEditingRailingPath,
@@ -22,19 +22,28 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.tool as tool
from mathutils import Vector
from mathutils import Vector, Matrix
class AddArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_array"
bl_label = "Add Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0, "use_local_space": True}
array = {
"children": [],
"count": 1,
"x": 0.0,
"y": 0.0,
"z": 0.0,
"use_local_space": True,
"sync_children": False,
"method": "OFFSET",
}
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
@@ -58,7 +67,7 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_array"
bl_label = "Disable Editing Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
context.active_object.BIMArrayProperties.is_editing = -1
@@ -68,7 +77,7 @@ class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_array"
bl_label = "Enable Editing Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.IntProperty()
def _execute(self, context):
@@ -81,6 +90,8 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
props.y = data["y"]
props.z = data["z"]
props.use_local_space = data.get("use_local_space", False)
props.sync_children = data.get("sync_children", False)
props.method = data.get("method", "OFFSET")
props.is_editing = self.item
return {"FINISHED"}
@@ -88,7 +99,7 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
class EditArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_array"
bl_label = "Edit Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.IntProperty()
def _execute(self, context):
@@ -105,6 +116,8 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
"y": props.y,
"z": props.z,
"use_local_space": props.use_local_space,
"sync_children": props.sync_children,
"method": props.method,
}
props.is_editing = -1
@@ -125,7 +138,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_array"
bl_label = "Remove Array"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.IntProperty()
def _execute(self, context):
@@ -173,3 +186,48 @@ class SelectArrayParent(bpy.types.Operator):
context.view_layer.objects.active = obj
obj.select_set(True)
return {"FINISHED"}
class Input3DCursorXArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_x_array"
bl_label = "Get 3d Cursor X Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.x = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).x
else:
props.x = cursor.location.x - obj.location.x
return {"FINISHED"}
class Input3DCursorYArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_y_array"
bl_label = "Get 3d Cursor Y Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.y = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).y
else:
props.y = cursor.location.y - obj.location.y
return {"FINISHED"}
class Input3DCursorZArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_z_array"
bl_label = "Get 3d Cursor Z Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.z = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).z
else:
props.z = cursor.location.z - obj.location.z
return {"FINISHED"}
@@ -24,8 +24,6 @@ import ifcopenshell
import ifcopenshell.util.element
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
def refresh():
@@ -49,9 +47,8 @@ class AuthoringData:
def load(cls):
cls.is_loaded = True
cls.props = bpy.context.scene.BIMModelProperties
cls.load_ifc_classes()
cls.load_relating_types()
cls.load_relating_types_browser()
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["relating_type_id"] = cls.relating_type_id()
cls.data["type_class"] = cls.type_class()
cls.data["type_predefined_type"] = cls.type_predefined_type()
cls.data["total_types"] = cls.total_types()
@@ -64,6 +61,7 @@ class AuthoringData:
cls.data["has_visible_openings"] = cls.has_visible_openings()
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()
@classmethod
def type_class(cls):
@@ -99,23 +97,11 @@ class AuthoringData:
@classmethod
def type_thumbnail(cls):
if not cls.data["relating_types_ids"]:
if not cls.data["relating_type_id"]:
return 0
element = tool.Ifc.get().by_id(int(cls.props.relating_type_id))
return cls.type_thumbnails.get(element.id(), None) or 0
@classmethod
def load_ifc_classes(cls):
cls.data["ifc_classes"] = cls.ifc_classes()
@classmethod
def load_relating_types(cls):
cls.data["relating_types_ids"] = cls.relating_types()
@classmethod
def load_relating_types_browser(cls):
cls.data["relating_types_ids_browser"] = cls.relating_types_browser()
@classmethod
def total_types(cls):
type_class = cls.props.type_class
@@ -187,169 +173,45 @@ class AuthoringData:
if element:
return tool.Model.get_usage_type(element)
@classmethod
def active_representation_type(cls):
if bpy.context.active_object:
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
if representation:
return representation.RepresentationType
@classmethod
def ifc_classes(cls):
results = []
classes = {
e.is_a()
for e in tool.Ifc.get().by_type("IfcElementType")
+ tool.Ifc.get().by_type("IfcDoorStyle")
+ tool.Ifc.get().by_type("IfcWindowStyle")
+ tool.Ifc.get().by_type("IfcSpaceType")
e.is_a() for e in (tool.Ifc.get().by_type("IfcElementType") + tool.Ifc.get().by_type("IfcSpaceType"))
}
if tool.Ifc.get_schema() in ("IFC2X3", "IFC4"):
classes.update(
{e.is_a() for e in (tool.Ifc.get().by_type("IfcDoorStyle") + tool.Ifc.get().by_type("IfcWindowStyle"))}
)
results.extend([(c, c, "") for c in sorted(classes)])
return results
@classmethod
def constr_class_entities(cls, ifc_class=None):
def relating_type_id(cls):
ifc_classes = cls.data["ifc_classes"]
if not ifc_classes:
return []
results = []
if ifc_class is None:
ifc_class = cls.props.ifc_class
ifc_class = cls.props.ifc_class
if not ifc_class and ifc_classes:
ifc_class = ifc_classes[0][0]
if ifc_class:
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name or "Unnamed")
results.extend(elements)
return results
return [
(str(e.id()), e.Name or "Unnamed", e.Description or "")
for e in results
]
return []
@classmethod
def relating_types(cls, ifc_class=None):
return [
(str(e.id()), e.Name or "Unnamed", e.Description or "")
for e in cls.constr_class_entities(ifc_class=ifc_class)
]
@classmethod
def relating_types_browser(cls):
if cls.data["ifc_classes"]:
return cls.relating_types(ifc_class=cls.props.ifc_class_browser)
@classmethod
def new_constr_class_info(cls, ifc_class):
if ifc_class not in cls.props.constr_classes:
cls.props.constr_classes.add().name = ifc_class
return cls.props.constr_classes[ifc_class]
@classmethod
def assetize_constr_class(cls, ifc_class=None):
selected_ifc_class = cls.props.ifc_class
selected_relating_type_id = cls.props.relating_type_id
if ifc_class is None:
ifc_class = cls.props.ifc_class_browser
if cls.constr_class_info(ifc_class) is None:
cls.new_constr_class_info(ifc_class)
constr_class_occurrences = cls.constr_class_entities(ifc_class)
constr_classes = cls.props.constr_classes
for constr_class_entity in constr_class_occurrences:
if (
ifc_class not in constr_classes
or constr_class_entity.Name not in constr_classes[ifc_class].constr_types
):
obj = tool.Ifc.get_object(constr_class_entity)
cls.assetize_object(obj, ifc_class, constr_class_entity)
cls.constr_class_info(ifc_class).fully_loaded = True
cls.props.updating = True
cls.props.ifc_class = selected_ifc_class
cls.props.relating_type_id = selected_relating_type_id
cls.props.updating = False
@classmethod
def assetize_object(cls, obj, ifc_class, ifc_class_entity, from_selection=False):
relating_type_id = ifc_class_entity.id()
to_be_deleted = False
if obj.type == "EMPTY":
kwargs = {}
if not from_selection:
kwargs.update({"ifc_class": ifc_class, "relating_type_id": relating_type_id})
new_obj = cls.new_relating_type(**kwargs)
if new_obj is not None:
to_be_deleted = True
obj = new_obj
obj.hide_set(True)
obj.asset_mark()
obj.asset_generate_preview()
blender33_or_above = bpy.app.version >= (3, 3, 0)
interval = 1e-4
def wait_for_asset_previews_generation(check_interval_seconds=interval):
if blender33_or_above and bpy.app.is_job_running("RENDER_PREVIEW"):
return check_interval_seconds
else:
if ifc_class not in cls.props.constr_classes:
cls.props.constr_classes.add().name = ifc_class
constr_class_info = cls.props.constr_classes[ifc_class]
# relating_type = cls.relating_type_name_by_id(ifc_class, relating_type_id)
if str(relating_type_id) not in constr_class_info.constr_types:
constr_class_info.constr_types.add().name = str(relating_type_id)
relating_type_info = constr_class_info.constr_types[str(relating_type_id)]
relating_type_info.object = obj
relating_type_info.icon_id = obj.preview.icon_id
if to_be_deleted:
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
tool.Ifc.unlink(obj=obj)
for collection in obj.users_collection:
collection.objects.unlink(obj)
return None
first_interval = 0 if blender33_or_above else interval
bpy.app.timers.register(wait_for_asset_previews_generation, first_interval=first_interval)
@classmethod
def assetize_relating_type_from_selection(cls, browser=False):
ifc_class = cls.props.ifc_class_browser if browser else cls.props.ifc_class
relating_type_id = cls.props.relating_type_id_browser if browser else cls.props.relating_type_id
constr_class_occurrences = cls.constr_class_entities(ifc_class=ifc_class)
constr_class_occurrences = [
entity for entity in constr_class_occurrences if entity.id() == int(relating_type_id)
]
if len(constr_class_occurrences) == 0:
raise ConstrTypeEntityNotFound()
constr_class_entity = constr_class_occurrences[0]
if (obj := tool.Ifc.get_object(constr_class_entity)) is None:
raise ConstrTypeEntityNotFound()
cls.assetize_object(obj, ifc_class, constr_class_entity, from_selection=True)
@staticmethod
def constr_class_info(ifc_class):
props = bpy.context.scene.BIMModelProperties
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
@classmethod
def new_relating_type(cls, ifc_class=None, relating_type_id=None):
if ifc_class is None:
bpy.ops.bim.add_constr_type_instance(
ifc_class=cls.props.ifc_class, relating_type_id=int(cls.props.relating_type_id)
)
else:
cls.props.updating = True
cls.props.ifc_class = ifc_class
cls.props.relating_type_id = str(relating_type_id)
cls.props.updating = False
bpy.ops.bim.add_constr_type_instance()
return bpy.context.selected_objects[-1]
@staticmethod
def relating_type_name_by_id(ifc_class, relating_type_id):
file = IfcStore.get_file()
try:
constr_class_entity = file.by_id(int(relating_type_id))
except (RuntimeError, ValueError):
return None
return constr_class_entity.Name if constr_class_entity.is_a() == ifc_class else None
@classmethod
def relating_type_id_by_name(cls, ifc_class, relating_type):
relating_types = [ct[0] for ct in cls.relating_types(ifc_class=ifc_class) if ct[1] == relating_type]
return None if len(relating_types) == 0 else relating_types[0]
class ArrayData:
data = {}
@@ -17,25 +17,22 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import gpu
import bgl
import bmesh
import blenderbim.tool as tool
from math import pi, degrees, sin, cos, radians
from math import sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
white = (1, 1, 1, 1)
lightgrey = (0.7, 0.7, 0.7, 1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
blue = (0.157, 0.565, 1, 1)
grey = (0.2, 0.2, 0.2, 1)
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
faces_color = (0.494, 0.540, 0.593, 1)
preview_edges_color = (0.130, 0.141, 0.371, 1)
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
@@ -71,13 +68,11 @@ class ProfileDecorator:
pass
cls.installed = None
def create_batch(self, shader_type, content_pos, color, indices=None, bind=True):
batch = batch_for_shader(self.shader, shader_type, {"pos": content_pos}, indices=indices)
# TODO: what's bind is for?
if bind:
self.shader.bind()
self.shader.uniform_float("color", color)
batch.draw(self.shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_faces(self, bm, vertices_coords):
"""mutates original bm (triangulates it)
@@ -87,9 +82,15 @@ class ProfileDecorator:
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
self.create_batch("TRIS", vertices_coords, faces_color, face_indices)
faces_color = transparent_color(self.model_props.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
obj = context.active_object
if obj.mode != "EDIT":
@@ -103,20 +104,8 @@ class ProfileDecorator:
else:
bm = bmesh.from_edit_mesh(obj.data)
def gl_init(use_bgl=False):
# TODO: remove as deprecated?
if use_bgl:
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
else:
gpu.state.line_width_set(2)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
bgl.glEnable(bgl.GL_LINE_SMOOTH)
gl_init(True)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
### Actually drawing
all_vertices = []
@@ -202,22 +191,31 @@ class ProfileDecorator:
unselected_edges.append(edge_indices)
### Actually drawing
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader.bind()
# Draw faces
if draw_faces:
self.draw_faces(bm, all_vertices)
self.create_batch("LINES", all_vertices, lightgrey, unselected_edges)
self.create_batch("LINES", all_vertices, green, selected_edges)
self.create_batch("LINES", all_vertices, grey, arc_edges)
self.create_batch("LINES", all_vertices, preview_edges_color, preview_edges)
self.create_batch("LINES", all_vertices, blue, roof_angle_edges)
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
self.draw_batch("LINES", all_vertices, UNSPECIAL_ELEMENT_COLOR, arc_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
self.create_batch("POINTS", unselected_vertices, lightgrey)
self.create_batch("POINTS", error_vertices, red)
self.create_batch("POINTS", special_vertices, blue)
self.create_batch("POINTS", selected_vertices, green)
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
self.draw_batch("POINTS", special_vertices, self.model_props.decorator_color_special)
self.draw_batch("POINTS", selected_vertices, self.model_props.decorator_color_selected)
# Draw arcs
arc_centroids = []
@@ -242,9 +240,9 @@ class ProfileDecorator:
arc_centroids.append(tuple(centroid))
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
self.create_batch("POINTS", arc_centroids, grey, bind=False)
self.draw_batch("POINTS", arc_centroids, UNSPECIAL_ELEMENT_COLOR)
for verts, edges in arc_segments:
self.create_batch("LINES", verts, blue, edges, bind=False)
self.draw_batch("LINES", verts, special_elements_color, edges)
# Draw circles
circle_centroids = []
@@ -263,9 +261,9 @@ class ProfileDecorator:
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
circle_segments.append(segments)
self.create_batch("POINTS", circle_centroids, grey, bind=False)
self.draw_batch("POINTS", circle_centroids, UNSPECIAL_ELEMENT_COLOR)
for verts, edges in circle_segments:
self.create_batch("LINES", verts, blue, edges, bind=False)
self.draw_batch("LINES", verts, special_elements_color, edges)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
@@ -43,6 +43,7 @@ def update_door_modifier_representation(context):
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
sliding_door = "SLIDING" in props.door_type
representation_data = {
"operation_type": props.door_type,
@@ -87,13 +88,37 @@ def update_door_modifier_representation(context):
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# PLAN_VIEW representation
plan = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
if plan:
representation_data["context"] = plan
# Body/PLAN_VIEW representation
plan_body = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
if plan_body:
representation_data["context"] = plan_body
plan_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
tool.Model.replace_object_ifc_representation(plan_body, obj, plan_representation)
# Annotation/PLAN_VIEW representation
plan_annotation = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Annotation", "PLAN_VIEW")
if plan_annotation:
if not sliding_door:
# only sliding doors have Annotation/PLAN_VIEW
# for other types we just check for old representation and remove it if it's there
old_representation = ifcopenshell.util.representation.get_representation(
element, "Plan", "Annotation", "PLAN_VIEW"
)
if old_representation:
core.remove_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=old_representation,
)
else:
representation_data["context"] = plan_annotation
plan_representation = ifcopenshell.api.run(
"geometry.add_door_representation", ifc_file, **representation_data
)
tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation)
if plan_body or plan_annotation:
# adding switch representation at the end instead of changing order of representations
# to prevent #2744
core.switch_representation(
@@ -263,13 +288,17 @@ def update_door_modifier_bmesh(context):
door_type = props.door_type
double_swing_door = "DOUBLE_SWING" in door_type
double_door = "DOUBLE_DOOR" in door_type
sliding_door = "SLIDING" in door_type
# lining params
lining_depth = props.lining_depth * si_conversion
lining_thickness_default = props.lining_thickness * si_conversion
lining_offset = props.lining_offset * si_conversion
lining_to_panel_offset_x = props.lining_to_panel_offset_x * si_conversion
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion
lining_to_panel_offset_x = (
props.lining_to_panel_offset_x * si_conversion if not sliding_door else lining_thickness_default
)
panel_depth = props.panel_depth * si_conversion
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion if not sliding_door else -panel_depth
transom_thickness = props.transom_thickness * si_conversion / 2
transfom_offset = props.transom_offset * si_conversion
@@ -278,10 +307,10 @@ def update_door_modifier_bmesh(context):
window_lining_height = overall_height - transfom_offset - transom_thickness
side_lining_thickness = lining_thickness_default
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0) if not sliding_door else 0
top_lining_thickness = transom_thickness or lining_thickness_default
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0) if not sliding_door else 0
door_opening_width = overall_width - lining_to_panel_offset_x * 2
if double_swing_door:
side_lining_thickness = side_lining_thickness - panel_lining_overlap_x
@@ -296,7 +325,6 @@ def update_door_modifier_bmesh(context):
casing_depth = props.casing_depth * si_conversion
# panel params
panel_depth = props.panel_depth * si_conversion
panel_width = door_opening_width * props.panel_width_ratio
frame_depth = props.frame_depth * si_conversion
frame_thickness = props.frame_thickness * si_conversion
@@ -389,7 +417,7 @@ def update_door_modifier_bmesh(context):
panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y, threshold_thickness)
if double_door:
# TODO: keep a little space between doors for readibility?
# keeping a little space between doors for readibility
double_door_offset = 0.001 * si_conversion
panel_size.x = panel_size.x / 2 - double_door_offset
door_verts.extend(create_bm_door_panel(panel_size, panel_position, "LEFT"))
@@ -516,7 +544,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_door"
bl_label = "Cancel editing Door"
bl_label = "Cancel Editing Door"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -545,7 +573,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_door"
bl_label = "Finish editing door"
bl_label = "Finish Editing Door"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -60,9 +60,6 @@ def load_post(*args):
product.regenerate_profile_usage,
)
IfcStore.add_element_listener(wall.element_listener)
IfcStore.add_element_listener(profile.element_listener)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbWall.CalculateQuantities", wall.calculate_quantities
)
@@ -18,7 +18,6 @@
import bpy
import gpu
import bgl
import bmesh
import logging
import numpy as np
@@ -36,7 +35,6 @@ from bpy.types import Operator
from bpy.types import SpaceView3D
from bpy.props import FloatProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
@@ -217,8 +215,9 @@ class FilledOpeningGenerator:
def generate_opening_from_filling(self, filling, filling_obj, voided_obj):
thickness = voided_obj.dimensions[1] + 0.1 + 0.1
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
profile = None
filling_type = ifcopenshell.util.element.get_type(filling)
@@ -227,47 +226,59 @@ class FilledOpeningGenerator:
filling_type, "Model", "Profile", "ELEVATION_VIEW"
)
filling_obj = tool.Ifc.get_object(filling_type)
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
if profile:
curve_3d = ifcopenshell.util.representation.resolve_representation(profile).Items[0]
def get_curve_2d_from_3d(curve_3d):
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
ifc_points = ifc_file.createIfcCartesianPointList2D([Vector(p).xz for p in curve_3d.Points.CoordList])
ifc_curve = ifc_file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return ifc_curve
extrusion = shape_builder.extrude(
ifcopenshell.util.representation.resolve_representation(profile).Items[0],
get_curve_2d_from_3d(curve_3d),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_x_axis=Vector((1, 0, 0)),
position_z_axis=Vector((0, -1, 0)),
extrusion_vector=Vector((0, 0, -1)),
)
return shape_builder.get_representation(context, [extrusion])
x, y, z = filling_obj.dimensions
opening_position = Vector([0.0, -0.1 / unit_scale, 0.0])
opening_size = Vector([x, 0, z]) / unit_scale
opening_size = Vector([x, z]) / unit_scale
# Windows and doors can have a casing that overlaps the wall
# but shouldn't affect the size of the opening.
# So we shouldn't use object dimensions in that case. More: #2784
# Just keeping it for windows and doors for now to be safe
x_redefined, z_redefined = False, False
has_width_attribute, has_height_attribute = False, False
if filling.is_a() in ["IfcWindow", "IfcDoor"]:
if filling.OverallWidth:
opening_size.x = filling.OverallWidth
x_redefined = True
has_width_attribute = True
if filling.OverallHeight:
opening_size.z = filling.OverallHeight
z_redefined = True
opening_size.y = filling.OverallHeight
has_height_attribute = True
# making sure if min_x or min_z != 0 to shift the opening accordingly
# to prevent something like #2784
if not x_redefined:
if not has_width_attribute:
opening_position.x = min(v[0] for v in filling_obj.bound_box)
if not z_redefined:
if not has_height_attribute:
opening_position.z = min(v[2] for v in filling_obj.bound_box)
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=opening_size),
magnitude=thickness / unit_scale,
position=opening_position,
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_z_axis=Vector((0.0, -1.0, 0.0)),
position_x_axis=Vector((1.0, 0.0, 0.0)),
extrusion_vector=Vector((0.0, 0.0, -1.0)),
)
return shape_builder.get_representation(context, [extrusion])
@@ -325,15 +336,13 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
for building_element in decomposed_building_elements:
building_obj = tool.Ifc.get_object(building_element)
if building_obj and building_obj.data:
body = ifcopenshell.util.representation.get_representation(
building_element, "Model", "Body", "MODEL_VIEW"
)
if body:
representation = tool.Geometry.get_active_representation(building_obj)
if representation:
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=building_obj,
representation=body,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
@@ -709,13 +718,12 @@ class EditOpenings(Operator, tool.Ifc.Operator):
if opening_obj:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
building_objs.add(obj)
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
elif tool.Ifc.is_moved(opening_obj):
blenderbim.core.geometry.edit_object_placement(
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
)
building_objs.add(obj)
building_objs.add(obj)
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
@@ -759,6 +767,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
return results
# TODO: merge with ProfileDecorator?
class DecorationsHandler:
installed = None
@@ -777,27 +786,38 @@ class DecorationsHandler:
pass
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
self.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
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
for opening in context.scene.BIMModelProperties.openings:
obj = opening.obj
if not obj:
continue
white = (1, 1, 1, 1)
white_t = (1, 1, 1, 0.1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
red_t = (1, 0.2, 0.322, 0.1)
blue = (0.157, 0.565, 1, 1)
blue_t = (0.157, 0.565, 1, 0.1)
grey = (0.2, 0.2, 0.2, 1)
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
verts = []
@@ -829,41 +849,22 @@ class DecorationsHandler:
else:
unselected_edges.append(edge_indices)
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=selected_edges)
self.shader.uniform_float("color", green)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
self.draw_batch("LINES", verts, transparent_color(unselected_elements_color, 0.5), unselected_edges)
self.draw_batch("LINES", verts, selected_elements_color, selected_edges)
self.draw_batch("POINTS", unselected_vertices, unselected_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
edges = [tuple([v.index for v in e.verts]) for e in bm.edges]
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.bind()
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
batch.draw(self.shader)
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges)
obj.data.calc_loop_triangles()
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
batch = batch_for_shader(self.shader, "TRIS", {"pos": verts}, indices=tris)
self.shader.bind()
self.shader.uniform_float("color", blue_t)
batch.draw(self.shader)
self.draw_batch("TRIS", verts, transparent_color(special_elements_color), tris)
if "HalfSpaceSolid" in obj.name:
# Arrow shape
@@ -876,10 +877,8 @@ class DecorationsHandler:
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
]
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.bind()
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
batch.draw(self.shader)
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges)
if obj.mode != "EDIT":
bm.free()
@@ -402,7 +402,7 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mirror_elements"
bl_label = "Mirror Elements"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Faux-mirrors the selected object by an active empty along a mirror plane."
bl_description = "Faux-mirrors the selected objects by an active empty along a mirror plane"
@classmethod
def poll(cls, context):
@@ -429,6 +429,13 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
mirror.select_set(False)
if not context.selected_objects:
self.report(
{"INFO"},
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
)
return {"FINISHED"}
bpy.ops.bim.override_object_duplicate_move(is_interactive=False)
for obj in context.selected_objects:
@@ -31,64 +31,10 @@ import blenderbim.core.geometry
from math import pi, degrees, inf
from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.wall import DumbWallRecalculator
from blenderbim.bim.module.model.decorator import ProfileDecorator
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
return
if obj.mode == "EDIT":
tool.Ifc.edit(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
else:
material_usage = ifcopenshell.util.element.get_material(product)
x, y = obj.dimensions[0:2]
if not material_usage.CardinalPoint:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
elif material_usage.CardinalPoint == 1:
new_origin = obj.matrix_world @ Vector(obj.bound_box[4])
elif material_usage.CardinalPoint == 2:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, 0, 0)) / 2))
elif material_usage.CardinalPoint == 3:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
elif material_usage.CardinalPoint == 4:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[4]) + (Vector((0, y, 0)) / 2))
elif material_usage.CardinalPoint == 5:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
elif material_usage.CardinalPoint == 6:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((0, y, 0)) / 2))
elif material_usage.CardinalPoint == 7:
new_origin = obj.matrix_world @ Vector(obj.bound_box[7])
elif material_usage.CardinalPoint == 8:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[3]) + (Vector((x, 0, 0)) / 2))
elif material_usage.CardinalPoint == 9:
new_origin = obj.matrix_world @ Vector(obj.bound_box[3])
if (obj.matrix_world.translation - new_origin).length < 0.001:
return
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
class DumbProfileGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
@@ -314,7 +260,7 @@ class DumbProfileJoiner:
body[1 if connection == "ATEND" else 0] = intersect
self.recreate_profile(element1, profile1, axis, body)
def set_depth(self, profile1, length):
def set_depth(self, profile1, si_length):
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
@@ -324,8 +270,6 @@ class DumbProfileJoiner:
axis1 = self.get_profile_axis(profile1)
axis = copy.deepcopy(axis1)
body = copy.deepcopy(axis1)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
si_length = unit_scale * length
end = profile1.matrix_world @ Vector((0, 0, si_length))
axis[1] = end
body[1] = end
@@ -892,15 +836,24 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects
def _execute(self, context):
objs = []
profile_objs = []
layer2_objs = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
usage = tool.Model.get_usage_type(element)
if usage == "PROFILE":
profile_objs.append(obj)
elif usage == "LAYER2":
layer2_objs.append(obj)
if element.ConnectedTo or element.ConnectedFrom:
objs.append(obj)
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH")
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
obj.matrix_world @= rotate_matrix
bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(objs)
DumbProfileRecalculator().recalculate(profile_objs)
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
@@ -1022,5 +975,5 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.view_layer.update()
joiner = DumbProfileJoiner()
joiner.set_depth(obj, depth / self.unit_scale)
joiner.set_depth(obj, depth)
return {"FINISHED"}
+100 -122
View File
@@ -21,7 +21,6 @@ import ifcopenshell
import blenderbim.tool as tool
from blenderbim.bim.prop import ObjProperty
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
from bpy.types import PropertyGroup, NodeTree
from math import pi
@@ -38,16 +37,10 @@ def get_type_class(self, context):
return AuthoringData.data["type_class"]
def get_relating_type(self, context):
def get_relating_type_id(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["relating_types_ids"]
def get_relating_type_browser(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["relating_types_ids_browser"]
return AuthoringData.data["relating_type_id"]
def get_type_predefined_type(self, context):
@@ -56,33 +49,9 @@ def get_type_predefined_type(self, context):
return AuthoringData.data["type_predefined_type"]
def update_icon_id(self, context, browser=False):
if context == "lost_context" or (context.region is not None and context.region.type != "TOOL_HEADER"):
ifc_class = self.ifc_class_browser if browser else self.ifc_class
relating_type_id = self.relating_type_id_browser if browser else self.relating_type_id
# relating_type = AuthoringData.relating_type_name_by_id(ifc_class, relating_type_id)
if ifc_class not in self.constr_classes or relating_type_id not in self.constr_classes[ifc_class].constr_types:
try:
AuthoringData.assetize_relating_type_from_selection(browser=browser)
except ConstrTypeEntityNotFound:
return
def set_icon(update_interval_seconds=1e-4):
if (
ifc_class not in self.constr_classes
or relating_type_id not in self.constr_classes[ifc_class].constr_types
):
return update_interval_seconds
else:
self.icon_id = self.constr_classes[ifc_class].constr_types[relating_type_id].icon_id
bpy.app.timers.register(set_icon)
def update_ifc_class(self, context):
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
AuthoringData.data["relating_types_ids"] = AuthoringData.relating_types()
AuthoringData.data["relating_type_id"] = AuthoringData.relating_types()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
@@ -95,21 +64,8 @@ def update_type_class(self, context):
AuthoringData.data["type_predefined_type"] = AuthoringData.type_predefined_type()
def update_ifc_class_browser(self, context):
if context.region is not None and context.region.type != "TOOL_HEADER":
AuthoringData.load_ifc_classes()
AuthoringData.load_relating_types_browser()
if self.updating:
return
ifc_class = self.ifc_class_browser
constr_class_info = AuthoringData.constr_class_info(ifc_class)
if constr_class_info is None or not constr_class_info.fully_loaded:
AuthoringData.assetize_constr_class(ifc_class)
def update_relating_type(self, context):
AuthoringData.load_relating_types()
def update_relating_type_id(self, context):
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
@@ -117,81 +73,20 @@ def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
def update_relating_type_browser(self, context):
AuthoringData.load_relating_types_browser()
if not self.updating:
update_icon_id(self, context, browser=True)
def update_relating_type_by_name(self, context):
AuthoringData.load_relating_types()
relating_type_id = AuthoringData.relating_type_id_by_name(self.ifc_class, self.relating_type)
if relating_type_id is not None:
self.relating_type_id = relating_type_id
def get_constr_class_info(props, ifc_class):
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
def update_preview_multiple(self, context):
if context.region is not None and context.region.type != "TOOL_HEADER":
if self.preview_multiple_constr_types:
ifc_class = self.ifc_class
constr_class_info = get_constr_class_info(self, ifc_class)
if constr_class_info is None or not constr_class_info.fully_loaded:
AuthoringData.assetize_constr_class(ifc_class)
else:
update_relating_type(self, context)
class ConstrTypeInfo(PropertyGroup):
name: bpy.props.StringProperty(name="Construction type ID")
icon_id: bpy.props.IntProperty(name="Icon ID")
object: bpy.props.PointerProperty(name="Object", type=bpy.types.Object)
class ConstrClassInfo(PropertyGroup):
name: bpy.props.StringProperty(name="Construction class")
constr_types: bpy.props.CollectionProperty(type=ConstrTypeInfo)
fully_loaded: bpy.props.BoolProperty(default=False)
class ConstrBrowserState(PropertyGroup):
cursor_x: bpy.props.IntProperty()
cursor_y: bpy.props.IntProperty()
window_x: bpy.props.IntProperty()
window_y: bpy.props.IntProperty()
far_away_x: bpy.props.IntProperty(default=10) # lower left corner to temporarily warp the mouse
far_away_y: bpy.props.IntProperty(default=10) # useful to close popup operators
updating: bpy.props.BoolProperty()
update_delay: bpy.props.FloatProperty(default=3e-2)
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
ifc_class_browser: bpy.props.EnumProperty(
items=get_ifc_class, name="Construction Class", update=update_ifc_class_browser
)
relating_type: bpy.props.StringProperty(update=update_relating_type_by_name)
relating_type_id: bpy.props.EnumProperty(
items=get_relating_type, name="Construction Type", update=update_relating_type
)
relating_type_id_browser: bpy.props.EnumProperty(
items=get_relating_type_browser, name="Construction Type", update=update_relating_type_browser
items=get_relating_type_id, name="Construction Type", update=update_relating_type_id
)
icon_id: bpy.props.IntProperty()
preview_multiple_constr_types: bpy.props.BoolProperty(default=False, update=update_preview_multiple)
updating: bpy.props.BoolProperty(default=False)
occurrence_name_style: bpy.props.EnumProperty(
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
name="Occurrence Name Style",
)
occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function")
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type}
constr_classes: bpy.props.CollectionProperty(type=ConstrClassInfo)
constr_browser_state: bpy.props.PointerProperty(type=ConstrBrowserState)
extrusion_depth: bpy.props.FloatProperty(default=42.0)
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type_id}
extrusion_depth: bpy.props.FloatProperty(default=42.0, subtype="DISTANCE")
cardinal_point: bpy.props.EnumProperty(
items=(
# TODO: complain to buildingSMART
@@ -218,7 +113,7 @@ class BIMModelProperties(PropertyGroup):
name="Cardinal Point",
default="5",
)
length: bpy.props.FloatProperty(default=42.0)
length: bpy.props.FloatProperty(default=42.0, subtype="DISTANCE")
openings: bpy.props.CollectionProperty(type=ObjProperty)
x: bpy.props.FloatProperty(name="X", default=0.5)
y: bpy.props.FloatProperty(name="Y", default=0.5)
@@ -246,13 +141,40 @@ 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)",
)
class BIMArrayProperties(PropertyGroup):
is_editing: bpy.props.IntProperty(
default=-1, description="Currently edited array index. -1 if not in array editing mode."
)
count: bpy.props.IntProperty(name="Count", default=0)
count: bpy.props.IntProperty(name="Count", default=0, min=0)
x: bpy.props.FloatProperty(name="X", default=0)
y: bpy.props.FloatProperty(name="Y", default=0)
z: bpy.props.FloatProperty(name="Z", default=0)
@@ -261,6 +183,16 @@ class BIMArrayProperties(PropertyGroup):
description="Use local space for array items offset instead of world space",
default=True,
)
method: bpy.props.EnumProperty(
items=(("OFFSET", "Offset", ""), ("DISTRIBUTE", "Distribute", "")),
name="Method",
default="OFFSET",
)
sync_children: bpy.props.BoolProperty(
name="Sync Children",
description="Regenerate all children based on the parent object",
default=False,
)
class BIMStairProperties(PropertyGroup):
@@ -342,6 +274,7 @@ class BIMWindowProperties(PropertyGroup):
)
# number of panels and default mullion/transom values
# fmt: off
window_types_panels = {
"SINGLE_PANEL": (1, ((0, 0 ), (0, 0 ))),
"DOUBLE_PANEL_HORIZONTAL": (2, ((0, 0 ), (0.45, 0 ))),
@@ -353,6 +286,7 @@ class BIMWindowProperties(PropertyGroup):
"TRIPLE_PANEL_HORIZONTAL": (3, ((0, 0 ), (0.3, 0.6))),
"TRIPLE_PANEL_VERTICAL": (3, ((0.2, 0.4), (0, 0 ))),
}
# fmt: on
window_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
@@ -455,7 +389,9 @@ class BIMDoorProperties(PropertyGroup):
("DOUBLE_SWING_LEFT", "DOUBLE_SWING_LEFT", ""),
("DOUBLE_SWING_RIGHT", "DOUBLE_SWING_RIGHT", ""),
("DOUBLE_DOOR_SINGLE_SWING", "DOUBLE_DOOR_SINGLE_SWING", ""),
("DOUBLE_DOOR_DOUBLE_SWING", "DOUBLE_DOOR_DOUBLE_SWING", ""),
("SLIDING_TO_LEFT", "SLIDING_TO_LEFT", ""),
("SLIDING_TO_RIGHT", "SLIDING_TO_RIGHT", ""),
("DOUBLE_DOOR_SLIDING", "DOUBLE_DOOR_SLIDING", ""),
)
door_added_previously: bpy.props.BoolProperty(default=False)
@@ -525,10 +461,12 @@ class BIMDoorProperties(PropertyGroup):
"lining_depth": self.lining_depth,
"lining_thickness": self.lining_thickness,
"lining_offset": self.lining_offset,
"lining_to_panel_offset_x": self.lining_to_panel_offset_x,
"lining_to_panel_offset_y": self.lining_to_panel_offset_y,
}
if "SLIDING" not in self.door_type:
kwargs["lining_to_panel_offset_x"] = self.lining_to_panel_offset_x
kwargs["lining_to_panel_offset_y"] = self.lining_to_panel_offset_y
kwargs["transom_thickness"] = self.transom_thickness
if self.transom_thickness:
kwargs["transom_offset"] = self.transom_offset
@@ -556,7 +494,18 @@ class BIMDoorProperties(PropertyGroup):
class BIMRailingProperties(PropertyGroup):
railing_types = (("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),)
railing_types = (
("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),
("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""),
)
cap_types = (
("none", "none", ""),
("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", ""),
)
railing_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
@@ -567,13 +516,42 @@ class BIMRailingProperties(PropertyGroup):
thickness: bpy.props.FloatProperty(name="Thickness", default=0.050)
spacing: bpy.props.FloatProperty(name="Spacing", default=0.050)
# wall mounted handrail specific properties
use_manual_supports: bpy.props.BoolProperty(
name="Use Manual Supports",
default=False,
description="If enabled, supports are added on every vertex on the edges of the railing path.\n"
"If disabled, supports are added automatically based on the support spacing",
)
support_spacing: bpy.props.FloatProperty(
name="Support Spacing", default=1.0, description="Distance between supports if automatic supports are used"
)
railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050)
clear_width: bpy.props.FloatProperty(
name="Clear Width", default=0.040, description="Clear width between the railing and the wall"
)
terminal_type: bpy.props.EnumProperty(name="Terminal Type", items=cap_types, default="180")
def get_general_kwargs(self):
return {
base_kwargs = {
"railing_type": self.railing_type,
"height": self.height,
"thickness": self.thickness,
"spacing": self.spacing,
}
additional_kwargs = {}
if self.railing_type == "FRAMELESS_PANEL":
additional_kwargs = {
"thickness": self.thickness,
"spacing": self.spacing,
}
elif self.railing_type == "WALL_MOUNTED_HANDRAIL":
additional_kwargs = {
"railing_diameter": self.railing_diameter,
"clear_width": self.clear_width,
"use_manual_supports": self.use_manual_supports,
"support_spacing": self.support_spacing,
"terminal_type": self.terminal_type,
}
return base_kwargs | additional_kwargs
class BIMRoofProperties(PropertyGroup):
@@ -18,14 +18,11 @@
import bpy
from bpy.types import Operator
import bmesh
import ifcopenshell
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.tool as tool
import blenderbim.core.geometry as core
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.door import bm_sort_out_geom
@@ -41,6 +38,15 @@ import json
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailingType.htm
NON_SI_RAILING_PROPS = (
"is_editing",
"railing_type",
"railing_added_previously",
"use_manual_supports",
"terminal_type",
)
def bm_split_edge_at_offset(edge, offset):
v0, v1 = edge.verts
@@ -82,7 +88,31 @@ def update_railing_modifier_ifc_data(context):
"Height": props.height,
},
)
tool.Ifc.edit(obj)
if props.railing_type == "WALL_MOUNTED_HANDRAIL":
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
railing_path = [Vector(v) for v in RailingData.data["parameters"]["data_dict"]["path_data"]["verts"]]
representation_data = {
"railing_type": props.railing_type,
"context": body,
"railing_path": railing_path,
"use_manual_supports": props.use_manual_supports,
"support_spacing": props.support_spacing,
"railing_diameter": props.railing_diameter,
"clear_width": props.clear_width,
"terminal_type": props.terminal_type,
"height": props.height,
}
model_representation = ifcopenshell.api.run(
"geometry.add_railing_representation", ifc_file, **representation_data
)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# hacky way to ensure tha ifc representation won't get tessellated at project save
IfcStore.edited_objs.discard(obj)
elif props.railing_type == "FRAMELESS_PANEL":
tool.Ifc.edit(obj)
def update_bbim_railing_pset(element, railing_data):
@@ -118,77 +148,114 @@ def update_railing_modifier_bmesh(context):
tool.Blender.apply_bmesh(obj.data, bm)
return
# generating the entire railing
height = props.height * si_conversion
thickness = props.thickness * si_conversion
spacing = props.spacing * si_conversion
if props.railing_type != "FRAMELESS_PANEL":
return
# spacing
# split each edge in 3 segments by 0.5 * spacing by x-y plane
main_edges = bm.edges[:]
for main_edge in main_edges:
bm_split_edge_at_offset(main_edge, spacing)
def generate_frameless_panel_railing():
# generating FRAMELESS_PANEL railing
height = props.height * si_conversion
thickness = props.thickness * si_conversion
spacing = props.spacing * si_conversion
# thickness
# keep track of translated verts so we won't translate the same
# vert twice
edge_dissolving_verts = []
for main_edge in main_edges:
v0, v1 = main_edge.verts
edge_dissolving_verts.extend([v0, v1])
# spacing
# split each edge in 3 segments by 0.5 * spacing by x-y plane
main_edges = bm.edges[:]
for main_edge in main_edges:
bm_split_edge_at_offset(main_edge, spacing)
edge_dir = ((v1.co - v0.co) * V(1, 1, 0)).normalized()
ortho_vector = edge_dir.cross(V(0, 0, 1))
# thickness
# keep track of translated verts so we won't translate the same
# vert twice
edge_dissolving_verts = []
for main_edge in main_edges:
v0, v1 = main_edge.verts
edge_dissolving_verts.extend([v0, v1])
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
edge_dir = ((v1.co - v0.co) * V(1, 1, 0)).normalized()
ortho_vector = edge_dir.cross(V(0, 0, 1))
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
# dissolve middle edge
bmesh.ops.dissolve_edges(bm, edges=[main_edge])
# height
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
extrusion_vector = Vector((0, 0, 1)) * height
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
# dissolve middle edges
edges_to_dissolve = []
verts_to_dissolve = []
for v in edge_dissolving_verts:
for e in v.link_edges:
other_vert = e.other_vert(v)
if other_vert in extruded_verts:
edges_to_dissolve.append(e)
verts_to_dissolve.append(other_vert)
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bmesh.ops.dissolve_verts(bm, verts=verts_to_dissolve)
# dissolve middle edge
bmesh.ops.dissolve_edges(bm, edges=[main_edge])
# to remove unnecessary verts in 0 spacing case
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
# height
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
extrusion_vector = Vector((0, 0, 1)) * height
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
tool.Blender.apply_bmesh(obj.data, bm)
# dissolve middle edges
edges_to_dissolve = []
verts_to_dissolve = []
for v in edge_dissolving_verts:
for e in v.link_edges:
other_vert = e.other_vert(v)
if other_vert in extruded_verts:
edges_to_dissolve.append(e)
verts_to_dissolve.append(other_vert)
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bmesh.ops.dissolve_verts(bm, verts=verts_to_dissolve)
# to remove unnecessary verts in 0 spacing case
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
tool.Blender.apply_bmesh(obj.data, bm)
generate_frameless_panel_railing()
def get_path_data(obj):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if obj.mode == "EDIT":
# otherwise mesh may not contain all changes
# added in edit mode
obj.update_from_editmode()
mesh = obj.data
path_data = dict()
path_data["edges"] = [e.vertices[:] for e in mesh.edges]
path_data["verts"] = [v.co / si_conversion for v in mesh.vertices]
if not path_data["edges"] or not path_data["verts"]:
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
end_points = [v for v in bm.verts if len(v.link_edges) == 1]
if not end_points:
return None
# if we have some previous data then we try to match
# start or end of the path with the previous path
previous_data = False
if previous_data:
previous_start = previous_data[0]
previous_end = previous_data[-1]
potential_start = min([(v, (v.co - previous_start).length) for v in end_points], key=lambda v_data: v_data[1])
potential_end = min([(v, (v.co - previous_end).length) for v in end_points], key=lambda v_data: v_data[1])
if potential_start[1] < potential_end[1]:
start_point = potential_start[0]
else:
start_point = next(v for v in end_points if v != potential_start[0])
else:
start_point = min(end_points, key=lambda v: v.index)
# walking through the path
# to make sure all verts and in consequent order
edge = start_point.link_edges[0]
v = edge.other_vert(start_point)
points = [start_point.co, v.co]
segments = [(0, 1)]
i = 2
other_edge = lambda edges, edge: next(e for e in edges if e != edge)
while len(link_edges := v.link_edges) != 1:
link_edges = v.link_edges
edge = other_edge(link_edges, edge)
v = edge.other_vert(v)
points.append(v.co)
segments.append((i - 1, i))
i += 1
path_data = {"edges": segments, "verts": [p / si_conversion for p in points]}
return path_data
@@ -248,8 +315,7 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
# need to make sure all default props will have correct units
if not props.railing_added_previously:
skip_props = ("is_editing", "railing_type", "railing_added_previously")
convert_property_group_from_si(props, skip_props=skip_props)
convert_property_group_from_si(props, skip_props=NON_SI_RAILING_PROPS)
railing_data = props.get_general_kwargs()
path_data = get_path_data(obj)
@@ -289,8 +355,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
# need to make sure all props that weren't used before
# will have correct units
skip_props = ("is_editing", "railing_type", "railing_added_previously")
skip_props += tuple(data.keys())
skip_props = NON_SI_RAILING_PROPS + tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
@@ -299,7 +364,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_railing"
bl_label = "Cancel editing Railing"
bl_label = "Cancel Editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -328,7 +393,7 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_railing"
bl_label = "Finish editing railing"
bl_label = "Finish Editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -349,6 +414,37 @@ class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class FlipRailingPathOrder(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.flip_railing_path_order"
bl_label = "Flip Railing Path Order"
bl_description = "Can be useful to maintain railing supports direction"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]["data_dict"]["path_data"]
# flip the vertex order and edges
path_data["verts"] = path_data["verts"][::-1]
last_vert_i = len(path_data["verts"]) - 1
edges = []
for edge in path_data["edges"][::-1]:
edge = [abs(vi - last_vert_i) for vi in edge[::-1]]
edges.append(edge)
railing_data = props.get_general_kwargs()
railing_data["path_data"] = path_data
update_bbim_railing_pset(element, railing_data)
update_railing_modifier_ifc_data(context)
return {"FINISHED"}
class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_railing_path"
bl_label = "Enable Editing Railing Path"
@@ -17,7 +17,6 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import Operator
import bmesh
import ifcopenshell
@@ -29,20 +28,18 @@ from blenderbim.bim.module.model.data import RoofData, refresh
from blenderbim.bim.module.model.decorator import ProfileDecorator
import json
from math import tan, radians, degrees, atan
from math import tan, pi
from mathutils import Vector, Matrix
from bpypolyskel import bpypolyskel
import shapely
from pprint import pprint
from itertools import chain
from math import pi
# reference:
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoof.htm
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoofType.htm
# create read only property in blender operator
NON_SI_ROOF_PROPS = ("is_editing", "roof_type", "roof_added_previously", "generation_method", "angle")
def float_is_zero(f):
@@ -116,10 +113,13 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
original_geometry_data = dict()
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
if angle_layer:
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), e[angle_layer]) for e in bm.edges]
original_geometry_data["edges"] = [
(set(bm_get_indices(e.verts)), e[angle_layer], e[separate_verts_layer]) for e in bm.edges
]
else:
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None) for e in bm.edges]
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None, None) for e in bm.edges]
original_geometry_data["verts"] = {v.index: v.co.copy() for v in bm.verts}
footprint_z = bm.verts[:][0].co.z
@@ -212,6 +212,8 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
footprint_edges = []
footprint_verts = set()
verts_to_change = {}
verts_to_rip = []
bottom_chords_to_remove = []
# find footprint edges
for edge in bm.edges:
@@ -227,7 +229,7 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
# iterate over edges from original geometry
# if their angle was redefined by user - apply the changes to the related vertices
# to match the requested angle
for old_edge_verts, defined_angle in original_geometry_data["edges"]:
for old_edge_verts, defined_angle, separate_verts in original_geometry_data["edges"]:
if not defined_angle:
continue
@@ -240,14 +242,38 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
verts_to_move = find_other_polygon_verts(identical_edge)
for v in verts_to_move:
vert_co = verts_to_change.get(v, v.co)
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_change[v] = new_vert_co
if not separate_verts:
vert_co = verts_to_change.get(v, v.co)
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_change[v] = new_vert_co
else:
vert_co = v.co
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_rip.append([v, new_vert_co, identical_edge])
if defined_angle >= pi / 2:
bottom_chords_to_remove.append(identical_edge)
def separate_vert(bm, vert, edge, new_co):
face = next(f for f in vert.link_faces if edge in f.edges)
new_v = bmesh.utils.face_vert_separate(face, vert)
new_v.co = new_co
new_edge = bm.edges.new((vert, new_v))
for cur_edge in face.edges:
if cur_edge == edge:
continue
bmesh.ops.contextual_create(bm, geom=[new_edge, cur_edge])
# apply all changes once at the end
for v in verts_to_change:
v.co = verts_to_change[v]
for v, new_co, edge in verts_to_rip:
separate_vert(bm, v, edge, new_co)
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
extrusion_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
extruded_verts = bm_sort_out_geom(extrusion_geom)["verts"]
bmesh.ops.translate(bm, vec=[0.0, 0.0, 0.1], verts=extruded_verts)
@@ -289,9 +315,6 @@ def update_roof_modifier_ifc_data(context):
# occurences attributes
# occurences = tool.Ifc.get_all_element_occurences(element)
# TODO: add Qto_RoofBaseQuantities, need to calculate GrossArea, NetArea, ProjectedArea
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/Qto_RoofBaseQuantities.htm
tool.Ifc.edit(obj)
@@ -311,12 +334,14 @@ def update_roof_modifier_bmesh(context):
RoofData.load()
path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
angle_layer_data = path_data.get("gable_roof_angles", None)
separate_verts_data = path_data.get("gable_roof_separate_verts", None)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# need to make sure we support edit mode
# since users will probably be in edit mode when they'll be changing roof path
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
angle_layer = bm.edges.layers.float.new("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.new("BBIM_gable_roof_separate_verts")
# generating roof path
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
@@ -325,6 +350,7 @@ def update_roof_modifier_bmesh(context):
e = path_data["edges"][i]
edge = bm.edges.new((new_verts[e[0]], new_verts[e[1]]))
edge[angle_layer] = angle_layer_data[i] if angle_layer_data else 0
edge[separate_verts_layer] = separate_verts_data[i] if separate_verts_data else 0
new_edges.append(edge)
if props.is_editing_path:
@@ -349,12 +375,15 @@ def get_path_data(obj):
bm_mesh_clean_up(bm)
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
path_data = dict()
path_data["edges"] = [bm_get_indices(e.verts) for e in bm.edges]
path_data["verts"] = [v.co / si_conversion for v in bm.verts]
if angle_layer:
path_data["gable_roof_angles"] = [e[angle_layer] for e in bm.edges]
if separate_verts_layer:
path_data["gable_roof_separate_verts"] = [e[separate_verts_layer] for e in bm.edges]
if not path_data["edges"] or not path_data["verts"]:
return None
@@ -417,8 +446,7 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
# need to make sure all default props will have correct units
if not props.roof_added_previously:
skip_props = ("is_editing", "roof_type", "roof_added_previously", "generation_method")
convert_property_group_from_si(props, skip_props=skip_props)
convert_property_group_from_si(props, skip_props=NON_SI_ROOF_PROPS)
roof_data = props.get_general_kwargs()
path_data = get_path_data(obj)
@@ -459,8 +487,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
# need to make sure all props that weren't used before
# will have correct units
skip_props = ("is_editing", "roof_type", "roof_added_previously", "generation_method")
skip_props += tuple(data.keys())
skip_props = NON_SI_ROOF_PROPS + tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
@@ -469,7 +496,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_roof"
bl_label = "Cancel editing Roof"
bl_label = "Cancel Editing Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -498,7 +525,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_roof"
bl_label = "Finish editing roof"
bl_label = "Finish Editing Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -633,9 +660,10 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
class SetGableRoofEdgeAngle(bpy.types.Operator):
bl_idname = "bim.set_gable_roof_edge_angle"
bl_label = "Set gable roof edge angle"
bl_label = "Set Gable Roof Edge Angle"
bl_options = {"REGISTER", "UNDO"}
angle: bpy.props.FloatProperty(name="Angle", default=90)
separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
@classmethod
def poll(cls, context):
@@ -658,12 +686,17 @@ class SetGableRoofEdgeAngle(bpy.types.Operator):
if "BBIM_gable_roof_angles" not in me.attributes:
me.attributes.new("BBIM_gable_roof_angles", type="FLOAT", domain="EDGE")
if "BBIM_gable_roof_separate_verts" not in me.attributes:
me.attributes.new("BBIM_gable_roof_separate_verts", type="INT", domain="EDGE")
angles_layer = bm.edges.layers.float["BBIM_gable_roof_angles"]
separate_verts_layer = bm.edges.layers.int["BBIM_gable_roof_separate_verts"]
for e in bm.edges:
if not e.select:
continue
e[angles_layer] = self.angle
e[separate_verts_layer] = self.separate_verts
tool.Blender.apply_bmesh(me, bm)
return {"FINISHED"}
@@ -48,8 +48,3 @@ def sync_name(usecase_path, ifc_file, settings):
collection.name = new_name
obj.name = new_name
blenderbim.bim.handler.refresh_ui_data()
class ConstrTypeEntityNotFound(Exception):
pass
@@ -238,11 +238,11 @@ class DumbSlabPlaner:
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
self.change_thickness(element, thickness)
self.change_thickness(element, total_thickness)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
self.change_thickness(element, thickness)
self.change_thickness(element, total_thickness)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
obj = tool.Ifc.get_object(settings["related_object"])
@@ -604,6 +604,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion.Position:
@@ -636,8 +637,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(representation)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
position[0][3] *= self.unit_scale
@@ -670,37 +672,42 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.data.meshes.remove(profile_mesh)
# Only certain classes should have a footprint
if element.is_a() not in ("IfcSlab", "IfcRamp"):
return
footprint_context = ifcopenshell.util.representation.get_context(
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
)
if footprint_context:
curves = [profile.OuterCurve]
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
curves.extend(profile.InnerCurves)
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
if not footprint_context:
return
curves = [profile.OuterCurve]
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
curves.extend(profile.InnerCurves)
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
)
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
)
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
blenderbim.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint
)
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
else:
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_footprint
)
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
blenderbim.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint
)
else:
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_footprint
)
return {"FINISHED"}
class ResetVertex(bpy.types.Operator):
@@ -287,6 +287,22 @@ def update_ifc_stair_props(obj):
)
tool.Ifc.edit(obj)
# update related annotation objects
def get_elements_from_product(product):
elements = []
for rel in product.ReferencedBy:
if not rel.is_a("IfcRelAssignsToProduct"):
continue
elements.extend(rel.RelatedObjects)
return elements
stair_obj = obj
for rel_element in get_elements_from_product(element):
if not rel_element.is_a("IfcAnnotation") or rel_element.ObjectType != "STAIR_ARROW":
continue
if annotation_obj := tool.Ifc.get_object(rel_element):
tool.Drawing.setup_annotation_object(annotation_obj, "STAIR_ARROW", stair_obj)
class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_clever_stair"
@@ -368,7 +384,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_stair"
bl_label = "Cancel editing Stair"
bl_label = "Cancel Editing Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -388,7 +404,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_stair"
bl_label = "Finish editing stair"
bl_label = "Finish Editing Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -182,7 +182,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
# removed the part that was relying on node graph to be opened at execution
class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.import_sverchok_graph"
bl_label = "Import sverchok graph"
bl_label = "Import Sverchok Graph"
bl_options = {"REGISTER"}
filepath: bpy.props.StringProperty(
@@ -224,7 +224,7 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
# removed the part that was relying on node graph to be opened at execution
class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.export_sverchok_graph"
bl_label = "Export sverchok graph"
bl_label = "Export Sverchok Graph"
bl_options = {"REGISTER"}
filepath: bpy.props.StringProperty(
@@ -137,59 +137,6 @@ class BIM_PT_authoring(Panel):
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
class HelpConstrTypes(Operator):
bl_idname = "bim.help_relating_types"
bl_label = "Construction Types Help"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Click to read some contextual help"
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=510)
def draw(self, context):
layout = self.layout
layout.row().separator(factor=0.5)
row = layout.row()
row.alignment = "CENTER"
row.label(text="BlenderBIM Help", icon="BLENDER")
layout.row().separator(factor=0.5)
row = layout.row().row()
row.label(text="Overview:", icon="KEYTYPE_MOVING_HOLD_VEC")
self.draw_lines(layout, self.message_summary)
layout.row().separator()
row = layout.row().row()
row.label(text="Further support:", icon="KEYTYPE_MOVING_HOLD_VEC")
layout.row().separator(factor=0.5)
row = layout.row()
op = row.operator("bim.open_upstream", text="Homepage", icon="HOME")
op.page = "home"
op = row.operator("bim.open_upstream", text="Docs", icon="DOCUMENTS")
op.page = "docs"
op = row.operator("bim.open_upstream", text="Wiki", icon="CURRENT_FILE")
op.page = "wiki"
op = row.operator("bim.open_upstream", text="Community", icon="COMMUNITY")
op.page = "community"
layout.row().separator()
def draw_lines(self, layout, lines):
box = layout.box()
for line in lines:
row = box.row()
row.label(text=f" {line}")
@property
def message_summary(self):
return [
"The Construction Type Browser allows to preview and add new instances to the model.",
"For further support, please click on the Documentation link below.",
]
class BIM_PT_array(bpy.types.Panel):
bl_label = "IFC Array"
bl_idname = "BIM_PT_array"
@@ -224,21 +171,29 @@ class BIM_PT_array(bpy.types.Panel):
row.operator("bim.edit_array", icon="CHECKMARK", text="").item = i
row.operator("bim.disable_editing_array", icon="CANCEL", text="")
row = box.row(align=True)
row.prop(props, "use_local_space")
row.prop(props, "method")
row = box.row(align=True)
row.prop(props, "use_local_space")
row.prop(props, "sync_children")
col = box.column()
row = col.row(align=True)
row.prop(props, "x")
row.operator("bim.input_cursor_x_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "y")
row.operator("bim.input_cursor_y_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "z")
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
else:
row = box.row(align=True)
row.label(text=f"{array['count']} Items", icon="MOD_ARRAY")
name = f"{array['count']} Items ({array.get('method', 'OFFSET').capitalize()})"
row.label(text=name, icon="MOD_ARRAY")
row.operator("bim.enable_editing_array", icon="GREASEPENCIL", text="").item = i
row.operator("bim.remove_array", icon="X", text="").item = i
row = box.row(align=True)
row.label(text=f"Use Local Space: {array.get('use_local_space', False)}")
row = box.row(align=True)
row.label(text=f"X: {array['x']}")
icon = "EMPTY_ARROWS" if array.get("use_local_space", False) else "EMPTY_AXIS"
row.label(text=f"X: {array['x']}", icon=icon)
row.label(text=f"Y: {array['y']}")
row.label(text=f"Z: {array['z']}")
else:
@@ -273,7 +228,7 @@ class BIM_PT_stair(bpy.types.Panel):
stair_data = StairData.data["parameters"]["data_dict"]
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
@@ -373,7 +328,7 @@ class BIM_PT_window(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -488,7 +443,7 @@ class BIM_PT_door(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -572,7 +527,7 @@ class BIM_PT_railing(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -588,6 +543,9 @@ class BIM_PT_railing(bpy.types.Panel):
else:
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
# TODO: good for preview but probably should move to .is_editing == -1
# since it's writing to ifc
row.operator("bim.flip_railing_path_order", icon="ARROW_LEFTRIGHT", text="")
row.operator("bim.remove_railing", icon="X", text="")
box = self.layout.box()
@@ -631,7 +589,7 @@ class BIM_PT_roof(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -656,7 +614,7 @@ class BIM_PT_roof(bpy.types.Panel):
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
if prop == 'angle':
if prop == "angle":
prop_value = round(degrees(prop_value), 2)
row.label(text=str(prop_value))
else:
@@ -37,38 +37,6 @@ from mathutils import Vector, Matrix
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2":
return
if obj.mode == "EDIT":
tool.Ifc.edit(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
else:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.join_wall"
bl_label = "Join Wall"
@@ -239,6 +207,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
layer2_objs = []
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -251,7 +220,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
return
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
extrusion.Depth = self.depth * (1 / cos(x_angle))
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
@@ -1028,7 +997,7 @@ class DumbWallJoiner:
self.recreate_wall(element1, wall1, axis, body)
def set_length(self, wall1, length):
def set_length(self, wall1, si_length):
element1 = tool.Ifc.get_entity(wall1)
if not element1:
return
@@ -1038,8 +1007,6 @@ class DumbWallJoiner:
axis1 = tool.Model.get_wall_axis(wall1)
axis = copy.deepcopy(axis1["reference"])
body = copy.deepcopy(axis1["reference"])
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
si_length = unit_scale * length
end = (wall1.matrix_world @ Vector((si_length, 0, 0))).to_2d()
axis[1] = end
body[1] = end
@@ -69,10 +69,12 @@ def update_simple_openings(element, opening_width, opening_height):
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height])),
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height]).xz),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_x_axis=V(1, 0, 0),
position_z_axis=V(0, -1, 0),
extrusion_vector=V(0, 0, -1),
)
new_representation = shape_builder.get_representation(context, extrusion)
@@ -480,7 +482,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_window"
bl_label = "Cancel editing Window"
bl_label = "Cancel Editing Window"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -508,7 +510,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_window"
bl_label = "Finish editing window"
bl_label = "Finish Editing Window"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -68,7 +68,17 @@ class BimTool(WorkSpaceTool):
def add_layout_hotkey_operator(layout, text, hotkey, description):
op = layout.operator("bim.hotkey", text=text)
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op
@@ -101,7 +111,7 @@ class BimToolUI:
@classmethod
def draw_create_object_interface(cls):
if not AuthoringData.data["relating_types_ids"]:
if not AuthoringData.data["relating_type_id"]:
return
if cls.props.ifc_class == "IfcWallType":
row = cls.layout.row(align=True)
@@ -132,10 +142,7 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl2", text="RL")
elif cls.props.ifc_class in ("IfcSpaceType"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
add_layout_hotkey_operator(cls.layout, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
@classmethod
def draw_edit_object_interface(cls, context):
@@ -155,47 +162,19 @@ class BimToolUI:
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Y")
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_M")
add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_K")
add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
row.operator("bim.join_wall", icon="X", text="").join_type = ""
elif AuthoringData.data["active_material_usage"] == "LAYER3":
# unnecessary check because BIM Tool is not available in EDIT mode?
if context.active_object.mode == "OBJECT":
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Profile").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
@@ -215,32 +194,26 @@ class BimToolUI:
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Axis").hotkey = "A_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Y")
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not context.active_object.BIMRailingProperties.is_editing_path
):
# NOTE: should be above "active_representation_type" = "SweptSolid" check
# because it could be a SweptSolid too
add_layout_hotkey_operator(cls.layout, "Edit Railing Path", "S_E", "")
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
elif AuthoringData.data["active_class"] in (
"IfcWindow",
"IfcWindowStandardCase",
@@ -254,20 +227,11 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl1", text="RL")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
row.operator("bim.hotkey", text="Flip").hotkey = "S_F"
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "")
elif AuthoringData.data["active_class"] in ("IfcSpace",):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
elif AuthoringData.data["active_class"] in (
"IfcCableCarrierSegmentType",
@@ -275,39 +239,22 @@ class BimToolUI:
"IfcDuctSegmentType",
"IfcPipeSegmentType",
):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Extend", icon="EVENT_E")
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not context.active_object.BIMRailingProperties.is_editing_path
):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Railing Path").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and not context.active_object.BIMRoofProperties.is_editing_path
):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Roof Path").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Edit Roof Path", "S_E", "")
elif DecoratorData.get_ifc_text_data(bpy.context.object):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit text").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if len(context.selected_objects) == 2:
row.operator("bim.add_opening", text="Apply Void")
else:
@@ -320,27 +267,14 @@ class BimToolUI:
else:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
row = cls.layout.row(align=True)
row.label(text="Align")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Exterior", icon="EVENT_X")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Centerline", icon="EVENT_C")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Interior", icon="EVENT_V")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Mirror", icon="EVENT_M")
cls.layout.row(align=True).label(text="Align")
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "")
add_layout_hotkey_operator(cls.layout, "Align Interior", "S_V", "")
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
row = cls.layout.row(align=True)
row.label(text="Mode")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_O")
add_layout_hotkey_operator(row, "Void", "A_O", "Show / edit openings")
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")
@@ -369,7 +303,7 @@ class BimToolUI:
prop_with_search(row, cls.props, "ifc_class", text="")
row = cls.layout.row(align=True)
if AuthoringData.data["relating_types_ids"]:
if AuthoringData.data["relating_type_id"]:
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
else:
@@ -475,6 +409,16 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects:
return
# NOTE: placing it before the other operations because railing can also be SweptSolid
# and it might conflict with one of the conditions below
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
):
bpy.ops.bim.enable_editing_railing_path()
return
selected_usages = {}
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -483,8 +427,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
continue
usage = tool.Model.get_usage_type(element)
if not usage:
obj.select_set(False)
continue
representation = tool.Geometry.get_active_representation(obj)
if representation and representation.RepresentationType == "SweptSolid":
usage = "SWEPTSOLID"
else:
obj.select_set(False)
continue
selected_usages.setdefault(usage, []).append(obj)
if len(bpy.context.selected_objects) == 1:
@@ -498,7 +446,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
elif self.active_material_usage == "PROFILE":
# Extend PROFILE to cursor
bpy.ops.bim.extend_profile(join_type="T")
else:
# Edit SWEPTSOLID profile (assuming single profile for now)
bpy.ops.bim.enable_editing_extrusion_profile()
elif self.active_material_usage == "LAYER2" and selected_usages.get("PROFILE", []):
# Extend PROFILEs to LAYER2
[o.select_set(False) for o in selected_usages.get("LAYER3", [])]
@@ -523,15 +473,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
[o.select_set(False) for o in selected_usages.get("LAYER2", [])]
bpy.ops.bim.extend_profile(join_type="T")
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
):
# undo the unselection done above because railing has no usage type 🙃
bpy.context.object.select_set(True)
bpy.ops.bim.enable_editing_railing_path()
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
@@ -573,7 +514,13 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if self.active_material_usage == "LAYER2":
bpy.ops.bim.merge_wall()
else:
bpy.ops.bim.mirror_elements()
if len(bpy.context.selected_objects) == 1:
self.report(
{"INFO"},
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
)
else:
bpy.ops.bim.mirror_elements()
def hotkey_S_R(self):
if not bpy.context.selected_objects:
@@ -102,7 +102,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
class UpdateIfcPatchArguments(bpy.types.Operator):
bl_idname = "bim.update_ifc_patch_arguments"
bl_label = "Update IFC Patch arguments"
bl_label = "Update IFC Patch Arguments"
recipe: bpy.props.StringProperty()
def execute(self, context):
@@ -812,7 +812,6 @@ class ExportIFC(bpy.types.Operator):
bl_idname = "export_ifc.bim"
bl_label = "Export IFC"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export the IFC project"
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@@ -892,6 +891,12 @@ class ExportIFC(bpy.types.Operator):
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
@classmethod
def description(cls, context, properties):
if properties.should_save_as:
return "Export the IFC project to a selected file"
return "Export the IFC project to this file"
class ImportIFC(bpy.types.Operator):
bl_idname = "import_ifc.bim"
@@ -96,7 +96,10 @@ class MaterialPsetsData(Data):
@classmethod
def load(cls):
cls.data = {"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material))}
cls.data = {
"ifc_definition_id": bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id,
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material)),
}
cls.is_loaded = True
@@ -295,9 +295,10 @@ class EditPset(bpy.types.Operator, Operator):
name=props.active_pset_name,
properties=properties,
)
bpy.ops.bim.load_cost_item_quantities()
if tool.Cost.has_schedules():
tool.Cost.update_cost_items(pset=pset)
bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type)
tool.Blender.update_viewport()
class RemovePset(bpy.types.Operator, Operator):
@@ -380,32 +381,21 @@ class CalculateQuantity(bpy.types.Operator):
self.qto_calculator = QtoCalculator()
obj = context.active_object
prop = obj.PsetProperties.properties.get(self.prop)
prop.metadata.float_value = self.calculate_quantity(obj, context)
quantity = self.calculate_quantity(obj, context)
if quantity is None:
self.report({"ERROR"}, "Could not calculate quantity")
return {"CANCELLED"}
prop.metadata.float_value = quantity
return {"FINISHED"}
def calculate_quantity(self, obj, context):
quantity = self.qto_calculator.calculate_quantity(obj.PsetProperties.active_pset_name, self.prop, obj)
prefix, name = self.get_blender_prefix_name(context)
quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name)
if quantity is None:
return
return round(quantity, 3)
def get_prefix_name(self, value):
if "/" in value:
return value.split("/")
return None, value
def get_blender_prefix_name(self, context):
unit_settings = context.scene.unit_settings
if unit_settings.system == "IMPERIAL":
if unit_settings.length_unit == "INCHES":
return None, "inch"
elif unit_settings.length_unit == "FEET":
return None, "foot"
elif unit_settings.system == "METRIC":
if unit_settings.length_unit == "METERS":
return None, "METRE"
return unit_settings.length_unit[0 : -len("METERS")], "METRE"
class GuessQuantity(bpy.types.Operator):
bl_idname = "bim.guess_quantity"
@@ -422,35 +412,7 @@ class GuessQuantity(bpy.types.Operator):
def guess_quantity(self, obj, context):
quantity = self.qto_calculator.guess_quantity(self.prop, [p.name for p in obj.PsetProperties.properties], obj)
if "area" in self.prop.lower():
if context.scene.BIMProperties.area_unit:
prefix, name = self.get_prefix_name(context.scene.BIMProperties.area_unit)
quantity = ifcopenshell.util.unit.convert(quantity, None, "SQUARE_METRE", prefix, name)
elif "volume" in self.prop.lower():
if context.scene.BIMProperties.volume_unit:
prefix, name = self.get_prefix_name(context.scene.BIMProperties.volume_unit)
quantity = ifcopenshell.util.unit.convert(quantity, None, "CUBIC_METRE", prefix, name)
else:
prefix, name = self.get_blender_prefix_name(context)
quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name)
return round(quantity, 3)
def get_prefix_name(self, value):
if "/" in value:
return value.split("/")
return None, value
def get_blender_prefix_name(self, context):
unit_settings = context.scene.unit_settings
if unit_settings.system == "IMPERIAL":
if unit_settings.length_unit == "INCHES":
return None, "inch"
elif unit_settings.length_unit == "FEET":
return None, "foot"
elif unit_settings.system == "METRIC":
if unit_settings.length_unit == "METERS":
return None, "METRE"
return unit_settings.length_unit[0 : -len("METERS")], "METRE"
return round(quantity, 3) if quantity is not None else None
class CopyPropertyToSelection(bpy.types.Operator, Operator):
@@ -494,7 +456,7 @@ class BIM_OT_add_property_to_edit(bpy.types.Operator):
class BIM_OT_remove_property_to_edit(bpy.types.Operator):
bl_label = "Remove property to be renamed"
bl_label = "Remove Property to Be Renamed"
bl_idname = "bim.remove_property_to_edit"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -510,7 +472,7 @@ class BIM_OT_remove_property_to_edit(bpy.types.Operator):
class BIM_OT_clear_list(bpy.types.Operator):
bl_label = "Clear list of properties"
bl_label = "Clear List of Properties"
bl_idname = "bim.clear_list"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.StringProperty()
@@ -558,7 +520,7 @@ class BIM_OT_rename_parameters(bpy.types.Operator):
class BIM_OT_add_edit_custom_property(bpy.types.Operator):
bl_label = "Add or edit a custom property"
bl_label = "Add or Edit a Custom Property"
bl_idname = "bim.add_edit_custom_property"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -615,7 +577,7 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator):
class BIM_OT_bulk_remove_psets(bpy.types.Operator):
bl_label = "Bulk remove psets from selected objects"
bl_label = "Bulk Remove Psets from Selected Objects"
bl_idname = "bim.bulk_remove_psets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Bulk remove psets from selected objects"
@@ -26,10 +26,10 @@ from shapely.ops import unary_union
import blenderbim.tool as tool
import ifcopenshell
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
import blenderbim.bim
class QtoCalculator:
def __init__(self):
self.mapping_dict = {}
for key in mapper.keys():
@@ -46,6 +46,7 @@ class QtoCalculator:
self.mapping_dict[key][item] = None
def calculate_quantity(self, qto_name, quantity_name, obj):
"""calculates the value of the quantity in the project units"""
string = "self.mapping_dict[qto_name][quantity_name](obj"
if isinstance(mapper[qto_name][quantity_name], dict):
args = mapper[qto_name][quantity_name]["args"]
@@ -53,33 +54,53 @@ class QtoCalculator:
args = ""
string += args
string += ")"
return eval(string)
value = eval(string)
return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value
def guess_quantity(self, prop_name, alternative_prop_names, obj):
"""guess the value of the quantity by name, returns the value in the project units"""
prop_name = prop_name.lower()
alternative_prop_names = [p.lower() for p in alternative_prop_names]
value = None
if "length" in prop_name and "width" not in alternative_prop_names and "height" not in alternative_prop_names:
return self.get_linear_length(obj)
value = self.get_linear_length(obj)
elif "length" in prop_name:
return self.get_length(obj)
value = self.get_length(obj)
elif "width" in prop_name and "length" not in alternative_prop_names:
return self.get_length(obj)
value = self.get_length(obj)
elif "width" in prop_name:
return self.get_width(obj)
value = self.get_width(obj)
elif "height" in prop_name or "depth" in prop_name:
return self.get_height(obj)
value = self.get_height(obj)
elif "perimeter" in prop_name:
return self.get_net_perimeter(obj)
value = self.get_net_perimeter(obj)
elif "area" in prop_name and ("footprint" in prop_name or "section" in prop_name or "floor" in prop_name):
return self.get_net_footprint_area(obj)
value = self.get_net_footprint_area(obj)
elif "area" in prop_name and "side" in prop_name:
return self.get_side_area(obj)
value = self.get_side_area(obj)
elif "area" in prop_name:
return self.get_gross_surface_area(obj)
value = self.get_gross_surface_area(obj)
elif "volume" in prop_name and "gross" in prop_name:
return self.get_gross_volume(obj)
elif "volume" in prop_name :
return self.get_net_volume(obj)
value = self.get_gross_volume(obj)
elif "volume" in prop_name:
value = self.get_net_volume(obj)
if value is None:
return
unit_type_keywords = {
"length": "Q_LENGTH",
"width": "Q_LENGTH",
"height": "Q_LENGTH",
"depth": "Q_LENGTH",
"perimeter": "Q_LENGTH",
"area": "Q_AREA",
"volume": "Q_VOLUME",
}
unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name)
return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value
def get_units(self, o, vg_index):
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
@@ -100,7 +121,7 @@ class QtoCalculator:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
if self.get_object_main_axis(o) == "x" or main_axis=="x":
if self.get_object_main_axis(o) == "x" or main_axis == "x":
return max(x, y)
if self.get_object_main_axis(o) == "z":
return max(z, x)
@@ -226,7 +247,10 @@ class QtoCalculator:
decompositions = ifcopenshell.util.element.get_decomposition(element)
finish_floor_height = 0
for decomposition in decompositions:
if decomposition.get_info()['PredefinedType'] == 'FLOORING' and decomposition.get_info()['type'] == 'IfcCovering' :
if (
decomposition.get_info()["PredefinedType"] == "FLOORING"
and decomposition.get_info()["type"] == "IfcCovering"
):
floor_obj = tool.Ifc.get_object(decomposition)
new_finish_floor_height = self.get_height(floor_obj)
if new_finish_floor_height > finish_floor_height:
@@ -239,7 +263,10 @@ class QtoCalculator:
decompositions = ifcopenshell.util.element.get_decomposition(element)
finish_ceiling_height = 0
for decomposition in decompositions:
if decomposition.get_info()['PredefinedType'] == 'CEILING' and decomposition.get_info()['type'] == 'IfcCovering' :
if (
decomposition.get_info()["PredefinedType"] == "CEILING"
and decomposition.get_info()["type"] == "IfcCovering"
):
ceiling_obj = tool.Ifc.get_object(decomposition)
new_finish_ceiling_height = self.get_height(ceiling_obj)
if new_finish_ceiling_height > finish_ceiling_height:
@@ -247,8 +274,6 @@ class QtoCalculator:
return finish_ceiling_height
def get_net_perimeter(self, o):
parsed_edges = []
shared_edges = []
@@ -276,9 +301,9 @@ class QtoCalculator:
pass
def get_rectangular_perimeter(self, obj):
length = self.get_length(obj, main_axis='x')
length = self.get_length(obj, main_axis="x")
height = self.get_height(obj)
return (length+height)*2
return (length + height) * 2
def get_lowest_polygons(self, o):
lowest_polygons = []
@@ -297,7 +322,6 @@ class QtoCalculator:
return lowest_polygons
def get_highest_polygons(self, o):
highest_polygons = []
highest_z = None
for polygon in o.data.polygons:
@@ -327,8 +351,8 @@ class QtoCalculator:
total_net_floor_area = self.get_net_footprint_area(obj)
for decomposition in decompositions:
decomposition_type = decomposition.get_info()['type']
if decomposition_type == 'IfcColumn' or decomposition_type == 'IfcColumn':
decomposition_type = decomposition.get_info()["type"]
if decomposition_type == "IfcColumn" or decomposition_type == "IfcColumn":
decomposition_obj = tool.Ifc.get_object(decomposition)
net_footprint_obj_area = self.get_net_footprint_area(decomposition_obj)
total_net_floor_area -= net_footprint_obj_area
@@ -343,9 +367,9 @@ class QtoCalculator:
total_gross_ceiling_area = 0
for decomposition in decompositions:
decomposition_type = decomposition.get_info()['type']
decomposition_predefined_type = decomposition.get_info()['PredefinedType']
if decomposition_type == 'IfcCovering' and decomposition_predefined_type == 'CEILING':
decomposition_type = decomposition.get_info()["type"]
decomposition_predefined_type = decomposition.get_info()["PredefinedType"]
if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_gross_ceiling_area += self.get_gross_footprint_area(decomposition_obj)
@@ -359,13 +383,13 @@ class QtoCalculator:
total_net_ceiling_area = 0
for decomposition in decompositions:
decomposition_type = decomposition.get_info()['type']
decomposition_predefined_type = decomposition.get_info()['PredefinedType']
if decomposition_type == 'IfcCovering' and decomposition_predefined_type == 'CEILING':
decomposition_type = decomposition.get_info()["type"]
decomposition_predefined_type = decomposition.get_info()["PredefinedType"]
if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_net_ceiling_area += self.get_net_footprint_area(decomposition_obj)
if decomposition_type == 'IfcWall' or decomposition_type == 'IfcColumn':
if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_net_ceiling_area -= self.get_net_roofprint_area(decomposition_obj)
@@ -379,8 +403,8 @@ class QtoCalculator:
total_space_net_volume = self.get_gross_volume(obj)
for decomposition in decompositions:
decomposition_type = decomposition.get_info()['type']
if decomposition_type == 'IfcWall' or decomposition_type == 'IfcColumn':
decomposition_type = decomposition.get_info()["type"]
if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_space_net_volume -= self.get_net_volume(decomposition_obj)
@@ -541,33 +565,37 @@ class QtoCalculator:
or material.is_a("IfcMaterialProfileSet")
or material.is_a("IfcMaterialConstituentSet")
):
return
return
if material.is_a('IfcMaterial'):
if material.is_a("IfcMaterial"):
material_mass_density = ifcopenshell.util.element.get_pset(material, "Pset_MaterialCommon", "MassDensity")
return material_mass_density
if material.is_a('IfcMaterialLayerSetUsage'):
if material.is_a("IfcMaterialLayerSetUsage"):
material_layers = material.ForLayerSet.MaterialLayers
densities = []
thicknesses = []
obj_mass_density = 0
for material_layer in material_layers:
material_mass_density = ifcopenshell.util.element.get_pset(material_layer.Material, "Pset_MaterialCommon", "MassDensity")
material_mass_density = ifcopenshell.util.element.get_pset(
material_layer.Material, "Pset_MaterialCommon", "MassDensity"
)
if material_mass_density is None:
return
densities.append(material_mass_density)
thickness = material_layer.LayerThickness
thicknesses.append(thickness)
obj_mass_density = obj_mass_density + (material_mass_density* thickness)
obj_mass_density = obj_mass_density + (material_mass_density * thickness)
total_thickness = sum(thicknesses)
obj_mass_density = obj_mass_density/total_thickness
obj_mass_density = obj_mass_density / total_thickness
return obj_mass_density
if material.is_a('IfcMaterialProfileSetUsage'):
if material.is_a("IfcMaterialProfileSetUsage"):
material_profiles = material.ForProfileSet.MaterialProfiles
if len(material_profiles) == 1:
material_mass_density = ifcopenshell.util.element.get_pset(material_profiles[0].Material, "Pset_MaterialCommon", "MassDensity")
material_mass_density = ifcopenshell.util.element.get_pset(
material_profiles[0].Material, "Pset_MaterialCommon", "MassDensity"
)
return material_mass_density
else:
return
@@ -639,8 +667,8 @@ class QtoCalculator:
for opening in openings:
opening_id = opening.RelatedOpeningElement.GlobalId
ifc_opening_element = ifc.by_guid(opening_id)
#bl_opening_obj = tool.Ifc.get_object(ifc_opening_element)
#mesh = bpy.data.meshes.new('myMesh')
# bl_opening_obj = tool.Ifc.get_object(ifc_opening_element)
# mesh = bpy.data.meshes.new('myMesh')
mesh = self.get_gross_element_mesh(ifc_opening_element)
bl_opening_obj = bpy.data.objects.new("MyObject", mesh)
@@ -656,8 +684,12 @@ class QtoCalculator:
bl_OBB_opening_object = self.get_OBB_object(bl_opening_obj)
opening_area = self.get_lateral_area(
#self.get_OBB_object(bl_opening_obj), angle_z1=angle_z1, angle_z2=angle_z2, exclude_end_areas=True
bl_OBB_opening_object, angle_z1=angle_z1, angle_z2=angle_z2, exclude_end_areas=True, main_axis = 'x',
# self.get_OBB_object(bl_opening_obj), angle_z1=angle_z1, angle_z2=angle_z2, exclude_end_areas=True
bl_OBB_opening_object,
angle_z1=angle_z1,
angle_z2=angle_z2,
exclude_end_areas=True,
main_axis="x",
)
if opening_area >= min_area:
total_opening_area += opening_area
@@ -732,16 +764,16 @@ class QtoCalculator:
if not self.has_openings(obj):
return self.get_net_side_area(obj)
gross_side_area = self.get_lateral_area(obj, exclude_end_areas = True, subtract_openings = False, main_axis = 'x') / 2
gross_side_area = self.get_lateral_area(obj, exclude_end_areas=True, subtract_openings=False, main_axis="x") / 2
return gross_side_area
def get_net_side_area(self, obj):
net_side_area = self.get_lateral_area(obj, exclude_end_areas = True, main_axis = 'x') / 2
net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
return net_side_area
def get_outer_surface_area(self, obj):
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas = True, angle_z1 = 0, angle_z2 = 360)
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
return outer_surface_area
def get_end_area(self, obj):
@@ -751,7 +783,7 @@ class QtoCalculator:
gross_obj.matrix_world = obj.matrix_world
end_area = self.get_lateral_area(gross_obj, exclude_side_areas = True) / 2
end_area = self.get_lateral_area(gross_obj, exclude_side_areas=True) / 2
self.delete_obj(gross_obj)
self.delete_mesh(gross_mesh)
@@ -774,7 +806,7 @@ class QtoCalculator:
ifc = tool.Ifc.get()
ifc_element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id)
# if len(openings := ifc_element.HasOpenings) != 0:
# if len(openings := ifc_element.HasOpenings) != 0:
if len(openings := self.has_openings(obj)) != 0:
for opening in openings:
if opening.RelatedOpeningElement.PredefinedType == "OPENING":
@@ -813,8 +845,8 @@ class QtoCalculator:
if angle_to_z_axis < angle:
# offset the raycast, otherwise the raycast will always collide with the object.
offset = polygon.center+Vector((0,0,0.01))
if ignore_internal and obj.ray_cast(offset, (0,0,1))[0]:
offset = polygon.center + Vector((0, 0, 0.01))
if ignore_internal and obj.ray_cast(offset, (0, 0, 1))[0]:
continue
area += polygon.area
@@ -895,7 +927,8 @@ class QtoCalculator:
# add object to scene collection and then hide them.
collection.objects.get(new_OBB_object.name, collection.objects.link(new_OBB_object))
new_OBB_object.hide_set(True)
if bpy.context.view_layer.objects.get(new_OBB_object.name):
new_OBB_object.hide_set(True)
return new_OBB_object
@@ -941,14 +974,15 @@ class QtoCalculator:
new_AABB_object = bpy.data.objects.new(f"OBB_{ifc_id}", aabb_mesh)
new_AABB_object.matrix_world = obj.matrix_world
# create new collection for QtoCalculator
# create new collection for QtoCalculator
collection = bpy.data.collections.get("QtoCalculator", bpy.data.collections.new("QtoCalculator"))
if not bpy.context.scene.collection.children.get(collection.name):
bpy.context.scene.collection.children.link(collection)
# add object to scene collection and then hide them.
collection.objects.link(new_AABB_object)
new_AABB_object.hide_set(True)
if bpy.context.view_layer.objects.get(new_AABB_object.name):
new_AABB_object.hide_set(True)
return new_AABB_object
@@ -992,7 +1026,8 @@ class QtoCalculator:
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.bisect(plane_co=plane_co_neg, plane_no=plane_no_neg, use_fill=True, clear_outer=True)
bpy.ops.object.editmode_toggle()
bis_obj.hide_set(True)
if bpy.context.view_layer.objects.get(bis_obj.name):
bis_obj.hide_set(True)
return bis_obj
@@ -1095,12 +1130,7 @@ class QtoCalculator:
return 0
# touching polygons should be coplanar:
plane_intersection = mathutils.geometry.intersect_plane_plane(
center1,
normal1,
center2,
normal2
)
plane_intersection = mathutils.geometry.intersect_plane_plane(center1, normal1, center2, normal2)
# sometimes coplanar planes will interesect far off into the distance. This is a crude way of filtering out those intersections.
if plane_intersection[0] is None or (plane_intersection[0] - center1).magnitude > 20:
@@ -1209,15 +1239,16 @@ class QtoCalculator:
bpy.data.meshes.remove(mesh)
def delete_obj(self, obj):
bpy.data.objects.remove(obj, do_unlink = True)
bpy.data.objects.remove(obj, do_unlink=True)
# # Following code is here temporarily to test newly created functions:
#qto = QtoCalculator()
#o = bpy.context.active_object
#sel = bpy.context.selected_objects
# qto = QtoCalculator()
# o = bpy.context.active_object
# sel = bpy.context.selected_objects
#
#nl = '\n'
# nl = '\n'
# print(
# f"get_linear_length: {qto.get_linear_length(o)}{nl}{nl}"
# f"get_width: {qto.get_width(o)}{nl}{nl}"
@@ -1242,5 +1273,3 @@ class QtoCalculator:
# f"get_touching_objects(o, ['IfcElement']): {qto.get_touching_objects(o, ['IfcElement'])}{nl}{nl}"
# #f"get_contact_area: {qto.get_contact_area(o)}{nl}{nl}"
# )
@@ -268,6 +268,11 @@ class BIM_PT_material_psets(Panel):
def draw(self, context):
if not MaterialPsetsData.is_loaded:
MaterialPsetsData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialPsetsData.data["ifc_definition_id"]
):
MaterialPsetsData.load()
props = context.active_object.active_material.PsetProperties
row = self.layout.row(align=True)
@@ -161,7 +161,7 @@ class EnableEditingPropTemplate(bpy.types.Operator):
class DeletePropEnum(bpy.types.Operator):
bl_idname = "bim.delete_prop_enum"
bl_label = "delete property enumeration"
bl_label = "Delete Property Enumeration"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -173,7 +173,7 @@ class DeletePropEnum(bpy.types.Operator):
class AddPropEnum(bpy.types.Operator):
bl_idname = "bim.add_prop_enum"
bl_label = "add property enumeration"
bl_label = "Add Property Enumeration"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -186,7 +186,7 @@ class AssignBaseQto(bpy.types.Operator, tool.Ifc.Operator):
class CalculateAllQuantities(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_all_quantities"
bl_label = "Calculate all quantities"
bl_label = "Calculate All Quantities"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Calculate all possible quantities and assign them to selected object"
@@ -195,5 +195,5 @@ class CalculateAllQuantities(bpy.types.Operator, tool.Ifc.Operator):
return tool.Ifc.get() and context.selected_objects
def _execute(self, context):
core.calculate_objects_base_quantities(tool.Ifc, tool.Qto, QtoCalculator(), selected_objects = context.selected_objects)
core.calculate_objects_base_quantities(tool.Ifc, tool.Cost, tool.Qto, QtoCalculator(), selected_objects = context.selected_objects)
return {"FINISHED"}
@@ -47,7 +47,7 @@ class LoadResourceProperties(bpy.types.Operator):
class AddResource(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_resource"
bl_label = "Add resource"
bl_label = "Add Resource"
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty()
parent_resource: bpy.props.IntProperty()
@@ -74,7 +74,7 @@ class EnableEditingResource(bpy.types.Operator):
class DisableEditingResource(bpy.types.Operator):
bl_idname = "bim.disable_editing_resource"
bl_label = "Disable Editing Resources"
bl_label = "Disable Editing Resource"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -161,7 +161,7 @@ class UnassignResource(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingResourceTime(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_resource_time"
bl_label = "Enable Editing Resource Usage"
bl_label = "Enable Editing Resource Time"
bl_options = {"REGISTER", "UNDO"}
resource: bpy.props.IntProperty()
@@ -267,7 +267,7 @@ class EditResourceCostValue(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingResourceBaseQuantity(bpy.types.Operator):
bl_idname = "bim.enable_editing_resource_base_quantity"
bl_label = "Enable Editing Resource Quantity"
bl_label = "Enable Editing Resource Base Quantity"
bl_options = {"REGISTER", "UNDO"}
resource: bpy.props.IntProperty()
@@ -168,9 +168,14 @@ class UnlinkObject(bpy.types.Operator):
objects = context.selected_objects
for obj in objects:
if obj.BIMObjectProperties.ifc_definition_id:
if obj in IfcStore.edited_objs:
IfcStore.edited_objs.remove(obj)
IfcStore.unlink_element(obj=obj)
if obj.data:
obj.data = obj.data.copy()
for material_slot in obj.material_slots:
if material_slot.material:
material_slot.material = material_slot.material.copy()
blenderbim.core.style.unlink_style(tool.Ifc, tool.Style, obj=material_slot.material)
blenderbim.core.material.unlink_material(tool.Ifc, obj=material_slot.material)
if "Ifc" in obj.name and "/" in obj.name:
@@ -20,7 +20,6 @@ import bpy
from . import ui, prop, operator
classes = (
operator.HighlightProductRelatedTask,
operator.ExpandAllTasks,
operator.ContractAllTasks,
operator.AddSummaryTask,
@@ -113,6 +112,8 @@ classes = (
operator.VisualiseWorkScheduleDateRange,
operator.LoadTaskAnimationColors,
operator.DisableEditingTaskAnimationColors,
operator.LoadProductTasks,
operator.HighlightTask,
prop.WorkPlan,
prop.BIMWorkPlanProperties,
prop.Task,
@@ -140,6 +141,8 @@ classes = (
ui.BIM_PT_4D_Tools,
ui.BIM_PT_Task_Bar_Creator,
ui.BIM_UL_animation_colors,
ui.BIM_UL_product_input_tasks,
ui.BIM_UL_product_output_tasks,
)
@@ -53,7 +53,7 @@ def parse_duration_as_blender_props(dt, simplify=True):
seconds = getattr(dt, "seconds", 0)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
days = getattr(dt, "days",0)
days = getattr(dt, "days", 0)
months = int(getattr(dt, "months", 0))
years = int(getattr(dt, "years", 0))
return {
@@ -170,7 +170,7 @@ class EnableEditingWorkSchedule(bpy.types.Operator):
class EnableEditingWorkScheduleTasks(bpy.types.Operator):
bl_idname = "bim.enable_editing_work_schedule_tasks"
bl_label = "Enable Editing Tasks"
bl_label = "Enable Editing Work Schedule Tasks"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
@@ -454,11 +454,8 @@ class UnassignProcess(bpy.types.Operator):
)
else:
core.unassign_input_products(
tool.Ifc,
tool.Sequence,
tool.Spatial,
task=tool.Ifc.get().by_id(self.task)
)
tool.Ifc, tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task)
)
elif self.related_object_type == "CONTROL":
pass # TODO
return {"FINISHED"}
@@ -606,7 +603,7 @@ class ImportP6XER(bpy.types.Operator, ImportHelper):
class ImportPP(bpy.types.Operator, ImportHelper):
bl_idname = "import_pp.bim"
bl_label = "Import Powerproject pp"
bl_label = "Import Powerproject .pp"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".pp"
filter_glob: bpy.props.StringProperty(default="*.pp", options={"HIDDEN"})
@@ -964,7 +961,7 @@ class EditSequenceTimeLag(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingSequence(bpy.types.Operator):
bl_idname = "bim.disable_editing_sequence"
bl_label = "Disable Editing Sequence Attributes"
bl_label = "Disable Editing Sequence"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -1006,6 +1003,7 @@ class VisualiseWorkScheduleDate(bpy.types.Operator):
core.visualise_work_schedule_date(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule))
return {"FINISHED"}
class GuessDateRange(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.guess_date_range"
bl_label = "Guess Work Schedule Date Range"
@@ -1024,7 +1022,10 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator):
@classmethod
def poll(cls, context):
has_start, has_finish = bpy.context.scene.BIMWorkScheduleProperties.visualisation_start, bpy.context.scene.BIMWorkScheduleProperties.visualisation_finish
has_start, has_finish = (
bpy.context.scene.BIMWorkScheduleProperties.visualisation_start,
bpy.context.scene.BIMWorkScheduleProperties.visualisation_finish,
)
return bool(has_start and has_finish) and not "-" in (has_start, has_finish)
def execute(self, context):
@@ -1106,7 +1107,7 @@ class BlenderBIM_DatePicker(bpy.types.Operator):
class BlenderBIM_DatePickerSetDate(bpy.types.Operator):
bl_label = "set date"
bl_label = "Set Date"
bl_idname = "bim.datepicker_setdate"
bl_options = {"REGISTER", "UNDO"}
selected_date: bpy.props.StringProperty()
@@ -1117,7 +1118,7 @@ class BlenderBIM_DatePickerSetDate(bpy.types.Operator):
class BlenderBIM_RedrawDatePicker(bpy.types.Operator):
bl_label = "redraw datepicker window"
bl_label = "Redraw Datepicker Window"
bl_idname = "bim.redraw_datepicker"
bl_options = {"REGISTER", "UNDO"}
action: bpy.props.StringProperty()
@@ -1230,20 +1231,9 @@ class CalculateTaskDuration(bpy.types.Operator, tool.Ifc.Operator):
core.calculate_task_duration(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
class HighlightProductRelatedTask(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.highlight_product_related_task"
bl_label = "Highlights the related task"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Finds the related Task"
product_type: bpy.props.StringProperty()
def _execute(self, context):
core.highlight_product_related_task(tool.Sequence, tool.Spatial, product_type=self.product_type)
class ExpandAllTasks(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.expand_all_tasks"
bl_label = "Expands all tasks"
bl_label = "Expands All Tasks"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Finds the related Task"
product_type: bpy.props.StringProperty()
@@ -1254,7 +1244,7 @@ class ExpandAllTasks(bpy.types.Operator, tool.Ifc.Operator):
class ContractAllTasks(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.contract_all_tasks"
bl_label = "Expands all tasks"
bl_label = "Expands All Tasks"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Finds the related Task"
product_type: bpy.props.StringProperty()
@@ -1286,13 +1276,14 @@ class LoadTaskAnimationColors(bpy.types.Operator):
class DisableEditingTaskAnimationColors(bpy.types.Operator):
bl_idname = "bim.disable_editing_task_animation_colors"
bl_label = "Disable Editing Colors"
bl_label = "Disable Editing Task Animation Colors"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.disable_editing_task_animation_colors(tool.Sequence)
return {"FINISHED"}
class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_task"
bl_label = "Copy Task"
@@ -1300,4 +1291,39 @@ class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
task: bpy.props.IntProperty()
def _execute(self, context):
core.duplicate_task(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
core.duplicate_task(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
class LoadProductTasks(bpy.types.Operator):
bl_idname = "bim.load_product_tasks"
bl_label = "Load Product Tasks"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
return False
if not context.active_object:
return False
if not context.active_object.BIMObjectProperties.ifc_definition_id:
return False
return True
def execute(self, context):
core.load_product_tasks(
tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
return {"FINISHED"}
class HighlightTask(bpy.types.Operator):
bl_idname = "bim.highlight_task"
bl_label = "Highlight Task"
bl_options = {"REGISTER", "UNDO"}
task: bpy.props.IntProperty()
def execute(self, context):
r = core.highlight_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
@@ -96,6 +96,14 @@ def update_active_task_outputs(self, context):
bpy.ops.bim.load_task_outputs()
def update_active_task_resources(self, context):
bpy.ops.bim.load_task_resources()
def update_active_task_inputs(self, context):
bpy.ops.bim.load_task_inputs()
def updateTaskName(self, context):
props = context.scene.BIMWorkScheduleProperties
if not props.is_task_update_enabled or self.name == "Unnamed":
@@ -389,9 +397,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_input_index: IntProperty(name="Active Task Input Index")
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
active_task_output_index: IntProperty(name="Active Task Output Index")
is_nested_task_outputs: BoolProperty(
name="Is Nested Task Outputs", default=False, update=update_active_task_outputs
)
show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs)
show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources)
show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs)
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
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")
class BIMTaskTreeProperties(PropertyGroup):
@@ -433,6 +433,8 @@ class BIM_PT_task_icom(Panel):
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
op.task = task.ifc_definition_id
row2 = col.row()
row2.prop(self.props, "show_nested_inputs", text="Show Nested")
row2 = col.row()
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
@@ -456,6 +458,9 @@ class BIM_PT_task_icom(Panel):
op.related_object_type = "RESOURCE"
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
row2 = col.row()
row2.prop(self.props, "show_nested_resources", text="Show Nested")
row2 = col.row()
row2.template_list(
"BIM_UL_task_resources", "", self.props, "task_resources", self.props, "active_task_resource_index"
@@ -481,7 +486,7 @@ class BIM_PT_task_icom(Panel):
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select")
op.task = task.ifc_definition_id
row2 = col.row()
row2.prop(self.props, "is_nested_task_outputs", text="Show Nested")
row2.prop(self.props, "show_nested_outputs", text="Show Nested")
row2 = col.row()
row2.template_list(
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
@@ -534,6 +539,26 @@ class BIM_UL_task_outputs(UIList):
row.prop(item, "name", emboss=False, text="")
class BIM_UL_product_input_tasks(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE")
op.task = item.ifc_definition_id
row.split(factor=0.8)
row.label(text=item.name)
class BIM_UL_product_output_tasks(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE")
op.task = item.ifc_definition_id
row.split(factor=0.8)
row.label(text=item.name)
class BIM_UL_tasks(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
@@ -798,14 +823,32 @@ class BIM_PT_4D_Tools(Panel):
bl_category = "4D/5D Toolkit"
def draw(self, context):
self.props = context.scene.BIMWorkScheduleProperties
row = self.layout.row()
row.operator(
"bim.highlight_product_related_task", text="Find Input-related Task ", icon="STYLUS_PRESSURE"
).product_type = "Input"
row = self.layout.row()
row.operator(
"bim.highlight_product_related_task", text="Go to Output-related Task ", icon="STYLUS_PRESSURE"
).product_type = "Output"
row.operator("bim.load_product_tasks", text="Load Tasks", icon="FILE_REFRESH")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col1 = grid.column()
col1.label(text="Product Input Tasks")
col1.template_list(
"BIM_UL_product_input_tasks",
"",
self.props,
"product_input_tasks",
self.props,
"active_product_input_task_index",
)
col2 = grid.column()
col2.label(text="Product Output Tasks")
col2.template_list(
"BIM_UL_product_output_tasks",
"",
self.props,
"product_output_tasks",
self.props,
"active_product_output_task_index",
)
class BIM_PT_Task_Bar_Creator(Panel):
@@ -34,6 +34,7 @@ class StylesData:
@classmethod
def load(cls):
cls.data = {"style_types": cls.style_types(), "total_styles": cls.total_styles()}
cls.is_loaded = True
@classmethod
def style_types(cls):
@@ -55,7 +56,12 @@ class StyleAttributesData:
@classmethod
def load(cls):
cls.data = {"attributes": cls.get_attributes()}
cls.data = {"ifc_style_id": cls.ifc_style_id(), "attributes": cls.get_attributes()}
cls.is_loaded = True
@classmethod
def ifc_style_id(cls):
return bpy.context.active_object.active_material.BIMMaterialProperties.ifc_style_id
@classmethod
def get_attributes(cls):
@@ -23,8 +23,6 @@ from blenderbim.bim.module.style.data import StylesData, StyleAttributesData
from bl_ui.properties_material import MaterialButtonsPanel
class BIM_PT_styles(Panel):
bl_label = "IFC Styles"
bl_idname = "BIM_PT_styles"
@@ -93,7 +91,7 @@ class BIM_PT_style(MaterialButtonsPanel, Panel):
row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id
row = self.layout.row(align=True)
row.prop(mat, "diffuse_color", text="Color")
else:
row.operator("bim.add_style", icon="ADD")
@@ -117,6 +115,11 @@ class BIM_PT_style_attributes(Panel):
def draw(self, context):
if not StyleAttributesData.is_loaded:
StyleAttributesData.load()
elif (
context.active_object.active_material.BIMMaterialProperties.ifc_style_id
!= StyleAttributesData.data["ifc_style_id"]
):
StyleAttributesData.load()
obj = context.active_object.active_material
mprops = obj.BIMMaterialProperties
@@ -64,19 +64,50 @@ class UnassignType(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
def exclude_callback(attribute):
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
self.file = IfcStore.get_file()
related_objects = (
[bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
)
for related_object in related_objects:
oprops = related_object.BIMObjectProperties
ifcopenshell.api.run(
"type.unassign_type",
self.file,
**{
"related_object": self.file.by_id(oprops.ifc_definition_id),
},
)
objs = [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element or element.is_a("IfcElementType"):
continue
ifcopenshell.api.run("type.unassign_type", self.file, related_object=element)
active_representation = tool.Geometry.get_active_representation(obj)
active_context = active_representation.ContextOfItems
new_active_representation = None
if element.Representation:
representations = []
for representation in element.Representation.Representations:
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
if representation == resolved_representation:
representations.append(representation)
else:
# We must unmap representations.
copied_representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
resolved_representation,
exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
)
representations.append(copied_representation)
if representation.ContextOfItems == active_context:
new_active_representation = copied_representation
element.Representation.Representations = representations
if new_active_representation:
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_active_representation,
should_reload=False,
is_global=False,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -197,6 +228,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
context=body,
ifc_representation_class=None,
)
elif template in ("LAYERSET_AXIS2", "LAYERSET_AXIS3"):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
obj = bpy.data.objects.new(name, None)
@@ -230,6 +262,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
elif template == "LAYERSET_AXIS3":
axis = "AXIS3"
ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={"LayerSetDirection": axis})
elif template == "PROFILESET":
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
obj = bpy.data.objects.new(name, None)
@@ -20,6 +20,7 @@ import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.type.data import TypeData
@@ -69,7 +70,7 @@ class BIM_PT_type(Panel):
row.prop(props, "relating_type_class", text="")
if type_prop.get_relating_type(None, context):
row.prop(props, "relating_type", text="")
prop_with_search(row, props, "relating_type", text="")
row.operator("bim.assign_type", icon="CHECKMARK", text="")
else:
row.label(text="No Types Found")
+51 -27
View File
@@ -101,6 +101,7 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ReloadSelectedIfcFile(bpy.types.Operator, IFCFileSelector):
bl_idname = "bim.reload_selected_ifc_file"
bl_label = "Reload selected IFC File"
@@ -114,6 +115,7 @@ class ReloadSelectedIfcFile(bpy.types.Operator, IFCFileSelector):
context.scene.BIMProperties.ifc_file = context.scene.BIMProperties.ifc_file
return {"FINISHED"}
class SelectDataDir(bpy.types.Operator):
bl_idname = "bim.select_data_dir"
bl_label = "Select Data Directory"
@@ -211,6 +213,11 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
def create_section_compare_node(self):
group = bpy.data.node_groups.new("Section Compare", type="ShaderNodeTree")
group.inputs.new("NodeSocketFloat", "Value")
group.inputs["Value"].default_value = 1.0 # Mandatory multiplier for the last node group
group.inputs.new("NodeSocketVector", "Vector")
group.outputs.new("NodeSocketFloat", "Value")
group.outputs.new("NodeSocketFloat", "Line Decorator")
group_input = group.nodes.new(type="NodeGroupInput")
group_input.location = 0, 50
@@ -222,6 +229,12 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
greater.inputs[1].default_value = 0
greater.location = 400, 0
compare = group.nodes.new(type="ShaderNodeMath")
compare.operation = "COMPARE"
compare.inputs[1].default_value = 0
compare.inputs[2].default_value = 0.04
compare.location = 400, -200
multiply = group.nodes.new(type="ShaderNodeMath")
multiply.operation = "MULTIPLY"
multiply.inputs[0].default_value = 1
@@ -230,57 +243,68 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
group_output = group.nodes.new(type="NodeGroupOutput")
group_output.location = 800, 0
group.links.new(group_input.outputs[""], multiply.inputs[0])
group.links.new(group_input.outputs[""], separate_xyz.inputs[0])
group.links.new(group_input.outputs["Value"], multiply.inputs[0])
group.links.new(group_input.outputs["Vector"], separate_xyz.inputs[0])
group.links.new(separate_xyz.outputs[2], greater.inputs[0])
group.links.new(greater.outputs[0], multiply.inputs[1])
group.links.new(multiply.outputs[0], group_output.inputs[""])
group.links.new(multiply.outputs[0], group_output.inputs["Value"])
group.links.new(separate_xyz.outputs[2], compare.inputs[0])
group.links.new(compare.outputs[0], group_output.inputs["Line Decorator"])
def create_section_override_node(self, obj, context):
group = bpy.data.node_groups.new("Section Override", type="ShaderNodeTree")
group.inputs.new("NodeSocketShader", "Shader")
group.outputs.new("NodeSocketShader", "Shader")
links = group.links
nodes = group.nodes
group_input = nodes.new(type="NodeGroupInput")
group_output = nodes.new(type="NodeGroupOutput")
group_output.location = 600, 250
group_output.location = 800, 250
backfacing_mix = nodes.new(type="ShaderNodeMixShader")
backfacing_mix.location = group_output.location - Vector((400, 350))
mix_decorator = group.nodes.new(type="ShaderNodeMixShader")
mix_decorator.name = "Line Decorator Mix"
mix_decorator.location = group_output.location - Vector((200, 0))
mix_section = group.nodes.new(type="ShaderNodeMixShader")
mix_section.name = "Section Mix"
mix_section.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway
mix_section.location = mix_decorator.location - Vector((200, 200))
transparent = nodes.new(type="ShaderNodeBsdfTransparent")
transparent.location = mix_section.location - Vector((200, 100))
mix_backfacing = nodes.new(type="ShaderNodeMixShader")
mix_backfacing.location = mix_section.location - Vector((200, 0))
group_input.location = mix_backfacing.location - Vector((200, 50))
backfacing = nodes.new(type="ShaderNodeNewGeometry")
backfacing.location = backfacing_mix.location + Vector((-200, 200))
group_input.location = backfacing_mix.location - Vector((200, 50))
backfacing.location = mix_backfacing.location + Vector((-200, 200))
emission = nodes.new(type="ShaderNodeEmission")
emission.inputs[0].default_value = list(context.scene.BIMProperties.section_plane_colour) + [1]
emission.location = backfacing_mix.location - Vector((200, 150))
transparent = nodes.new(type="ShaderNodeBsdfTransparent")
transparent.location = group_output.location - Vector((400, 100))
section_mix = group.nodes.new(type="ShaderNodeMixShader")
section_mix.name = "Section Mix"
section_mix.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway
section_mix.location = group_output.location - Vector((200, 0))
emission.location = mix_backfacing.location - Vector((200, 150))
cut_obj = nodes.new(type="ShaderNodeTexCoord")
cut_obj.object = obj
cut_obj.location = group_output.location - Vector((800, 150))
cut_obj.location = backfacing.location - Vector((200, 200))
section_compare = nodes.new(type="ShaderNodeGroup")
section_compare.node_tree = bpy.data.node_groups.get("Section Compare")
section_compare.name = "Last Section Compare"
section_compare.location = group_output.location - Vector((600, 0))
section_compare.location = backfacing.location + Vector((0, 200))
links.new(cut_obj.outputs["Object"], section_compare.inputs[1])
links.new(backfacing.outputs["Backfacing"], backfacing_mix.inputs[0])
links.new(group_input.outputs[""], backfacing_mix.inputs[1])
links.new(emission.outputs["Emission"], backfacing_mix.inputs[2])
links.new(section_compare.outputs[0], section_mix.inputs[0])
links.new(transparent.outputs["BSDF"], section_mix.inputs[1])
links.new(backfacing_mix.outputs["Shader"], section_mix.inputs[2])
links.new(section_mix.outputs["Shader"], group_output.inputs[""])
links.new(backfacing.outputs["Backfacing"], mix_backfacing.inputs[0])
links.new(group_input.outputs["Shader"], mix_backfacing.inputs[1])
links.new(emission.outputs["Emission"], mix_backfacing.inputs[2])
links.new(section_compare.outputs["Value"], mix_section.inputs[0])
links.new(transparent.outputs[0], mix_section.inputs[1])
links.new(mix_backfacing.outputs["Shader"], mix_section.inputs[2])
links.new(section_compare.outputs["Line Decorator"], mix_decorator.inputs[0])
links.new(mix_section.outputs["Shader"], mix_decorator.inputs[1])
links.new(mix_decorator.outputs["Shader"], group_output.inputs["Shader"])
def append_obj_to_section_override_node(self, obj):
group = bpy.data.node_groups.get("Section Override")
@@ -695,7 +719,7 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
class EditBlenderCollection(bpy.types.Operator):
bl_idname = "bim.edit_blender_collection"
bl_label = "Add or Remove blender collection item"
bl_label = "Add or Remove Blender Collection Item"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.StringProperty(description="add or remove item from collection")
collection: bpy.props.StringProperty(description="collection to be edited")
+20 -17
View File
@@ -173,6 +173,18 @@ def update_section_color(self, context):
pass
def update_section_line_decorator(self, context):
compare_node_group = bpy.data.node_groups.get("Section Compare")
if compare_node_group is None:
return
for node in compare_node_group.nodes:
if not hasattr(node, "operation"):
continue
if node.operation == "COMPARE":
node.inputs[2].default_value = self.section_line_decorator_width
break
class StrProperty(PropertyGroup):
pass
@@ -313,13 +325,20 @@ class BIMProperties(PropertyGroup):
last_transaction: StringProperty(name="Last Transaction")
should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False)
section_plane_colour: FloatVectorProperty(
name="Temporary Section Cutaway Colour",
name="Cutaway Colour",
subtype="COLOR",
default=(1, 0, 0),
min=0.0,
max=1.0,
update=update_section_color,
)
section_line_decorator_width: FloatProperty(
name="Line Decorator Width",
default=0.04,
min=0.0,
soft_max=1.0,
update=update_section_line_decorator,
)
area_unit: EnumProperty(
default="SQUARE_METRE",
items=[
@@ -353,22 +372,6 @@ class BIMProperties(PropertyGroup):
],
name="IFC Volume Unit",
)
metric_precision: FloatProperty(default=0, name="Drawing Metric Precision")
imperial_precision: EnumProperty(
items=[
("NONE", "No rounding", ""),
("1", 'Nearest 1"', ""),
("1/2", 'Nearest 1/2"', ""),
("1/4", 'Nearest 1/4"', ""),
("1/8", 'Nearest 1/8"', ""),
("1/16", 'Nearest 1/16"', ""),
("1/32", 'Nearest 1/32"', ""),
("1/64", 'Nearest 1/64"', ""),
("1/128", 'Nearest 1/128"', ""),
("1/256", 'Nearest 1/256"', ""),
],
name="Drawing Imperial Precision",
)
class IfcParameter(PropertyGroup):
+26 -10
View File
@@ -78,11 +78,9 @@ class BIM_PT_section_plane(Panel):
layout.use_property_split = True
props = context.scene.BIMProperties
row = layout.row()
row.prop(props, "should_section_selected_objects")
row = layout.row()
row.prop(props, "section_plane_colour")
layout.prop(props, "should_section_selected_objects")
layout.prop(props, "section_plane_colour")
layout.prop(props, "section_line_decorator_width")
row = layout.row(align=True)
row.operator("bim.add_section_plane")
@@ -175,6 +173,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row = self.layout.row()
row.prop(context.scene.DocProperties, "decorations_colour")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_selected")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_unselected")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_special")
row = self.layout.row(align=True)
row.prop(context.scene.BIMProperties, "schema_dir")
@@ -184,6 +188,23 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.prop(context.scene.BIMProperties, "data_dir")
row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "sheets_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "layouts_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "titleblocks_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "drawings_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "stylesheet_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "markers_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "symbols_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "patterns_path")
row = layout.row()
row.operator("bim.configure_visibility")
@@ -198,11 +219,6 @@ def ifc_units(self, context):
row.prop(props, "area_unit")
row = layout.row()
row.prop(props, "volume_unit")
row = layout.row()
if scene.unit_settings.system == "IMPERIAL":
row.prop(props, "imperial_precision")
else:
row.prop(props, "metric_precision")
# Scene panel groups
+140 -34
View File
@@ -45,13 +45,13 @@ def disable_editing_assigned_product(drawing, obj=None):
def edit_assigned_product(ifc, drawing, obj=None, product=None):
element = ifc.get_entity(obj)
existing_product = drawing.get_assigned_product(element)
if existing_product == product:
return
if existing_product:
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
if product:
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
drawing.update_text_value(obj)
if existing_product != product:
if existing_product:
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
if product:
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
drawing.update_text_value(obj)
drawing.disable_editing_assigned_product(obj)
@@ -66,26 +66,71 @@ def disable_editing_sheets(drawing):
def add_sheet(ifc, drawing, titleblock=None):
sheet = ifc.run("document.add_information")
layout = ifc.run("document.add_reference", information=sheet)
titleblock_reference = ifc.run("document.add_reference", information=sheet)
identification = drawing.generate_sheet_identification()
identification = drawing.ensure_unique_identification(identification)
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "DOCUMENTATION"}
if ifc.get_schema() == "IFC2X3":
attributes["DocumentId"] = attributes["Identification"]
del attributes["Identification"]
attributes = {"DocumentId": identification, "Name": "UNTITLED", "Scope": "SHEET"}
else:
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "SHEET"}
ifc.run("document.edit_information", information=sheet, attributes=attributes)
ifc.run(
"document.edit_reference",
reference=layout,
attributes={"Location": drawing.get_default_layout_path(identification, "UNTITLED"), "Description": "LAYOUT"},
)
ifc.run(
"document.edit_reference",
reference=titleblock_reference,
attributes={"Location": drawing.get_default_titleblock_path(titleblock), "Description": "TITLEBLOCK"},
)
drawing.create_svg_sheet(sheet, titleblock)
drawing.import_sheets()
def open_sheet(drawing, sheet=None):
drawing.open_svg(drawing.get_document_uri(sheet))
drawing.open_svg(drawing.get_document_uri(sheet, "LAYOUT"))
def remove_sheet(ifc, drawing, sheet=None):
for reference in drawing.get_document_references(sheet):
if drawing.get_reference_description(reference) in ("LAYOUT", "SHEET", "REVISION", "RASTER"):
uri = ifc.resolve_uri(drawing.get_document_uri(reference))
if drawing.does_file_exist(uri):
drawing.delete_file(uri)
ifc.run("document.remove_information", information=sheet)
drawing.import_sheets()
def rename_sheet(ifc, drawing, sheet=None, identification=None, name=None):
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":
old_location = drawing.get_reference_location(reference)
new_location = drawing.get_default_sheet_path(identification, name)
if old_location != new_location:
ifc.run("document.edit_reference", reference=reference, attributes={"Location": new_location})
old_location = ifc.resolve_uri(old_location)
if drawing.does_file_exist(old_location):
drawing.move_file(old_location, ifc.resolve_uri(new_location))
elif description == "LAYOUT":
old_location = drawing.get_reference_location(reference)
new_location = drawing.get_default_layout_path(identification, name)
if old_location != new_location:
ifc.run("document.edit_reference", reference=reference, attributes={"Location": new_location})
old_location = ifc.resolve_uri(old_location)
if drawing.does_file_exist(old_location):
drawing.move_file(old_location, ifc.resolve_uri(new_location))
def rename_reference(ifc, reference=None, identification=None):
ifc.run("document.edit_reference", reference=reference, attributes={"Identification": identification})
def load_schedules(drawing):
drawing.import_schedules()
drawing.enable_editing_schedules()
@@ -98,13 +143,13 @@ def disable_editing_schedules(drawing):
def add_schedule(ifc, drawing, uri=None):
schedule = ifc.run("document.add_information")
reference = ifc.run("document.add_reference", information=schedule)
name = drawing.get_path_filename(uri)
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"}
ifc.run("document.edit_information", information=schedule, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
attributes = {"DocumentId": "X", "Name": name, "Scope": "SCHEDULE"}
else:
attributes = {"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": uri}
ifc.run("document.edit_information", information=schedule, attributes=attributes)
attributes = {"Identification": "X", "Name": name, "Scope": "SCHEDULE"}
ifc.run("document.edit_information", information=schedule, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
drawing.import_schedules()
@@ -114,7 +159,7 @@ def remove_schedule(ifc, drawing, schedule=None):
def open_schedule(drawing, schedule=None):
drawing.open_spreadsheet(drawing.get_schedule_location(schedule))
drawing.open_spreadsheet(drawing.get_document_uri(schedule))
def update_schedule_name(ifc, drawing, schedule=None, name=None):
@@ -148,19 +193,39 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
ifc.run("group.assign_group", group=group, products=[element])
collector.assign(camera)
pset = ifc.run("pset.add_pset", product=element, name="EPset_Drawing")
if drawing.get_unit_system() == "METRIC":
scale = "1/100"
human_scale = "1:100"
else:
scale = "1/96"
human_scale = "1/8\"=1'-0\""
ifc.run(
"pset.edit_pset",
pset=pset,
properties={
"TargetView": target_view,
"Scale": "1/100",
"HumanScale": "1:100",
"Scale": scale,
"HumanScale": human_scale,
"HasUnderlay": False,
"HasLinework": True,
"HasAnnotation": True,
"GlobalReferencing": True,
"Stylesheet": drawing.get_default_drawing_resource_path("Stylesheet"),
"Markers": drawing.get_default_drawing_resource_path("Markers"),
"Symbols": drawing.get_default_drawing_resource_path("Symbols"),
"Patterns": drawing.get_default_drawing_resource_path("Patterns"),
},
)
information = ifc.run("document.add_information")
uri = drawing.get_default_drawing_path(drawing_name)
reference = ifc.run("document.add_reference", information=information)
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": "X", "Name": drawing_name, "Scope": "DRAWING"}
else:
attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"}
ifc.run("document.edit_information", information=information, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
ifc.run("document.assign_document", product=element, document=reference)
drawing.import_drawings()
@@ -182,6 +247,21 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati
drawing_tool.copy_representation(annotation, new_annotation)
ifc.run("group.unassign_group", group=group, product=new_annotation)
ifc.run("group.assign_group", group=new_group, products=[new_annotation])
old_reference = drawing_tool.get_drawing_document(new_drawing)
ifc.run("document.unassign_document", product=new_drawing, document=old_reference)
information = ifc.run("document.add_information")
uri = drawing_tool.get_default_drawing_path(drawing_name)
reference = ifc.run("document.add_reference", information=information)
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": "X", "Name": drawing_name, "Scope": "DRAWING"}
else:
attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"}
ifc.run("document.edit_information", information=information, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
ifc.run("document.assign_document", product=new_drawing, document=reference)
drawing_tool.import_drawings()
return new_drawing
@@ -190,16 +270,24 @@ def remove_drawing(ifc, drawing_tool, drawing=None):
collection = drawing_tool.get_drawing_collection(drawing)
if collection:
drawing_tool.delete_collection(collection)
group = drawing_tool.get_drawing_group(drawing)
if group:
drawing_tool.delete_drawing_elements(drawing_tool.get_group_elements(group))
ifc.run("group.remove_group", group=group)
for reference in drawing_tool.get_drawing_references(drawing):
reference_obj = ifc.get_object(reference)
if reference_obj:
drawing_tool.delete_object(reference_obj)
ifc.run("root.remove_product", product=reference)
ifc.run("root.remove_product", product=drawing)
information = drawing_tool.get_reference_document(drawing_tool.get_drawing_document(drawing))
uri = ifc.resolve_uri(drawing_tool.get_document_uri(information))
if drawing_tool.does_file_exist(uri):
drawing_tool.delete_file(uri)
ifc.run("document.remove_information", information=information)
group = drawing_tool.get_drawing_group(drawing)
if group:
drawing_tool.delete_drawing_elements(drawing_tool.get_group_elements(group))
ifc.run("group.remove_group", group=group)
drawing_tool.import_drawings()
@@ -213,11 +301,31 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None):
if collection:
drawing_tool.set_drawing_collection_name(group, collection)
reference = drawing_tool.get_drawing_document(drawing)
information = drawing_tool.get_reference_document(reference)
ifc.run("document.edit_information", information=information, attributes={"Name": name})
old_location = drawing_tool.get_reference_location(reference)
new_location = drawing_tool.get_default_drawing_path(name)
if old_location != new_location:
ifc.run("document.edit_reference", reference=reference, attributes={"Location": new_location})
resolved_old_location = ifc.resolve_uri(old_location)
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)
def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None):
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))
context = drawing_tool.get_annotation_context(target_view := drawing_tool.get_drawing_target_view(drawing))
if not context:
return "No annotation context for drawing"
return f"No annotation context Annotation/{target_view} for drawing"
drawing_tool.show_decorations()
obj = drawing_tool.create_annotation_object(drawing, object_type)
element = ifc.get_entity(obj)
@@ -237,7 +345,7 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
def build_schedule(drawing, schedule=None):
drawing.create_svg_schedule(schedule)
drawing.open_svg(drawing.get_document_uri(schedule))
drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg"))
def sync_references(ifc, collector, drawing_tool, drawing=None):
@@ -257,11 +365,9 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
should_create_annotation = False
if annotation:
if ifc.is_deleted(annotation):
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
should_delete_existing_annotation = True
elif reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
should_delete_existing_annotation = True
elif not reference_obj and ifc.is_deleted(reference_element):
elif not reference_obj:
should_delete_existing_annotation = True
if reference_obj and (should_delete_existing_annotation or not annotation):
@@ -287,8 +393,8 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
drawing_tool.sync_object_representation(reference_obj)
def select_assigned_product(drawing):
drawing.select_assigned_product()
def select_assigned_product(drawing, context):
drawing.select_assigned_product(context)
def activate_drawing_view(ifc, drawing_tool, drawing):
@@ -296,4 +402,4 @@ def activate_drawing_view(ifc, drawing_tool, drawing):
if not camera:
camera = drawing_tool.import_drawing(drawing)
drawing_tool.import_annotations_in_group(drawing_tool.get_drawing_group(drawing))
drawing_tool.activate_view(camera)
drawing_tool.activate_drawing(camera)
+6 -5
View File
@@ -106,6 +106,7 @@ def switch_representation(
data = existing_data
geometry.change_object_data(obj, data, is_global=is_global)
geometry.record_object_materials(obj)
if should_reload and existing_data:
geometry.delete_data(existing_data)
@@ -123,20 +124,20 @@ def remove_representation(ifc, geometry, obj=None, representation=None):
otherwise it will replace object with empty."""
element = ifc.get_entity(obj)
type = geometry.get_element_type(element)
element_type = geometry.get_element_type(element)
data = None
if type and (geometry.is_mapped_representation(representation) or geometry.is_type_product(element)):
if element_type and (geometry.is_mapped_representation(representation) or geometry.is_type_product(element)):
representation = geometry.resolve_mapped_representation(representation)
data = geometry.get_representation_data(representation)
if data and geometry.has_data_users(data):
for element in geometry.get_elements_of_type(type):
for element in geometry.get_elements_of_type(element_type):
obj = ifc.get_object(element)
if obj:
obj = geometry.replace_object_with_empty(obj)
obj = ifc.get_object(type)
obj = ifc.get_object(element_type)
if obj:
obj = geometry.replace_object_with_empty(obj)
ifc.run("geometry.unassign_representation", product=type, representation=representation)
ifc.run("geometry.unassign_representation", product=element_type, representation=representation)
ifc.run("geometry.remove_representation", representation=representation)
else:
data = geometry.get_representation_data(representation)
+62
View File
@@ -0,0 +1,62 @@
def create_repo(ifcgit, ifc):
path_ifc = ifc.get_path()
path_dir = ifcgit.get_path_dir(path_ifc)
ifcgit.init_repo(path_dir)
def add_file(ifcgit, ifc):
path_ifc = ifc.get_path()
repo = ifcgit.repo_from_path(path_ifc)
ifcgit.add_file_to_repo(repo, path_ifc)
def discard_uncomitted(ifcgit, ifc):
path_ifc = ifc.get_path()
# NOTE this is calling the git binary in a subprocess
ifcgit.git_checkout(path_ifc)
ifcgit.load_project(path_ifc)
def commit_changes(ifcgit, ifc, repo, context):
path_ifc = ifc.get_path()
ifcgit.git_commit(path_ifc)
if repo.head.is_detached:
ifcgit.create_new_branch()
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)
def colourise_revision(ifcgit, context):
step_ids = ifcgit.get_revisions_step_ids()
if not step_ids:
return
modified_shape_object_step_ids = ifcgit.get_modified_shape_object_step_ids(step_ids)
final_step_ids = ifcgit.update_step_ids(step_ids, modified_shape_object_step_ids)
ifcgit.colourise(final_step_ids)
def colourise_uncommitted(ifcgit, ifc, repo):
path_ifc = ifc.get_path()
step_ids = ifcgit.ifc_diff_ids(repo, None, "HEAD", path_ifc)
ifcgit.colourise(step_ids)
def switch_revision(ifcgit, ifc):
# FIXME bad things happen when switching to a revision that predates current project
path_ifc = ifc.get_path()
ifcgit.switch_to_revision_item()
ifcgit.load_project(path_ifc)
def merge_branch(ifcgit, ifc, operator):
path_ifc = ifc.get_path()
ifcgit.config_ifcmerge()
ifcgit.execute_merge(path_ifc, operator)

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