Compare commits

...

272 Commits

Author SHA1 Message Date
Dion Moult e072870fd3 Fix bcf pypi build 2024-09-01 13:35:57 +10:00
Dion Moult af4475fe46 Run black 2024-09-01 13:26:58 +10:00
Dion Moult eb3104367a Bump version to v0.8.0 2024-09-01 13:23:54 +10:00
Dion Moult 90f9ebd25e Add dev install instructins to official docs 2024-09-01 13:23:31 +10:00
Yassine Oualid f2554bc968 - Add button to generate cost schedule
- Feature to view, edit and add cost item values. ( Right Click - Edit ).
2024-09-01 01:40:53 +01:00
Dion Moult 53a78f38e5 You can now assign objects to the selected container in the spatial decomposition panel 2024-09-01 08:13:29 +10:00
Dion Moult b476cb825c Fix #5265. Accommodate invalid material sets. 2024-09-01 07:53:49 +10:00
Bruno Perdigão ec21574be5 Fix measure tool to now show the angle for the first point. 2024-08-31 14:39:58 -03:00
Bruno Perdigão b552665dd9 Round angle in polytool input numbers calculations. 2024-08-31 14:39:58 -03:00
Bruno Perdigão 48b630696f Fix 3ceeaa7 2024-08-31 14:39:58 -03:00
Bruno Perdigão 8c94ef6e54 A better fix for dabb85c. 2024-08-31 14:39:58 -03:00
Bruno Perdigão bdcadfd100 Refactor tool.Cad.angle_3_vectors
This functions was built to work with the input calculations for the polytool.
Part of its code was being duplicated in another part of the code. The refactor
keeps most of the calculation in the same place.
2024-08-31 14:39:58 -03:00
Bruno Perdigão 90e83d77a7 more improvement on distance and angle calculations 2024-08-31 14:39:58 -03:00
Bruno Perdigão 2e80363bac improved general input calculations 2024-08-31 14:39:58 -03:00
Bruno Perdigão b6f80eabb4 Fix input calculation and conversion for measure tool 2024-08-31 14:39:58 -03:00
Bruno Perdigão c93e52089b Fix input calculation and conversion for wall polytool 2024-08-31 14:39:58 -03:00
Andrej730 76ba3947f5 Expose header properties with invalid values #5246 2024-08-31 14:31:41 +02:00
Dion Moult 50cb09aac2 See #3880. Freestyle support for perspective vector linework. 2024-08-31 20:56:10 +10:00
Andrej730 dac1aaa7f3 Errors adding roof, stair, railing types #5261
It wasn't considering that type object can be hidden and can't be active.
Removed obj property since it can be covered with just temp_override.
2024-08-31 13:30:12 +05:00
Dion Moult eed603bbcd Bump IOS 2024-08-31 18:09:54 +10:00
Dion Moult 1d62f6f736 Support experimental freestyle linework generation 2024-08-31 18:09:14 +10:00
Andrej730 799cf47316 Fix error removing a cost schedule (after 7a44f9e) #5264
It wasn't really leaving orphaned data and interrupting the process of the ifc object deletion but it was leading to confusing errors
2024-08-31 13:00:11 +05:00
Dion Moult 03935a91da Fix bug where linework merging may create polygons where there should be interior holes instead. 2024-08-30 23:53:02 +10:00
Dion Moult 37baa9235a Fix bug where a voided element by OCC will be considered non-manifold. Remove doubles with a very small distance fixes it. 2024-08-30 23:51:47 +10:00
Dion Moult b4b2b60935 New bisect-based drawing cut mode. This replaces OCC's cut (but keeps the projection HLR) with a blender bisect based cut 2024-08-30 20:33:09 +10:00
Dion Moult a11c7535d8 Turn fill modes (shapely / svgfill) into a dropdown to prevent selecting both and crashing 2024-08-30 20:33:09 +10:00
Andrej730 e8d6dd333c Fix failing import if 'sun position' extension (9633d92)
ping @chiragsingh1711
2024-08-30 12:15:00 +05:00
Chirag Singh 9633d92c17 Light/radiance (#5252)
* Cam fix, Sun Fix, import-export as json

* Ran black formatting
2024-08-30 08:32:45 +10:00
Andrej730 afd5a5b23e Fix get_info_2 #5248 after d80bcd1 2024-08-29 18:57:47 +05:00
Dion Moult 15481c0a59 Bump docs to use spatial panel. Format menuselection. 2024-08-29 23:30:53 +10:00
Dion Moult 32b37b8823 Implement recursive toggling for elements in spatial panel 2024-08-29 23:30:53 +10:00
Andrej730 f3ab214752 Use first layer/profile from the set to pick up the material #5238
As currently we do not support multilayer/profile geometry and therefore we can't correctly represent them when they have multiple styles. As a workaround we'll use the material/style from the first layer and apply it to the entire object.
2024-08-29 17:21:21 +05:00
Dion Moult 4a59b30f1b Fix #5244. Prioritise collection of drawing groups over containment groups 2024-08-29 18:42:42 +10:00
Andrej730 87d6960356 Fix #5249 after acf135a 2024-08-29 13:42:08 +05:00
Andrej730 aff893e690 bim.create_drawing - poll message 2024-08-29 13:42:08 +05:00
Dion Moult 486eb259ea Fix #5245. Consider situation where object may not be loaded due to partial loading. 2024-08-29 18:09:31 +10:00
Ziad-I 8164b496f8 fix #5219 underlay links are now base64 encoded in the svg before sending it to web UI
this is not really the best solution and might need to look for better ones
2024-08-29 10:50:50 +03:00
Dion Moult fda23f350a Fix #5247. Fix typo when opening drawing web UI. 2024-08-29 17:19:28 +10:00
Dion Moult 711c597065 See #5231. Workaround for missing offset. 2024-08-29 16:22:42 +10:00
Dion Moult 80a396dc12 You can now select geometry library in advanced settings 2024-08-29 16:22:42 +10:00
Dion Moult 2b70f12d44 More bonsai name fixes 2024-08-29 15:17:52 +10:00
Dion Moult c3bcc2d90c See #5243. types_with_super() is no longer available. Also maybe overkill in this scenario. 2024-08-29 10:11:01 +10:00
Dion Moult 88e5a51f01 Merge pull request #5236 from IfcOpenShell/webui_updates
Add RMB context menu to web UI spreadsheets and small updates
2024-08-29 09:55:55 +10:00
Dion Moult 8b8af680bc Merge branch 'v0.8.0' into webui_updates 2024-08-29 09:55:41 +10:00
Dion Moult 8a57a52949 Bump IOS 2024-08-29 09:20:32 +10:00
Jean-Marc Couffin 720eda51c3 Update README.md with new blenderBMI branding - BONSAI 2024-08-29 08:35:24 +10:00
Bruno Perdigão 01a701007e Fix b74811b3f 2024-08-28 19:02:06 -03:00
Bruno Perdigão ec5368224f Fix after fbcb19fdb 2024-08-28 18:58:11 -03:00
Bruno Perdigão 56e9488d3e A few updates on Snap and Raycast to improve the snap feel. 2024-08-28 16:52:35 -03:00
Bruno Perdigão 23ec2fc48e Adds intersect_edges_v2 to Cad tools.
This function uses another method to calculate the closest points in two edges that are not parallel.
The `intersect_line_line` from mathutils were not getting good results when using orthogonal view.
2024-08-28 16:52:35 -03:00
Bruno Perdigão fbcb19fdbb Improved snap "stickiness".
Added more control on how the snapping system selects the snapping point, and "M" key now updates the input panel.

Added `intersect_edge_2` to Cad tool

Revert "Improved snap "stickiness"."

This reverts commit 85e67d3996ab8002b4cef7c0b859e9da251d39bf.

A few snap and raycast changes to improve the snap feel
2024-08-28 16:52:35 -03:00
Andrej730 d9f3c9d012 bim.activate_model to hide all annotations #5232 2024-08-28 23:54:17 +05:00
Yassine Oualid 91e236c42c Display and Load Work Schedules from webUI 2024-08-28 18:57:40 +01:00
Yassine Oualid 03ab88b5bb First Draft Cost Schedule Web UI:
- display cost schedules
	- load cost items
	- Add cost items
	- Edit cost item names
2024-08-28 18:54:18 +01:00
Andrej730 42ef06aae1 typing 2024-08-28 18:47:23 +05:00
Andrej730 691dfb45f0 bim.activate_model to unhide all objects #5232
Restore old behaviour (new behaviour was introduced by accident in 786b796) where it would unhide all objects instead of keeping only objects in the current drawing.
2024-08-28 18:42:10 +05:00
Andrej730 760816b7f0 Show occurrences in spatial manager decomposed elements
Example - https://imgur.com/a/PNFUbv0
2024-08-28 17:57:19 +05:00
Andrej730 fc04c6e41e Remove unused property 2024-08-28 17:53:13 +05:00
Andrej730 99e67d528f Remove duplicated then decorator from b9855b2
'then the object name is selected' were already defined in then_the_object_name_is_selected
2024-08-28 16:42:55 +05:00
Andrej730 c113061742 Preserve visibility status for non-ifc objects when drawing is activated
Mentioned in #3402
2024-08-28 16:37:26 +05:00
Andrej730 174ff00a6d Fix errors trying to setup decorations running tests 2024-08-28 16:37:26 +05:00
Andrej730 84a2d4266d Autodocument hotkeys in the BIM tool hotkeys descriptions
Addition to #5223

Example - https://i.imgur.com/vGVHNPL.png
2024-08-28 15:41:50 +05:00
Andrej730 64cba98c3d Fix #5235 2024-08-28 14:37:58 +05:00
Andrej730 1aed2a6b3c Fix error not excluding annotations from other drawings after 3848a21
Mentioned in #5233

Also some refactor:
- checking IfcProduct instead of IfcProject. Not sure when this issue occur in general but if it occurs then it might fail for other non-IfcProducts too, not just IfcProject
- small performance optimization
2024-08-28 12:35:47 +05:00
Andrej730 4e1a956830 Fix #5233 2024-08-28 12:35:47 +05:00
Andrej730 747a39b440 48f0dc6a00 to pass on unix too 2024-08-28 11:29:39 +05:00
Andrej730 1b49515d9f Replace ordered_set with orderly_set
deepdiff deprecated use of ordered_set and switched to orderly_set (ordered_set fork), so we switch too
see https://github.com/seperman/deepdiff/releases/tag/8.0.0
2024-08-28 11:14:52 +05:00
Bruno Perdigão dabb85c466 Fix polytool input for distance zero 2024-08-27 19:59:55 -03:00
Bruno Perdigão b74811b3f4 Polytool, changed decorator order to show created polyline first. 2024-08-27 19:53:03 -03:00
Bruno Perdigão 79eaf895f7 Fix polyline to work only with walls.
This is temporary until we enable polyline to work with other tools.
2024-08-27 19:05:30 -03:00
Bruno Perdigão 994c85a05a black format 2024-08-27 18:22:43 -03:00
Bruno Perdigão 0faacccf6d Raycast now filters objects that are not in the active local view. 2024-08-27 18:20:06 -03:00
Bruno Perdigão 89d27bdf5f Fixed edge snap.
The `intersect_line_line` were returning points that were outside
the object edges, so it was added a function to check if the point
return is on the object edge.
2024-08-27 18:20:06 -03:00
Bruno Perdigão 76853db32c Fix mixed snap for Polytool 2024-08-27 18:20:06 -03:00
Andrej730 002c50e29d black format 2024-08-27 18:25:30 +05:00
Andrej730 d2efb2ab78 operator to toggle container elements
Example - https://imgur.com/a/Sy83Tnz
2024-08-27 18:25:30 +05:00
Andrej730 8bb78959ef h5 cache - improve logs
Now there are logs when cache was successfully loaded/created/failed to load.
2024-08-27 18:25:30 +05:00
Andrej730 48f0dc6a00 bim.purge_hdf5_cache to skip currently loaded cache
Previously it would fail with:
PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'bonsai\\bim\\data\\cache\\988c190483ae6bab7cf6b00cee78694d.h5'
2024-08-27 18:25:30 +05:00
Andrej730 27aee31adf typing 2024-08-27 18:25:29 +05:00
Bruno Perdigão 24aaefb927 Polytool, fixed error with setting the plane method 2024-08-27 10:03:02 -03:00
Bruno Perdigão 7daec25454 Polytool, fixed decorator to show the mouse snapping point on top. 2024-08-27 10:03:02 -03:00
Andrej730 8309b13db0 reveal element filters and drawing underlay panels just if drawing is active
Those two panels were requiring drawing's camera to be active object which was a bit confusing since "Active Drawing" section above worked fine without selecting camera explicitly.

https://i.imgur.com/wcmA73Z.png
2024-08-27 17:55:44 +05:00
Andrej730 5e95985d19 Fix error setting integer properties in bim tests 2024-08-27 11:33:58 +05:00
Andrej730 fefe325ece Fix workflow for tests requiring Sun Position extension 2024-08-27 11:33:21 +05:00
Bruno Perdigão b717abf639 Polytool: small fix for Z axis line decorator 2024-08-26 17:11:20 -03:00
Bruno Perdigão 32ae12b45a Polytool: added on screen snap information. 2024-08-26 17:11:19 -03:00
Bruno Perdigão 48b74ebb76 Polytool: creates a decorator to show instructions on the screen. 2024-08-26 17:11:19 -03:00
Bruno Perdigão 197d0b3258 Fixed issue with X, Y and Z input for polytool.
This issue was caused by the change to X, Y, Z keys for lock axis.
2024-08-26 17:11:19 -03:00
Bruno Perdigão 2a1bc30f19 Polytool improved axis lock and mixed snapping
You can now lock into an axis while snapping to an object.
The result will be to closer point. In the future, there should
be a way for the user to select between different points.
2024-08-26 17:11:19 -03:00
Bruno Perdigão 6dcbd98769 Fixed error for measure tool when working with top or side view. 2024-08-26 17:11:19 -03:00
Bruno Perdigão 1be025ed86 Fixed typo 2024-08-26 17:11:19 -03:00
Bruno Perdigão 6fa87a16ab Polytool refactor for selecting plane and axis snap.
User can now select X, Y, Z to lock to an axis, and
S-X, S-Y and S-Z to lock to a plane.
2024-08-26 17:11:19 -03:00
Ziad-I 8c19b695ea fix drawings and sheets paths not being normalized
which lead to the paths sometimes having both / and \\
2024-08-26 20:07:50 +03:00
Ziad-I 5831c56380 remove left over print :) 2024-08-26 16:29:09 +03:00
Andrej730 3dde1b80b9 Fix errors initializing hdf5 cache #4832 2024-08-26 17:33:23 +05:00
Andrej730 08dd8f87ff Operator to select document related objects
https://i.imgur.com/4gXAram.png
2024-08-26 17:15:44 +05:00
Andrej730 1c3cf44122 get_referenced_elements to support IfcExternalInformation 2024-08-26 17:15:44 +05:00
Andrej730 868454c011 delete brick.ttl (added in d1d9db3) 2024-08-26 17:15:44 +05:00
Andrej730 edcfcd8850 Fix error if user hit shift-a in Annotation Tool without any drawing active 2024-08-26 15:47:00 +05:00
Andrej730 33bbf9fc53 remove debug print in collector.assign 2024-08-26 15:47:00 +05:00
Andrej730 286e90e299 Add annotation by default to the active drawing #5216
Approach in 358c834 didn't worked out because annotations are assigned to the drawing after assign_class and we can't check in assign_class whether it's going to be part of the drawing.
2024-08-26 15:46:59 +05:00
Bruno Perdigão 5aa7758795 Small fix in angle input for polytool 2024-08-25 21:36:11 -03:00
Bruno Perdigão 6c47baf0de Small fix in polytool with angle input when working with millimeters units 2024-08-25 19:13:27 -03:00
Bruno Perdigão 51018177cd Added option for wall_from_to_points not round the length and angle.
This function was hard coded to round length angle, now it has an option.
I assume it was originally built like this because it was supposed to
work only with Grease Pencil.
2024-08-25 19:07:50 -03:00
Bruno Perdigão be9719f5ca Fixed issue with creating duplicated points in polytool 2024-08-25 18:53:53 -03:00
Bruno Perdigão 9193ff7252 Fixed error with raycast in orthogonal view 2024-08-25 18:42:00 -03:00
Bruno Perdigão 53c3019b73 Small fix 2024-08-25 18:36:20 -03:00
Bruno Perdigão 6bbf3d3917 Refactor snapping functions and allow to choose snapping point.
By pressing "M", the user can now cycle through active snapping points when
there are many options close to each other. Still lacks a text to give
information about the active point.
2024-08-25 18:27:10 -03:00
Ziad-I 47a15011f6 add button to drawings panel to open documentation web ui page
might need a different icon since the URL icon is used by the open_drawing button
2024-08-25 21:38:15 +03:00
Ziad-I f26f6dfe5e fix broken display for multiple spreadsheets due to absolute position
removed absolute positioning as it made spreadsheets on top of each other
and made the spreadsheet's width and height as full screen as possible. also, add a separating margin between two spreadsheets.
2024-08-25 20:30:20 +03:00
Ziad-I 1745a4fd39 fix theme data not being sent when all Bonsai's are disconnected 2024-08-25 17:50:26 +03:00
Dion Moult fcaac3787b Continuing work to update docs 2024-08-25 22:15:12 +10:00
Thomas Krijnen 2d3adce59e Compatibility fix for v0.8 wrapper instances 2024-08-25 10:51:10 +02:00
Thomas Krijnen 7e6607adcf black 2024-08-25 10:39:57 +02:00
Thomas Krijnen 46c62d7cdb Add no-parallel-mapping setting to type literal 2024-08-25 10:33:54 +02:00
Thomas Krijnen c521892ca2 ::impl to make msvc17 a bit happier 2024-08-25 09:54:44 +02:00
Dion Moult d4a0dd1527 Split header and panel tool UI into two functions. Align related tool buttons together. 2024-08-25 15:43:55 +10:00
Dion Moult 527cdd3304 General interface docs 2024-08-25 14:55:21 +10:00
tim 31e836cfb4 png icons added 2024-08-25 14:54:30 +10:00
tim 86f123b6b0 Icons to header & sidebar 2024-08-25 14:54:30 +10:00
Thomas Krijnen fea8e3a5c9 Perform similar face bound lenciency in cgal kernel 2024-08-24 20:03:44 +02:00
Thomas Krijnen 3137904055 Perform mapping in parallel (or --no-parallel-mapping) 2024-08-24 20:03:44 +02:00
Bruno Perdigão f89dee7668 Fixed angle input for imperial units in polyline wall and measure tool. 2024-08-24 15:02:22 -03:00
Ziad-I c042d1afe5 make container padding/margin consistent over all pages 2024-08-24 18:48:58 +03:00
Ziad-I 42fc72bd4f add favicon to web UI 2024-08-24 18:12:13 +03:00
Bruno Perdigão f72c128ba6 Improved validation for inputs in polyline wall and measure tool. 2024-08-24 11:48:39 -03:00
Bruno Perdigão 3ceeaa739f Small fix on format_distance to show zero like 0'. 2024-08-24 11:48:39 -03:00
Thomas Krijnen 7a2e58944e clang (rightfully) complains about missing include 2024-08-24 15:00:49 +02:00
Dion Moult b9855b289f More qto and spatial tests. Tests can now select items in a UIList. 2024-08-24 22:48:37 +10:00
Dion Moult c7a8fd9309 More Qto tests and you can now spy on UILists and enum items 2024-08-24 21:39:46 +10:00
Thomas Krijnen daa6b31c5d msvc17 workaround 2024-08-24 13:24:23 +02:00
Dion Moult cd411e1f14 Add smoke tests for colour by property feature 2024-08-24 14:20:13 +10:00
Dion Moult 7a168069f7 Add solar analysis panel UI tests 2024-08-24 13:34:40 +10:00
Bruno Perdigão 97d2e08286 Initial implementation of unit handling for wall polyline tool.
The input panel now works with meters, millimeters and imperial units.
2024-08-23 17:33:34 -03:00
Thomas Krijnen f440a3ca17 Revert OCCT version to 7.5.3 due to HLR poly/edge behaviour 2024-08-23 20:54:06 +02:00
Thomas Krijnen 81bdb551ab SvgSerializer fix: Z == 2 2024-08-23 20:43:27 +02:00
Thomas Krijnen 5fdad744d3 Cgal kernel: Fail boolean op altogether when part conversion to Nef fails 2024-08-23 20:42:32 +02:00
Thomas Krijnen ab6f408db4 Unify edge orientation logic among kernels 2024-08-23 20:41:42 +02:00
Thomas Krijnen cdf8b9c038 Cgal kernel: taxonomy::line is intended to run along local Z 2024-08-23 20:41:17 +02:00
Thomas Krijnen 918fb41a7e Unify face bound counting logic among kernels 2024-08-23 20:39:20 +02:00
Thomas Krijnen 1d8e39e55f NB! For now let's use Epick for cgal-simple instead of actually Simple_cartesian 2024-08-23 20:38:38 +02:00
Thomas Krijnen b0b65e550c dispatch_curve_creation based on template magic instead of dynamic casting 2024-08-23 20:36:47 +02:00
Thomas Krijnen a1527a7e31 --use-material-names no longer in serializer_settings 2024-08-23 20:35:01 +02:00
Ziad-I 2ca3f86fc9 add RMB context menu to tables to set top calc type 2024-08-23 21:30:21 +03:00
Thomas Krijnen d80bcd1a94 Unify variant storage (#5118) 2024-08-23 20:29:07 +02:00
Ziad-I 60a06138d2 edit index.css for color consistency 2024-08-23 19:34:15 +03:00
Andrej730 e2001e82dd fix (hopefully) last failing test in github workflow 2024-08-23 17:25:10 +05:00
Andrej730 92f4cb0ffa fix error reloading camera representation
After 4b4ede7 it was leading to non-existing code
2024-08-23 17:25:10 +05:00
Andrej730 9f00e8aef0 Use Point RepresentationType for point clouds in ifc4x3
PointCloud representation type marked as deprecated in ifc4x3, see https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcShapeRepresentation.htm
2024-08-23 17:25:10 +05:00
Andrej730 638d200a27 fix error switching to point cloud representation
Previously it wasn't handled and would always result in errors. It seems the only way to load point cloud representation to BBIM was by loading IFC file and only if this is the only representation of the element.
2024-08-23 17:25:10 +05:00
Andrej730 59717d6e92 fix tests after a4a2497f33 2024-08-23 17:25:10 +05:00
Andrej730 009795f7dc More reliable way to get bbim package name #5214
See #5214 for details, can't rely on 'bpy.context.preferences.addons' as it may contain a missing addons too (if addon was manually uninstalled and it won't be able to load but it will still appear in 'addons').
2024-08-23 17:25:10 +05:00
Andrej730 a0792a4b93 typing 2024-08-23 17:25:09 +05:00
Ziad-I 8534b6e154 fix first demo message not showing 2024-08-23 19:34:35 +10:00
Ziad-I 7910a68028 add more details in demo page 2024-08-23 19:34:35 +10:00
Ziad-I d44ee9874b Bonsai web ui templates 2024-08-23 19:34:35 +10:00
Ziad-I 8cd86581ff handle operators from demo page 2024-08-23 19:34:35 +10:00
Ziad-I 0a5b6a41b0 add demo html/css/js 2024-08-23 19:34:35 +10:00
Ziad-I 7bac468151 add demo endpoint and event listener 2024-08-23 19:34:35 +10:00
Ziad-I b375579dcd add new webui demo panel in demo module 2024-08-23 19:34:35 +10:00
Dion Moult d3d1ca0169 Fix bug where select similar property didn't work 2024-08-23 15:29:19 +10:00
Dion Moult 2b0572af10 New feature to filter pset / qto quickly in the panel
Ever work regularly with models that have 30 or more properties per element?
2024-08-23 15:20:47 +10:00
Dion Moult ed6365b292 You can now filter spatial containers by name, long name, or class 2024-08-23 14:27:22 +10:00
Dion Moult e3b1e0773f Show grouping panel by default 2024-08-23 14:20:37 +10:00
Dion Moult 3d38068b90 Unindent project in spatial tree to save space 2024-08-23 14:20:32 +10:00
Dion Moult a4a2497f33 You can now filter and search in the spatial decomposition panel 2024-08-23 13:49:45 +10:00
Dion Moult 1ef2fb89af Show annotations and hide openings in spatial decomposition element filter 2024-08-23 13:48:19 +10:00
Bruno Perdigão 902d777574 Fix removing the dimension decorator in measure tool. 2024-08-22 17:44:48 -03:00
Bruno Perdigão 3bd0caade1 Fixed Polyline Wall after changes in Measure tool input 2024-08-22 16:11:22 -03:00
Bruno Perdigão 301cb4bbdc Small fix in TAB behavior for measure and polyline tool 2024-08-22 15:55:05 -03:00
Bruno Perdigão f0338ffdda Measure tools now shows decorator for the dimension of every line and the angles between them. Area calculation was removed and will be introduced in another operator 2024-08-22 15:54:11 -03:00
Dion Moult 2100528cbe Rebrand to Bonsai in IOS-Python docs 2024-08-23 00:12:11 +10:00
Dion Moult ae4f681f73 Remove superseded test 2024-08-22 23:58:15 +10:00
Dion Moult 53d7624fc4 Work in progress starting to restructure docs in preparation for next release 2024-08-22 23:56:59 +10:00
Andrej730 951b2b76a6 small fix for 94f5137684 2024-08-22 18:34:13 +05:00
Andrej730 e9b4b6560e drawing.create_camera not to assign camera to the scene collection
Noticed that added drawing camera was linked to the scene collection and after collector.assign it was linked to the 2 collections instead of just 1 - it's own drawing collection and the scene collection.
2024-08-22 18:34:13 +05:00
Andrej730 cabffdc549 fix tests after 4e48c35425 2024-08-22 18:33:21 +05:00
Andrej730 7ea60742d6 fix typo 2024-08-22 18:28:50 +05:00
Andrej730 358c834427 fix bim tests after 143e094
drawings and annotations that are part of some drawing don't need to be added to the spatial hierarchy
2024-08-22 18:28:50 +05:00
Andrej730 5d6655cbc7 fix issue adding drawing representation after ef04402ed8 2024-08-22 18:28:50 +05:00
Andrej730 4d122d77dc load_from_memory prop - add description 2024-08-22 17:20:48 +05:00
Dion Moult d4e863ed3b Replace BlenderBIM branding with Bonsai in docs 2024-08-22 21:02:19 +10:00
Dion Moult b6ccf505e7 Fix #5177. Refresh georeferencing viz data after data purged. 2024-08-22 20:18:13 +10:00
Dion Moult ce9842983b Fix #5037. Remove deprecated pie menu. 2024-08-22 19:59:55 +10:00
Dion Moult eb19b26a6e Revert xlsx feature, just csv for now
Don't want another dep
2024-08-22 17:24:43 +10:00
Dion Moult 06602d0e39 Download CSV/XLSX from web 2024-08-22 17:22:38 +10:00
Dion Moult 00279b63de Show sum of columns on web spreadsheet, really useful for qto 2024-08-22 17:22:29 +10:00
Dion Moult 6418353119 You can now filter headers in web spreadsheets 2024-08-22 17:22:09 +10:00
Dion Moult 3afbbe51d2 Make spreadsheets fullscreen
Spreadsheets from multiple sessions need to be redesigned to perhaps use tabs or split windows or something else
2024-08-22 17:21:48 +10:00
Dion Moult ddb767674b Fix bug where there would be missing data in web spreadsheets
See https://github.com/olifolkerd/tabulator/issues/3135
2024-08-22 15:37:04 +10:00
Bruno Perdigão 5575ea8296 Polyline and Measure tool now use right-click to confirm input 2024-08-21 23:01:06 -03:00
Bruno Perdigão e8a7a032aa Changed the order of the input panel for Polyline and Measure tool. Distance and Angle now come first. 2024-08-21 22:56:12 -03:00
Bruno Perdigão f23e419685 Fixed bug in Polyline and Measure tool. The input calculation was wrong when you had only one point in the polyline 2024-08-21 22:56:12 -03:00
Bruno Perdigão b87e8136af Raycast and Snap refactor. Snap now works with objects with no faces 2024-08-21 22:56:12 -03:00
Bruno Postle 473d0ae888 Remove Association op cleans blenderbim & bonsai
The remove *.ifc Association removes any stale Linux BlenderBIM
associations as well as Bonsai associations, this should now give a
clean upgrade path. Closes #5212
2024-08-21 21:11:11 +01:00
Andrej730 94f5137684 fix poll error messages addressing empty enum 2024-08-21 18:36:12 +05:00
Andrej730 4e75945435 add surface styles preview (based on their blender materials)
Example - https://i.imgur.com/jH9q7iF.png
2024-08-21 18:36:11 +05:00
Andrej730 e31019eb67 fix issue adding bcf topic viewpoints
missed the set statement which is requirement to make it work for all cases
2024-08-21 18:36:11 +05:00
Andrej730 c505630a20 fix error removing bcf topic comment 2024-08-21 18:36:11 +05:00
Andrej730 860fa81f73 bonsai bcf - activate comment's viewpoint
Example - https://imgur.com/a/iUMgpVm
2024-08-21 18:36:11 +05:00
Andrej730 847b5d226e bim.add_bcf_comment - poll message 2024-08-21 18:36:11 +05:00
Andrej730 ad77d09916 fix bug in bonsai bcf v3 api
it wasn't setting reference links / document references / labels if Topic container for them wasn't created before
2024-08-21 18:36:11 +05:00
Andrej730 9b12cae8b4 bcf bonsai - create bcf v3 projects and display loaded bcf version
Example - https://imgur.com/a/3ZSGtw7
2024-08-21 18:36:11 +05:00
Andrej730 84a8c5dfeb bim.convert_to_blender to ignore linked files 2024-08-21 18:36:11 +05:00
Andrej730 12a7b8a79e bim.convert_to_blender to avoid types without mesh properties 2024-08-21 18:36:11 +05:00
Andrej730 ef04402ed8 Fix Image empty interpreted as Mesh #5194
Apparently we can't trust `obj.data == None` to identify empties since in Blender there are empties that can use `bpy.types.Image` as their `obj.data`.
2024-08-21 18:36:10 +05:00
Andrej730 45fed747c2 fix failing test in model module
it was failing because type object was hidden in viewport and was unselectable
2024-08-21 18:36:10 +05:00
Dion Moult 85229fc865 Fix #5204. Bug where you could attempt to add IFC objects in edit mode which isn't allowed. 2024-08-21 22:22:46 +10:00
tim 916f854905 Update ifccsv.rst
BlenderBIM to Bonsai changes
2024-08-21 22:06:08 +10:00
tim 1915541d20 Update ifcconvert.rst
Changed BlenderBIM Add-on to Bonsai
2024-08-21 22:05:48 +10:00
tim e37b84a95a Update introduction.rst
Replaced BlenderBIM with Bonsai
2024-08-21 22:05:34 +10:00
tim bcb2a37ce0 Removed Linux defaults to not confuse mac users 2024-08-21 22:05:14 +10:00
tim 018d6847a0 Update blenderbim.rst
replaced BlenderBim text with Bonsai
2024-08-21 22:04:44 +10:00
Dion Moult 35918265d0 Fix bug on Linux where INBETWEEN_MOUSEMOVE is fired for measure tool 2024-08-21 21:47:41 +10:00
Dion Moult 4e48c35425 Remove old dumb stair, superseded by new clever stair (now just called stair) 2024-08-21 21:19:27 +10:00
Dion Moult d985fe9240 Fix #5205. Regression in creating walls / slabs / profiles / products with potentially no default container. 2024-08-21 21:09:29 +10:00
Dion Moult 7c00021aaa You can now toggle flat shading when showing colourschemes 2024-08-21 20:55:35 +10:00
Andrej730 d2dc8b519f fix for ef835e43b8 2024-08-21 15:14:15 +05:00
Andrej730 25fe025354 bonsai file association files #5212 2024-08-21 15:13:59 +05:00
Andrej730 1aae710286 Restore file association files removed by accident #5212 2024-08-21 15:13:59 +05:00
Andrej730 38e8130793 stop ignoring src/bonsai/bonsai/libs
After #4373 with new folder structure 'libs' is no longer used to store dependencies and there is no need to ignore it.
2024-08-21 15:13:58 +05:00
Dion Moult 7e94388074 You can select a colourscheme key via search / dropdown now rather than typing a query 2024-08-21 18:50:55 +10:00
Andrej730 ef835e43b8 bim.save_uv_to_style - support saving uv to objects without a style
There was a problem with assigning uv workflow if object wasn't originally from IFC - saving uv requires object to have IfcSurfaceStyle but assigning IfcSurfaceStyle would reload representation discarding UV.

So, the current workflow for saving non-ifc objects uv to ifc:
1) Assign class to ifc object
2) Create IfcSurfaceStyle with a texture (currently also requires a rendering style)
3) Use "Save UV To Style"

Example - https://imgur.com/a/GZqo38M
2024-08-21 12:33:25 +05:00
Andrej730 e5d0eb13bf Show in UI if representation item has UV mapping / colour map
Example - https://i.imgur.com/bWV4dRj.png
2024-08-21 12:33:25 +05:00
Andrej730 4fcf23319f Show in UI an operator for saving UV
It was lost when we deprecated style UI in materials tab.
Button location - https://i.imgur.com/V57MKU2.png

Also added some minor tweaks.
2024-08-21 12:33:25 +05:00
Dion Moult 99154afdb1 You can now specify custom min/max for quantitative legends 2024-08-21 16:34:10 +10:00
Dion Moult e83eb68fe1 Add seaborn colour palletes for tab10, paired, rocket, mako, spectral, and coolwarm
This covers three usecases: qualitative, quantitative min->max, and quantitative middle with low/high
2024-08-21 15:27:21 +10:00
Dion Moult e4a4a33920 Add support for colouring by quantitative colour legend, not just qualitative categories 2024-08-21 14:02:41 +10:00
Dion Moult e8848c9701 Natsort is now a dependency. We'll be using it to do better sorting everywhere. 2024-08-21 14:01:32 +10:00
Bruno Perdigão cfd8044cb3 Fixed bug related to pressing shift for plane selection 2024-08-20 17:47:02 -03:00
Bruno Perdigão 2d7866570e Fixed bug for x, y, z calculation 2024-08-20 17:47:02 -03:00
Bruno Perdigão eb0ed7fe6b Fixed bugs related to shift and added Z as input option 2024-08-20 17:47:02 -03:00
Bruno Perdigão 67733c4449 Improved calculation for coordinates from distance and angle 2024-08-20 17:47:02 -03:00
Bruno Perdigão b3940cdc45 Minor refactor and clean up 2024-08-20 17:47:02 -03:00
Bruno Perdigão 94e75aebb0 Added area calculation to the input panel 2024-08-20 17:47:02 -03:00
Bruno Perdigão ff203b2a13 Changed decorator name to PolylineDecorator 2024-08-20 17:47:02 -03:00
Bruno Perdigão 10616df494 black format 2024-08-20 17:47:02 -03:00
Bruno Perdigão 1dc6d4935e Polyline tool: improved plane selection method and decorators 2024-08-20 17:47:02 -03:00
Bruno Perdigão 04088af2b6 small fix 2024-08-20 17:47:02 -03:00
Bruno Perdigão c477173096 Improved plane selection for Measure tool 2024-08-20 17:47:02 -03:00
Bruno Perdigão 2f4093f958 Fixed decorator for mixed snaping 2024-08-20 17:47:02 -03:00
Bruno Perdigão 95fc51da76 Created function to mix snap point with axis lock in polyline tool 2024-08-20 17:47:02 -03:00
Bruno Perdigão 50e629c02c Removed code after refactor 2024-08-20 17:47:02 -03:00
Bruno Perdigão 4a178fca58 Fixed error in polyline tool, when two points where add at the same location sequentially 2024-08-20 17:47:02 -03:00
Bruno Perdigão aff5b238d9 WIP: Added possibility to change plane intersection method 2024-08-20 17:47:02 -03:00
Bruno Perdigão 315b6060b0 WIP: Changed coordinates, distance and angles calculations to allow for 3d vectors 2024-08-20 17:47:02 -03:00
Bruno Perdigão b137197586 Changed input validantion to Snap tool 2024-08-20 17:47:02 -03:00
Bruno Perdigão d1d9db3ed8 WIP: started refactoring to develop the measure tool with most of the logic from polyline wall 2024-08-20 17:47:02 -03:00
Andrej730 736b50c6fa bonsai bcf - fix issue when viewpoints were not reloaded
when new bcf file was loaded
2024-08-20 18:39:09 +05:00
Andrej730 64b96633c2 bonsai bcf v3 - support working with viewpoints #2790
Also fixed a bug when removing viewpoint always removed the last viewpoint
2024-08-20 18:39:09 +05:00
Andrej730 8eae997053 bcf v3, bonsai bcf v3 - support extracting files #2790
extracting files in general is also improved - now it's possible to extract a file that was just added to bcf in memory and not yet saved to the disk.

Update model.py
2024-08-20 18:39:09 +05:00
Andrej730 fee56fc120 bonsai bcf - remove "bim.edit_bcf_author"
bcfxml doesn't actually have "author" field - this field is not stored in bcf, it's just needed as an UI for author for newly added comments and topics.
2024-08-20 18:39:09 +05:00
Andrej730 d1f87d5467 fix bug in 71ddc6384a 2024-08-20 18:39:09 +05:00
Andrej730 7539404c27 bcf v3 & bonsai bcf v3 - support adding/removing header files #2790
Also removed unnecessary read in AddBcfDocumentReference.
2024-08-20 18:39:08 +05:00
Andrej730 71ddc6384a bonsai bcf v3 - support adding/removing related topics #2790
Not entirely sure about TopicHandler.guid typing, just made it consistent with v2 for now.
2024-08-19 18:44:33 +05:00
Andrej730 4b402f8ea5 bonsai bcf - dropdown for related topics
Previously you had to type in exactly manually.
Besides the dropdown it should also show now a topic description in the dropdown items tooltips.
Removed "bim.add_bcf_related_topic" poll as it should be now covered by the enum.

Example - https://imgur.com/a/xhgcGWI
2024-08-19 18:44:33 +05:00
Andrej730 c39fa80e09 bonsai bcf v3 - support working with topics #2790 2024-08-19 18:44:33 +05:00
Andrej730 c89ad8c7da related topic - poll message set 2024-08-19 18:44:33 +05:00
Andrej730 6dfe46d9bd bonsai bcf - add default project for bcf v3, fix small possible issue with bcf v2
In theory in bcf v2 there could be a project with project info but without project.
2024-08-19 18:44:33 +05:00
Andrej730 1de51ecf77 bonsai bcf - fix bim snippet ui issue
schema is optional field in bcf v2 and it was unreliable to use it to detect whether bim snippet is present or not
2024-08-19 18:44:33 +05:00
Andrej730 8a55f18c95 bonsai bcf - poll message for adding bim snippet 2024-08-19 18:44:32 +05:00
Andrej730 7a1976cfc1 bonsai bcf - show info message when project is saved/loaded
to make ui more responsive
2024-08-19 18:44:32 +05:00
Andrej730 78c6401453 bonsai bcf v3 - support working with bim snippets 2024-08-19 18:44:32 +05:00
Andrej730 e12838d93e bonsai bcf v3 - support working with comments #2790 2024-08-19 18:44:32 +05:00
Andrej730 7aa5b71a6a bcf - comments setter api 2024-08-19 18:44:32 +05:00
Andrej730 2ef3af5e1b bonsai bcf - make document reference uneditable
as this data is not really editable and won't be saved
2024-08-19 18:44:32 +05:00
Andrej730 d399f7c6b7 bonsai bcf - add save current project button
Now there are two separate buttons - "save project" and "save project as".
Example - https://i.imgur.com/1vuVfgi.png
2024-08-19 18:44:32 +05:00
Andrej730 beb7f75368 bonsai - bcf v3 document references #2790 2024-08-19 18:44:32 +05:00
Andrej730 dc6fdc2a98 typing 2024-08-19 12:26:49 +05:00
Thomas Krijnen 45be17c25a Remove duplicate UseMaterialNames setting binding #5191 2024-08-19 09:15:49 +02:00
Dion Moult 4f11b19b3d Update install instructions for Bonsai 2024-08-18 22:22:21 +10:00
Ryan Schultz f646cf4372 Removes the style from all selected objects 2024-08-17 13:36:10 -05:00
542 changed files with 127293 additions and 145984 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@v2 # https://github.com/actions/checkout
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
+6
View File
@@ -142,6 +142,12 @@ jobs:
sverchok_zip="$(pwd)/dist/$(ls dist)"
blender --command extension install-file -r user_default $sverchok_zip
# Install Sun Position extension.
blender --online-mode --command extension sync
blender --online-mode --background --python-expr "import bpy; \
bpy.ops.extensions.package_install(repo_index=0, pkg_id='sun_position'); \
bpy.ops.preferences.addon_enable(module='bl_ext.blender_org.sun_position'); bpy.ops.wm.save_userpref()"
cd ../bonsai
pip install pytest-blender
blender --background --python scripts/setup_pytest.py
-3
View File
@@ -73,9 +73,6 @@ src/ifcopenshell-python/test/build
# mypy cache
.mypy_cache
# bonsai libs
src/bonsai/bonsai/libs
# bonsai i18n
src/bonsai/bonsai/translations.py
+2 -2
View File
@@ -12,7 +12,7 @@ is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending
is possible at compile-time when using C++ and at run-time when using Python.
In addition to a C++ and Python API, IfcOpenShell comes with an ecosystem of tools, notably including IfcConvert (an application
to convert IFC models to other formats), the BlenderBIM Add-on (an add-on to Blender providing a graphical IFC authoring platform),
to convert IFC models to other formats), Bonsai (an add-on to Blender providing a graphical IFC authoring platform),
and many other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF and IDS.
For more information, see:
@@ -37,7 +37,7 @@ Contents
| Name | Description | License | Service |
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) |
| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=blenderbim-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) |
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) |
+1 -1
View File
@@ -1 +1 @@
0.7.11
0.8.0
+1 -1
View File
@@ -81,7 +81,7 @@ ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1"]
JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.7.1"
OCCT_VERSION = "7.5.3"
BOOST_VERSION = "1.80.0"
PCRE_VERSION = "8.41"
LIBXML2_VERSION = "2.9.11"
+10
View File
@@ -0,0 +1,10 @@
import bcf.v2.model
import bcf.v3.model
from typing import Union
BimSnippet = Union[bcf.v2.model.BimSnippet, bcf.v3.model.BimSnippet]
BitMap = Union[bcf.v2.model.VisualizationInfoBitmap, bcf.v3.model.Bitmap]
DocumentReference = Union[bcf.v2.model.TopicDocumentReference, bcf.v3.model.DocumentReference]
HeaderFile = Union[bcf.v2.model.HeaderFile, bcf.v3.model.File]
Topic = Union[bcf.v2.model.Topic, bcf.v3.model.Topic]
ViewPoint = Union[bcf.v2.model.ViewPoint, bcf.v3.model.ViewPoint]
+97 -1
View File
@@ -1,5 +1,101 @@
import tempfile
import bcf.v2.bcfxml
import bcf.v2.model
import bcf.v2.topic
import bcf.v3.bcfxml
import bcf.v3.model
import bcf.v3.topic
from typing import Union
import bcf.agnostic.model as mdl
from pathlib import Path
from typing import Union, Optional
from typing_extensions import assert_never
TopicHandler = Union[bcf.v2.topic.TopicHandler, bcf.v3.topic.TopicHandler]
def extract_file(
topic: TopicHandler,
entity: Union[mdl.HeaderFile, mdl.BimSnippet, mdl.DocumentReference, mdl.BitMap],
bcfxml: Optional[Union[bcf.v2.bcfxml.BcfXml, bcf.v3.bcfxml.BcfXml]] = None,
outfile: Optional[Path] = None,
) -> Union[Path, str, None]:
"""Extracts an element with a file into a temporary directory
These include header files, bim snippets, document references, and
viewpoint bitmaps. External reference are not downloaded. Instead, the
URI reference is returned.
:param entity: The entity with a file reference to extract
:param outfile: If provided, save the header file to that location.
Otherwise, a temporary directory is created and the filename is
derived from the header's original filename.
:param bcfxml: The BCF XML file to use for resolving document references files.
Required only for BCF v3 document references (in BCF v3 internal documents
are stored at BCF root, not in the topic).
:return: The filepath of the extracted file. It may be a URL if the
header file is external.
"""
if isinstance(entity, mdl.DocumentReference):
if isinstance(entity, bcf.v2.model.TopicDocumentReference):
reference = entity.referenced_document
else:
reference = entity.document_guid
else:
reference = entity.reference
if not reference:
return None
# For v3 document references external documents are detected by empty document_guid.
# External bitmaps are not supported by bcf.
if not isinstance(entity, (bcf.v3.model.DocumentReference, mdl.BitMap)) and entity.is_external:
return reference
if isinstance(entity, bcf.v3.model.DocumentReference):
# Extract document reference filename and contents.
if not bcfxml:
raise TypeError("bcfxml is required for BCF v3 document references.")
assert isinstance(bcfxml, bcf.v3.bcfxml.BcfXml)
error_msg = f"BCF XML is missing document with guid '{reference}'."
if not bcfxml.documents:
raise Exception(error_msg)
definition_docs = bcfxml.documents.definition.documents
if not definition_docs:
raise Exception(error_msg)
docs = next((doc for doc in definition_docs.document if doc.guid == reference), None)
if not docs:
raise Exception(error_msg)
filename = docs.filename
bytes_data = bcfxml.documents.documents[filename]
else:
if isinstance(entity, mdl.BimSnippet):
bytes_data = topic.bim_snippet
assert isinstance(bytes_data, bytes)
elif isinstance(entity, mdl.HeaderFile):
bytes_data = topic.reference_files[reference]
elif isinstance(entity, mdl.BitMap):
bytes_data = next(
byte_data
for vp in topic.viewpoints.values()
for data_reference, byte_data in vp.bitmaps.items()
if data_reference == reference
)
elif isinstance(entity, bcf.v2.model.TopicDocumentReference):
assert isinstance(topic, bcf.v2.topic.TopicHandler)
bytes_data = topic.document_references[reference]
else:
assert_never(entity)
# We don't really need it if 'outfile' is None, just keeping type checker happy.
if isinstance(entity, mdl.HeaderFile) and entity.filename:
filename = entity.filename
else:
filename = Path(reference).name
if not outfile:
outfile = Path(tempfile.mkdtemp()) / filename
with open(outfile, "wb") as f:
f.write(bytes_data)
return outfile
+10 -48
View File
@@ -63,11 +63,21 @@ class TopicHandler:
"""Return the header of the topic."""
return self.markup.header if self.markup else None
@header.setter
def header(self, header: mdl.Header) -> None:
"""Set the header of the topic."""
self.markup.header = header
@property
def comments(self) -> list[mdl.Comment]:
"""Return the comments of the topic."""
return self.markup.comment if self.markup else []
@comments.setter
def comments(self, comments: list[mdl.Comment]) -> None:
assert self.markup
self.markup.comment = comments
@property
def bim_snippet(self) -> Optional[bytes]:
if not self._bim_snippet and self._topic_dir:
@@ -230,54 +240,6 @@ class TopicHandler:
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
destination_zip.writestr(real_path.at, self.document_references[doc.referenced_document])
def extract_file(
self, entity: Union[mdl.HeaderFile, mdl.BimSnippet, mdl.TopicDocumentReference], outfile: Optional[Path] = None
) -> Union[Path, str, None]:
"""Extracts an element with a file into a temporary directory
These include header files, bim snippets, document references, and
viewpoint bitmaps. External reference are not downloaded. Instead, the
URI reference is returned.
:param entity: The entity with a file reference to extract
:param outfile: If provided, save the header file to that location.
Otherwise, a temporary directory is created and the filename is
derived from the header's original filename.
:return: The filepath of the extracted file. It may be a URL if the
header file is external.
:rtype: Path
"""
if hasattr(entity, "reference"):
reference = entity.reference
else:
reference = entity.referenced_document
if not reference:
return
if getattr(entity, "is_external", False):
return entity.reference
resolved_reference = self._topic_dir
for part in Path(reference).parts:
if part == "..":
resolved_reference = resolved_reference.parent
else:
resolved_reference = resolved_reference.joinpath(part)
if not outfile:
if getattr(entity, "filename", None):
filename = entity.filename
else:
filename = resolved_reference.name
outfile = Path(tempfile.mkdtemp()) / filename
with open(outfile, "wb") as f:
f.write(resolved_reference.read_bytes())
return outfile
def add_viewpoint(self, element: entity_instance) -> VisualizationInfoHandler:
"""Add a viewpoint pointed at the placement of an IFC element to the topic.
+52 -2
View File
@@ -27,6 +27,7 @@ class TopicHandler:
) -> None:
self._markup: Optional[mdl.Markup] = None
self._viewpoints: Optional[dict[str, VisualizationInfoHandler]] = None
self._reference_files: Optional[dict[str, bytes]] = None
self._bim_snippet: Optional[bytes] = None
self._xml_handler = xml_handler or XmlParserSerializer()
self._topic_dir = topic_dir
@@ -49,22 +50,36 @@ class TopicHandler:
return self.markup.topic
@property
def guid(self) -> Optional[str]:
def guid(self) -> str:
"""Return the GUID of the topic."""
if self._markup:
return self.topic.guid
return self._topic_dir.name if self._topic_dir else None
return self._topic_dir.name if self._topic_dir else ""
@property
def header(self) -> Optional[mdl.Header]:
"""Return the header of the topic."""
return self.markup.header
@header.setter
def header(self, header: mdl.Header) -> None:
"""Set the header of the topic."""
self.markup.header = header
@property
def comments(self) -> list[mdl.Comment]:
"""Return the comments of the topic."""
return self.topic.comments.comment if self.topic.comments else []
@comments.setter
def comments(self, comments: list[mdl.Comment]) -> None:
topic_comments = self.topic.comments
if topic_comments is None:
if not comments:
return
self.topic.comments = (topic_comments := mdl.TopicComments())
topic_comments.comment = comments
@property
def bim_snippet(self) -> Optional[bytes]:
if not self._bim_snippet and self._topic_dir:
@@ -94,6 +109,27 @@ class TopicHandler:
return bim_snippet_path.read_bytes()
return None
@property
def reference_files(self) -> dict[str, bytes]:
if self._reference_files is not None:
return self._reference_files
self._reference_files = {}
if not self.header:
return self._reference_files
if not self.header.files:
return self._reference_files
for ref in self.header.files.file:
if ref.is_external:
continue
real_path = self._topic_dir
for path_part in ref.reference.split("/"):
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
self._reference_files[ref.reference] = real_path.read_bytes()
return self._reference_files
@classmethod
def create_new(
cls,
@@ -145,6 +181,7 @@ class TopicHandler:
self._save_xml(destination_zip, self._markup, "markup.bcf")
self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip)
self._save_reference_files(destination_zip)
def _save_viewpoints(self, destination_zip: ZipFileInterface, topic_dir: str) -> None:
if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point):
@@ -165,6 +202,19 @@ class TopicHandler:
if self.bim_snippet:
destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet)
def _save_reference_files(self, destination_zip: ZipFileInterface) -> None:
if not self.header:
return
if not self.header.files:
return
for ref in self.header.files.file:
if ref.is_external or not ref.reference:
continue
real_path = self._topic_dir
for path_part in ref.reference.split("/"):
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
destination_zip.writestr(real_path.at, self.reference_files[ref.reference])
def add_viewpoint(self, element: entity_instance) -> VisualizationInfoHandler:
"""
Add a viewpoint tergeting an IFC element to the topic.
+3 -1
View File
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=d51fa2c
OLD:=03935a9
.PHONY: bump
bump:
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
@@ -130,6 +130,8 @@ endif
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel odfpy --wheel-dir=./wheels
# Required by IFCCityJSON
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download cjio --dest=./wheels
# Required in general for sorting all sorts of stuff in a nice way
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download natsort --dest=./wheels
# Provides express rule validation for ifcopenshell.validate
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pytest --dest=./wheels
# Provides Brickschema functionality
+5
View File
@@ -59,6 +59,7 @@ last_error = None
last_actions: deque = deque(maxlen=10)
FIRST_INSTALLED_BBIM_VERSION: Union[str, None] = None
REINSTALLED_BBIM_VERSION: Union[str, None] = None
REGISTERED_BBIM_PACKAGE: str
def initialize_bbim_semver():
@@ -238,6 +239,10 @@ if IN_BLENDER:
if platform.system() == "Windows":
clean_up_dlls_safe_links()
import bonsai
bonsai.REGISTERED_BBIM_PACKAGE = __package__
import bonsai.bim
current_version = bbim_semver["version"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+86 -15
View File
@@ -1,6 +1,6 @@
import sys
import os
import webbrowser
bonsai_lib_path = os.environ.get("BONSAI_LIB_PATH")
bonsai_version = os.environ.get("BONSAI_VERSION")
@@ -13,6 +13,8 @@ from aiohttp import web
import socketio
import pystache
import json
import base64
import xml.etree.ElementTree as ET
sio_port = 8080 # default port
@@ -20,14 +22,6 @@ sio = socketio.AsyncServer(
cors_allowed_origins="*",
async_mode="aiohttp",
)
# sio.instrument(
# auth={
# "username": "admin",
# "password": "admin",
# }
# )
app = web.Application()
sio.attach(app)
@@ -37,6 +31,8 @@ blender_messages = {}
blender_theme = {}
# we define two namespaces, one for Bonsai and one for Web UI
# each namespace has its own event handlers that are called when an event is emitted by a client
# Web namespace
class WebNamespace(socketio.AsyncNamespace):
def __init__(self, namespace):
@@ -44,9 +40,9 @@ class WebNamespace(socketio.AsyncNamespace):
async def on_connect(self, sid, environ):
print(f"Web client connected: {sid}")
await sio.emit("theme_data", blender_theme, namespace="/web", room=sid)
if blender_messages:
await sio.emit("connected_clients", list(blender_messages.keys()), namespace="/web", room=sid)
await sio.emit("theme_data", blender_theme, namespace="/web", room=sid)
await self.send_cached_messages(sid)
async def on_disconnect(self, sid):
@@ -61,11 +57,9 @@ class WebNamespace(socketio.AsyncNamespace):
)
async def on_get_svg(self, sid, data):
print("hello world!")
file_path = data["path"]
with open(file_path, "r") as file:
svg_data = file.read()
await sio.emit("svg_data", svg_data, room=sid, namespace="/web")
svg = await self.process_svg(file_path)
await sio.emit("svg_data", svg, room=sid, namespace="/web")
async def send_cached_messages(self, sid):
# Send cached messages to the connected web client
@@ -74,6 +68,37 @@ class WebNamespace(socketio.AsyncNamespace):
await self.emit("csv_data", {"blenderId": blenderId, "data": messages["csv_data"]}, room=sid)
if "gantt_data" in messages:
await self.emit("gantt_data", {"blenderId": blenderId, "data": messages["gantt_data"]}, room=sid)
if "demo_data" in messages:
await self.emit("demo_data", {"blenderId": blenderId, "data": messages["demo_data"]}, room=sid)
async def process_svg(self, file_path):
def encode_image(filepath):
# Encode file content to base64 string
with open(filepath, "rb") as file:
encoded_string = base64.b64encode(file.read()).decode("utf-8")
return f"data:image;base64,{encoded_string}"
file_dir = os.path.dirname(file_path)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
tree = ET.parse(file_path)
root = tree.getroot()
namespaces = {
"svg": "http://www.w3.org/2000/svg",
"xlink": "http://www.w3.org/1999/xlink",
}
for element in root.findall(".//svg:image[@xlink:href]", namespaces):
href = element.get("{http://www.w3.org/1999/xlink}href")
if href and href.endswith((".png", ".svg", ".jpeg")):
img_path = os.path.join(file_dir, href)
if os.path.exists(img_path):
base64_data = encode_image(img_path)
element.set("{http://www.w3.org/1999/xlink}href", base64_data)
return ET.tostring(root, "unicode")
# Blender namespace
@@ -117,10 +142,39 @@ class BlenderNamespace(socketio.AsyncNamespace):
async def on_theme_data(self, sid, data):
global blender_theme
print(f"Blender theme data")
blender_theme = data
await sio.emit("theme_data", data, namespace="/web")
# this function will be called when the event demo_data is emitted
async def on_demo_data(self, sid, data):
print(f"Demo data from Blender client {sid}")
blender_messages[sid]["demo_data"] = data
await sio.emit("demo_data", {"blenderId": sid, "data": data}, namespace="/web")
async def on_work_schedule_info(self, sid, data):
print(f"Work schedule info from Blender client {sid}")
blender_messages[sid]["work_schedule_info"] = data
await sio.emit("work_schedule_info", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_items(self, sid, data):
print(f"Cost items data from Blender client {sid}")
blender_messages[sid]["cost_items"] = data
await sio.emit("cost_items", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_schedules(self, sid, data):
print(f"Cost schedule info from Blender client {sid}")
blender_messages[sid]["cost_schedules"] = data
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_values(self, sid, data):
print(f"Cost values from Blender client {sid}")
blender_messages[sid]["cost_values"] = data
await sio.emit("cost_values", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_value(self, sid, data):
print(f"Cost values from Blender client {sid}")
blender_messages[sid]["cost_value"] = data
await sio.emit("cost_value", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request):
with open("templates/index.html", "r") as f:
@@ -128,6 +182,12 @@ async def schedules(request):
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def costing(request):
with open("templates/costing.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def sequencing(request):
with open("templates/gantt.html", "r") as f:
@@ -143,6 +203,15 @@ async def documentation(request):
return web.Response(text=html_content, content_type="text/html")
# This is a request handler for the /demo URL endpoint.
# It serves the demo HTML file after using pystache to render the variables
async def demo(request):
with open("templates/demo.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def on_startup(app):
pid_file = "running_pid.json"
@@ -169,6 +238,8 @@ sio.register_namespace(BlenderNamespace("/blender"))
app.router.add_get("/", schedules)
app.router.add_get("/documentation", documentation)
app.router.add_get("/sequencing", sequencing)
app.router.add_get("/costing", costing)
app.router.add_get("/demo", demo)
# Add static files
app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt")
@@ -0,0 +1,58 @@
:root.blender .flex-row {
display: flex;
flex-direction: row;
justify-content: space-between;
}
/* CSS for the work schedule cards */
:root.blender #work_schedules {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 20px;
}
:root.blender .card {
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 300px;
margin: 10px;
transition: transform 0.2s;
}
:root.blender .card:hover {
transform: scale(1.05);
}
:root.blender .card-body {
padding: 20px;
}
:root.blender .card-title {
font-size: 1.25rem;
margin-bottom: 10px;
}
:root.blender .card-text {
font-size: 1rem;
margin-bottom: 20px;
}
:root.blender .btn-primary {
background-color: #007bff;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 1rem;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
:root.blender .btn-primary:hover {
background-color: #0056b3;
}
@@ -0,0 +1,89 @@
th {
position: relative;
}
.resizer {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.context-menu {
display: none;
position: absolute;
background-color: rgb(51, 51, 51);
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
color: white;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.context-menu button:hover {
background-color: #f0f0f0;
color: black;
}
/* Context menu button is clicked, should zoom into object and change color */
.context-menu button:active {
background-color: #ff9634;
color: rgba(37, 51, 77, 0.466);
/* zoom */
transform: scale(0.9);
}
#cost-items tr:hover {
background-color: #f0f0f0;
color : black;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: #363636;
padding: 10px;
}
.cost-values-form {
position: absolute; /* Set initial position to absolute */
background-color: white;
border: 1px solid black;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 9999;
cursor: move; /* Change cursor to move */
}
.cost-values-table {
width: 100%;
border-collapse: collapse;
}
.cost-values-table th, .cost-values-table td {
border: 1px solid #ddd;
padding: 8px;
}
.cost-values-table th {
background-color: #f2f2f2;
text-align: left;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
}
@@ -0,0 +1,345 @@
:root {
--font-family: Arial, sans-serif;
--base-font-size: 16px;
--margin-tiny: 0.125rem;
--margin-small: 0.625rem;
--margin-medium: 1.25rem;
--margin-large: 2.5rem;
--padding-tiny: 0.125rem;
--padding-small: 0.625rem;
--padding-medium: 1rem;
--font-size-large: 1.2rem;
--logo-height: 1.5rem;
--nav-height: 0.625rem;
--border-radius: 0.3rem;
}
:root.dark {
color-scheme: dark;
--bg-color: #252525;
--text-color: #e0e0e0;
--nav-bg-color: #121212;
--nav-border-color: #25682a;
--nav-link-color: #fff;
--nav-link-hover-color: #3fb449;
--warning-color: #FFDB8F;
--border-color: #464444;
}
:root.light {
color-scheme: light;
--bg-color: #ffffff;
--text-color: #000000;
--nav-bg-color: #f8f8f8;
--nav-border-color: #cccccc;
--nav-link-color: #000000;
--nav-link-hover-color: #38a63d;
--warning-color: #FF4500;
--border-color: #222;
}
html {
font-size: var(--base-font-size);
}
body {
background-color: var(--blender-window-background, var(--bg-color));
color: var(--blender-text, var(--text-color));
margin: 0;
font-family: var(--font-family);
display: flex;
flex-direction: column;
min-height: 100vh;
}
#container {
flex: 1;
padding: var(--margin-small);
}
h3 {
margin-top: 0;
}
nav {
background-color: var(--blender-top-bar-header, var(--nav-bg-color));
height: var(--nav-height);
padding: var(--padding-medium) 0;
display: flex;
align-items: center;
position: relative;
border-bottom: 2px solid var(--blender-tab-outline, var(--nav-border-color));
}
nav .logo {
margin-left: var(--margin-large);
height: var(--logo-height);
}
nav ul {
list-style-type: none;
margin: 0;
display: flex;
flex: 1;
justify-content: center;
}
nav ul li {
margin-right: var(--margin-large);
}
nav ul li a {
text-decoration: none;
color: var(--blender-text, var(--nav-link-color));
}
nav ul li a:hover,
nav ul li a.active {
color: var(--blender-selected-object, var(--nav-link-hover-color));
}
#toggle-theme {
border: none;
background: none;
cursor: pointer;
font-size: var(--font-size-large);
margin-right: var(--margin-medium);
}
#toggle-theme:focus {
outline: none;
}
button,
input {
background-color: var(--blender-button-background, var(--bg-color));
color: var(--blender-button-text, var(--text-color));
border-color: var(--blender-button-border, var(--border-color));
transition: filter 0.2s ease;
}
footer {
background-color: var(--blender-top-bar-header, var(--nav-bg-color));
text-align: right;
padding: var(--padding-tiny);
border-top: 1px solid var(--blender-tab-outline, var(--nav-border-color));
}
footer p {
margin: 0;
color: var(--blender-text, var(--nav-link-color));
margin-right: var(--margin-small);
font-size: 0.8rem;
}
/* =========== styling for code blocks and rendering =========== */
/* ================== This is auto-generated ================== */
#explanation {
font-family: "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif;
font-size: 16px;
padding: 0 26px;
line-height: 22px;
word-wrap: break-word;
margin: 0;
}
#code-csp-warning {
position: fixed;
top: 0;
right: 0;
color: white;
margin: 16px;
text-align: center;
font-size: 12px;
font-family: sans-serif;
background-color: #444444;
cursor: pointer;
padding: 6px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .25);
}
#code-csp-warning:hover {
text-decoration: none;
background-color: #007acc;
box-shadow: 2px 2px 2px rgba(0, 0, 0, .25);
}
#explanation.scrollBeyondLastLine {
margin-bottom: calc(100vh - 22px);
}
#explanation.showEditorSelection .code-line {
position: relative;
}
#explanation.showEditorSelection .code-active-line:before,
#explanation.showEditorSelection .code-line:hover:before {
content: "";
display: block;
position: absolute;
top: 0;
left: -12px;
height: 100%;
}
#explanation.showEditorSelection li.code-active-line:before,
#explanation.showEditorSelection li.code-line:hover:before {
left: -30px;
}
#explanation a:focus,
#explanation input:focus,
#explanation select:focus,
#explanation textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
#explanation hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
#explanation h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
#explanation h1,
#explanation h2,
#explanation h3 {
font-weight: normal;
}
#explanation code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 1em;
line-height: 1.357em;
}
#explanation .wordWrap pre {
white-space: pre-wrap;
}
#explanation pre:not(.hljs),
#explanation pre.hljs code>div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
#explanation pre code {
tab-size: 4;
}
#explanation .hljs-comment,
#explanation .hljs-quote {
color: #B6B18B;
}
#explanation .hljs-variable,
#explanation .hljs-template-variable,
#explanation .hljs-tag,
#explanation .hljs-name,
#explanation .hljs-selector-id,
#explanation .hljs-selector-class,
#explanation .hljs-regexp,
#explanation .hljs-deletion {
color: #EB3C54;
}
#explanation .hljs-number,
#explanation .hljs-built_in,
#explanation .hljs-builtin-name,
#explanation .hljs-literal,
#explanation .hljs-type,
#explanation .hljs-params,
#explanation .hljs-meta,
#explanation .hljs-link {
color: #E7CE56;
}
#explanation .hljs-attribute {
color: #EE7C2B;
}
#explanation .hljs-string,
#explanation .hljs-symbol,
#explanation .hljs-bullet,
#explanation .hljs-addition {
color: #4FB4D7;
}
#explanation .hljs-title,
#explanation .hljs-section {
color: #78BB65;
}
#explanation .hljs-keyword,
#explanation .hljs-selector-tag {
color: #B45EA4;
}
#explanation .hljs {
display: block;
overflow-x: auto;
background: #1C1D21;
color: #c0c5ce;
padding: 0.5em;
}
#explanation .hljs-emphasis {
font-style: italic;
}
#explanation .hljs-strong {
font-weight: bold;
}
#explanation {
font-family: "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif, "Meiryo";
padding: 0 12px;
}
#explanation pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
border-radius: 3px;
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
#explanation pre:not(.hljs) {
padding: 23px;
line-height: 19px;
}
#explanation blockquote {
background: rgba(127, 127, 127, 0.1);
border-color: rgba(0, 122, 204, 0.5);
}
#explanation .emoji {
height: 1.4em;
}
#explanation code {
font-size: 14px;
line-height: 19px;
}
#explanation :not(pre):not(.hljs)>code {
color: #C9AE75;
font-size: inherit;
}
#explanation .page {
page-break-after: always;
}
@@ -55,10 +55,7 @@ body {
}
#container {
margin-top: var(--margin-medium);
margin-left: var(--margin-tiny);
margin-right: var(--margin-tiny);
margin-bottom: var(--margin-medium);
padding: var(--margin-small);
height: calc(100vh - var(--nav-height));
box-sizing: border-box;
display: flex;
@@ -1,3 +1,6 @@
@import url("./components/card.css");
@import url("./components/contextMenu.css");
:root {
--font-family: Arial, sans-serif;
--base-font-size: 16px;
@@ -72,10 +75,7 @@ body {
#container {
flex: 1;
margin-top: var(--margin-medium);
margin-left: var(--margin-small);
margin-right: var(--margin-small);
margin-bottom: var(--margin-medium);
padding: var(--margin-small);
}
h3 {
@@ -57,11 +57,9 @@ body {
}
#container {
position: relative;
padding: var(--margin-small);
flex: 1;
margin-top: var(--margin-medium);
margin-left: var(--margin-small);
margin-right: var(--margin-small);
margin-bottom: var(--margin-medium);
}
h3 {
@@ -204,7 +202,10 @@ footer p {
}
.table-container {
margin-bottom: var(--margin-large);
display: block;
height: calc(100vh - var(--margin-small));
width: 100%;
margin-bottom: var(--margin-medium);
}
.csv-table {
@@ -279,22 +280,29 @@ button:hover {
:root.blender .tabulator-selected .tabulator-cell {
:root.blender .tabulator-selected .tabulator-cell,
:root.blender .tabulator-row.tabulator-selected {
background-color: var(--blender-active-highlight) !important;
color: var(--blender-active-text-highlight) !important;
}
:root.blender .tabulator-selected:hover .tabulator-cell {
:root.blender .tabulator-selected:hover .tabulator-cell,
:root.blender .tabulator-row.tabulator-selected:hover {
filter: brightness(1.2)
}
.tabulator-menu-item {
:root.blender .tabulator-popup-container {
background: var(--blender-panel-background);
border: var(--blender-tab-outline);
}
:root.blender .tabulator-menu-item {
color: var(--blender-text) !important;
background-color: var(--blender-panel-background) !important;
transition: filter 0.1s ease;
}
.tabulator-menu-item:hover {
:root.blender .tabulator-menu-item:hover {
filter: brightness(1.4);
}
@@ -0,0 +1,204 @@
import { CostUI } from './utilities/costui.js';
const connectedClients = {};
let socket;
$(document).ready(function () {
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
setTheme(theme);
connectSocket();
CostUI.createColorPicker();
});
function connectSocket() {
const url = "ws://localhost:" + SOCKET_PORT + "/web";
socket = io(url);
socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("connected_clients", handleConnectedClients);
socket.on("theme_data", handleThemeData);
socket.on("connect", handleWebConnect);
socket.on("cost_schedules", handleCostSchedulesData);
socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData);
}
function handleBlenderConnect(blenderId) {
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = { shown: false, ifc_file: "" };
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
}
function handleBlenderDisconnect(blenderId) {
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeTableElement(blenderId);
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) - 1;
});
}
function removeTableElement(blenderId) {
$("#cost-items-" + blenderId).remove();
}
function handleCostValueData(data) {
const costItemId = data.data["cost_value"]["cost_item_id"];
const costValueId = data.data["cost_value"]["cost_value_id"];
console.log("Handling cost value data", costItemId, costValueId);
CostUI.addNewCostValueRow(costItemId, costValueId);
}
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
data.forEach(function (id) {
connectedClients[id] = { shown: false, ifc_file: "" };
});
}
function handleCostValuesData(data) {
CostUI.createCostValuesForm({
costValues: data.data["cost_values"]["cost_values"],
costItemId: data.data["cost_values"]["cost_item_id"],
callbacks: {
'editCostValues': editCostValues,
'addCostValue': addCostValue,
},
});
}
function addCostValue(costItemId) {
executeOperator({ type: "addCostValue", costItemId: costItemId });
}
function editCostValues(costItemId, costValues) {
executeOperator({
type: "editCostValues",
costItemId: costItemId,
costValues: costValues
});
}
function handleThemeData(themeData) {
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
if (a !== undefined) {
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
return `rgb(${r}, ${g}, ${b})`;
}
function generateCssVariableRule(theme) {
let cssVariables = ":root.blender {\n";
for (const key in theme) {
const cssVariableName = `--blender-${key.replace(/_/g, "-")}`;
const cssVariableValue = arrayToRgbString(theme[key]);
cssVariables += ` ${cssVariableName}: ${cssVariableValue};\n`;
}
cssVariables += "}";
return cssVariables;
}
const cssRule = generateCssVariableRule(themeData.theme);
var styleElement = $("#index-stylesheet")[0];
if (styleElement) {
var sheet = styleElement.sheet || styleElement.styleSheet;
sheet.insertRule(cssRule, sheet.cssRules.length);
}
}
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
if (theme === "light") {
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
} else if (theme === "blender") {
$("#toggle-theme").html('<i class="fas fa-adjust"></i>');
}
localStorage.setItem("theme", theme);
}
function addCostItem(costItemId) {
executeOperator({ type: "addCostItem", costItemId: costItemId });
}
function editCostItemName(costItemId, name) {
executeOperator({ type: "editCostItemName", costItemId: costItemId, name: name });
}
function selectAssignedElements(costItemId) {
executeOperator({ type: "selectAssignedElements", costItemId: costItemId });
}
function handleWebConnect() {
getCostSchedules();
}
function handleCostSchedulesData(data) {
const blenderId = data.blenderId;
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
const currency = data.data["cost_schedules"]["currency"]["name"];
const costScheduleDiv = $("#cost-schedules");
costSchedules.forEach((costSchedule) => {
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate);
const callback = () => loadCostSchedule(costSchedule.id, blenderId);
const card = CostUI.createCard(costSchedule.Name, mainContainer, callback);
costScheduleDiv.append(card);
});
}
function handleCostItemsData(data) {
CostUI.createCostSchedule({
data: data.data["cost_items"],
blenderID: data.blenderId,
callbacks: {
"addCostItem": addCostItem,
"selectAssignedElements": selectAssignedElements,
'editCostItemName': editCostItemName,
'enableEditingCostValues': enableEditingCostValues,
}
});
}
function executeOperator(operator, blenderId) {
const msg = {
sourcePage: "cost",
operator: operator,
};
if (blenderId !== undefined) {
msg.BlenderId = blenderId;
}
socket.emit("web_operator", msg);
}
function loadCostSchedule(costScheduleId, blenderId) {
executeOperator({ type: "loadCostSchedule", costScheduleId: costScheduleId }, blenderId);
}
function getCostSchedules(blenderId) {
executeOperator({ type: "getCostSchedules" }, blenderId);
}
function enableEditingCostValues(costItemId) {
executeOperator({ type: "enableEditingCostValues", costItemId: costItemId});
}
@@ -0,0 +1,146 @@
// A global variable to hold the WebSocket connection
let socket;
// Document ready function
// This function is triggered once the DOM is fully loaded.
// It sets the theme for the UI and
// establishes the WebSocket connection by calling connectSocket().
$(document).ready(function () {
var defaultTheme = "blender"; // Default theme to be applied
var theme = localStorage.getItem("theme") || defaultTheme; // Retrieve the stored theme or use the default
setTheme(theme); // Apply the theme
connectSocket();
});
// Function to establish WebSocket connection
// This function constructs the WebSocket connection URL using the SOCKET_PORT variable
// and initializes the socket connection. It also registers event handlers to listen
// for various events emitted by the server, such as 'blender_connect', 'blender_disconnect',
// 'theme_data', and 'demo_data'.
function connectSocket() {
const url = "ws://localhost:" + SOCKET_PORT + "/web";
socket = io(url);
console.log("socket: ", socket);
// Register socket event handlers
socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("theme_data", handleThemeData);
socket.on("demo_data", handleDemoData);
}
// Function to handle 'blender_connect' event
// This function is triggered when a new Bonsai instance connects to the server.
// The 'blenderId' is a unique identifier assigned by the server to each Bonsai instance.
// It helps distinguish between different instances of Bonsai connected to the server.
// The 'blenderId' is used in various parts of the code to ensure that messages and data
// are correctly associated with the appropriate Bonsai instance.
function handleBlenderConnect(blenderId) {
console.log("blender connected: ", blenderId);
}
// Function to handle 'blender_disconnect' event
// This function is triggered when a Bonsai instance disconnects from the server.
// The 'blenderId' is used here to log which specific Bonsai instance has disconnected.
function handleBlenderDisconnect(blenderId) {
console.log("blender disconnected: ", blenderId);
}
// Function to handle 'demo_data' event
// This function processes the demo data received from the server.
// It extracts the message and the Bonsai instance ID ('blenderId'),
// and updates the DOM to display the message. The 'blenderId' is used
// to differentiate between messages from different Bonsai instances.
// If the message for the specific Bonsai instance is already displayed, it updates the content;
// otherwise, it creates a new element to display the message.
function handleDemoData(demoData) {
console.log(demoData);
const message = demoData["data"]["demo_message"];
const blenderId = demoData["blenderId"];
const id = "message-" + blenderId;
const messageHeader = $("#" + id);
const messageText = `Bonsai instance with ID: ${blenderId} sent the message: ${message}`;
if (messageHeader[0] === undefined) {
const newMessageElement = $("<h3>", { id: id }).text(messageText);
$("#message-container").prepend(newMessageElement);
} else {
messageHeader.text(messageText);
messageHeader.prependTo("#message-container");
}
}
// Function to send a message to the server
// This function is triggered when the user submits a message.
// It collects the message from the input field, constructs a message object,
// and emits the 'web_operator' event to the server with the message data.
// If 'blenderId' is specified, the message is sent to a specific Bonsai instance.
function sendMessage() {
const inputMessage = $("#input-message").val();
const msg = {
sourcePage: "demo",
// blenderId: BlenderId, // specify the target Bonsai instance
operator: {
type: "message",
message: inputMessage,
},
};
socket.emit("web_operator", msg);
}
function handleThemeData(themeData) {
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
if (a !== undefined) {
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
return `rgb(${r}, ${g}, ${b})`;
}
function generateCssVariableRule(theme) {
let cssVariables = ":root.blender {\n";
for (const key in theme) {
const cssVariableName = `--blender-${key.replace(/_/g, "-")}`;
const cssVariableValue = arrayToRgbString(theme[key]);
cssVariables += ` ${cssVariableName}: ${cssVariableValue};\n`;
}
cssVariables += "}";
return cssVariables;
}
const cssRule = generateCssVariableRule(themeData.theme);
console.log(cssRule);
var styleElement = $("#demo-stylesheet")[0];
if (styleElement) {
var sheet = styleElement.sheet || styleElement.styleSheet;
sheet.insertRule(cssRule, sheet.cssRules.length);
}
}
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
if (theme === "light") {
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
} else if (theme === "blender") {
$("#toggle-theme").html('<i class="fas fa-adjust"></i>');
}
localStorage.setItem("theme", theme);
}
function toggleTheme() {
if ($("html").hasClass("light")) {
setTheme("dark");
} else if ($("html").hasClass("dark")) {
setTheme("blender");
} else {
setTheme("light");
}
}
@@ -1,3 +1,5 @@
import { CostUI } from './utilities/costui.js';
// keeps track of blenders connected in form of
// shown:bool, workSchedule: {}, ganttTasks: {}, warning: bool
const connectedClients = {};
@@ -36,9 +38,16 @@ function connectSocket() {
socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("connected_clients", handleConnectedClients);
socket.on("connect", handleWebConnect);
socket.on("theme_data", handleThemeData);
socket.on("gantt_data", handleGanttData);
socket.on("default_data", handleDefaultData);
socket.on("work_schedule_info", handleWorkScheduleData);
}
// function used to get drawings data from Bonsai
function handleWebConnect() {
getWorkScheduleData();
}
// Function to handle 'blender_connect' event
@@ -62,6 +71,7 @@ function handleBlenderDisconnect(blenderId) {
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeWorkScheduleInfo(blenderId);
removeGanttElement(blenderId);
}
@@ -72,7 +82,6 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = {
shown: false,
@@ -105,7 +114,6 @@ function handleThemeData(themeData) {
}
const cssRule = generateCssVariableRule(themeData.theme);
console.log(cssRule);
var styleElement = $("#gantt-stylesheet")[0];
if (styleElement) {
@@ -114,12 +122,25 @@ function handleThemeData(themeData) {
}
}
// Function to handle 'gantt_data' event
function handleWorkScheduleData(data) {
const blenderId = data["blenderId"];
const workSchedulesMainDiv = $("#work_schedules");
const workSchedulesDiv = $("<div>").attr("id", "work_schedules-" + blenderId);
workSchedulesMainDiv.append(workSchedulesDiv);
const workSchedules = data["data"]["work_schedule_info"];
workSchedules.forEach((workSchedule) => {
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
const callback = () => loadWorkSchedule(workSchedule.id);
const card = CostUI.createCard(workSchedule.Name,mainContainer, callback);
workSchedulesDiv.append(card);
});
}
function handleGanttData(data) {
const blenderId = data["blenderId"];
console.log(data);
const filename = data["data"]["ifc_file"];
const ganttTasks = data["data"]["gantt_data"]["tasks"];
const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"];
@@ -156,7 +177,6 @@ function handleDefaultData(data) {
const blenderId = data["blenderId"];
const isDirty = data["data"]["is_dirty"];
showWarning(blenderId, isDirty);
console.log(data);
}
// Function to add a new gantt with data and filename
@@ -551,3 +571,33 @@ function toggleClientList() {
clientList.addClass("show");
}
function loadWorkSchedule(workScheduleId) {
const msg = {
sourcePage: "gantt",
operator: {
type: "loadWorkSchedule",
workScheduleId: workScheduleId,
},
};
socket.emit("web_operator", msg);
}
function removeWorkScheduleInfo(blenderId) {
$("#work_schedules-"+ blenderId).empty();
}
function getWorkScheduleData(blenderId) {
const msg = {
sourcePage: "gantt",
operator: {
type: "getWorkSchedules",
},
};
if (blenderId !== undefined) {
msg.BlenderId = blenderId;
}
socket.emit("web_operator", msg);
}
@@ -144,6 +144,7 @@ function addTableElement(blenderId, csvData, filename) {
}
// console.log(connectedClients[blenderId]);
const tableContainer = $("<div></div>")
.addClass("table-container")
.attr("id", "container-" + blenderId);
@@ -206,26 +207,76 @@ function addTableElement(blenderId, csvData, filename) {
return menu;
}
// create right click context menu for choosing column top calculation
function createColumnContextMenu(column) {
const field = column.field;
const calculations = ["sum", "avg", "max", "min"];
// label is the item label in the menu
// update column defination on clicking an item
// specifically the topCalc and pass calc type to the topCalcFormatter function
// uisng the topCalcFormatterParams field
return calculations.map((calc) => ({
label: `Show ${
calc.charAt(0).toUpperCase() + calc.slice(1)
} for ${field}`,
action: (e, cell) =>
cell
.getColumn()
.updateDefinition({ topCalc: calc, topCalcFormatterParams: calc }),
}));
}
function calcFormatter(cell, formatterParams, onRendered) {
// formatterParams is anything in the topCalcFormatterParams field in the column definition
// so we pass the calculation type to display what the calculation type is
return `${formatterParams}: ${cell.getValue()}`;
}
var containerHeight = $(".table-container").height();
var h3Height = $(".table-container>h3").height();
var table = new Tabulator("#table-" + blenderId, {
placeholder: "No data to display",
height: "400px",
resizableColumnGuide: true,
index: "GlobalId",
data: csvData,
importFormat: "csv",
autoColumns: true,
selectableRows: 1,
layout: "fitColumns",
layout: "fitDataFill",
height: containerHeight - h3Height - 20 + "px",
movableColumns: true,
// Our fields are never nested, and we use the "." character in queries
nestedFieldSeparator: false,
// defines column defualts that are applied to all columns
columnDefaults: {
visible: true,
headerFilter: true,
topCalc: "sum",
topCalcFormatter: calcFormatter,
topCalcFormatterParams: "sum",
},
// defines a callback to edit the column definition generated by autoColumns
// that cannot be set as default
autoColumnsDefinitions: function (definitions) {
menu = createHeaderMenu(definitions, table);
headerMenu = createHeaderMenu(definitions, table);
definitions.forEach((column) => {
column.visible = true;
column.headerMenu = menu;
column.headerMenu = headerMenu;
column.contextMenu = createColumnContextMenu(column);
});
return definitions;
},
});
var downloadCsv = $('<a><i class="fa-solid fa-file-csv"></i></a>')
.css("margin-left", "10px")
.css("cursor", "pointer");
tableTitle.append(downloadCsv);
downloadCsv.on("click", function () {
table.download("csv", "data.csv");
});
table.on("rowSelected", function (row) {
var index = row.getIndex(); // the guid of the object
@@ -0,0 +1,736 @@
export class CostUI {
constructor() {}
static isCostScheduleLoaded(id) {
const existingTable = document.getElementById('cost-items-' + id);
return existingTable !== null;
}
static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove();
}
static createTable(id, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table");
table.id = 'cost-items-' + id;
const tbody = document.createElement("tbody");
tbody.setAttribute("id", "cost-items");
const columnHeaders = ["Name", "Quantity", "Unit", "Cost", "Total Cost", "Action"];
const tr = document.createElement("tr");
for (let i = 0; i < columnHeaders.length; i++) {
const th = document.createElement("th");
th.textContent = columnHeaders[i];
th.style.position = "relative"; // Required for the resizer handle
tr.appendChild(th);
// Add resizer handle
if (i < columnHeaders.length - 1) { // No resizer for the last column
const resizer = document.createElement("div");
resizer.classList.add("resizer");
th.appendChild(resizer);
CostUI.addResizer(resizer);
}
}
tbody.appendChild(tr);
table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table);
// Add CSS to set column widths, resizer styles, hover effect, and color scheme
CostUI.addTableStyles(id);
// Create context menu
CostUI.createContextMenu(callbacks);
table.get_blender_id = function() {
return this.getAttribute("id").split("-")[2];
};
return [table, tbody];
}
static addTableStyles(id) {
const style = document.createElement("style");
style.textContent = `
#cost-items-${id} th:nth-child(1),
#cost-items-${id} td:nth-child(1) {
width: auto;
}
#cost-items-${id} th:not(:nth-child(1)),
#cost-items-${id} td:not(:nth-child(1)) {
width: 100px; /* Set a fixed width for other columns */
}
`;
document.head.appendChild(style);
}
static createContextMenu(callbacks) {
// Create context menu
const contextMenu = document.createElement("div");
contextMenu.id = "context-menu";
contextMenu.classList.add("context-menu");
contextMenu.innerHTML = `
<button id="add-cost-item-button">Add sub-cost</button>
<button id="edit-cost-values-button">Edit</button>
<button id="delete-button">Delete</button>
<button id="duplicate-button">Duplicate</button>
`;
document.body.appendChild(contextMenu);
// Add event listeners for context menu
document.addEventListener("contextmenu", function(event) {
event.preventDefault();
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetRow.parentElement.id === "cost-items") {
const contextMenu = document.getElementById("context-menu");
contextMenu.style.display = "block";
contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.top = `${event.pageY}px`;
// Store the target row in the context menu for later use
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
document.getElementById("context-menu").style.display = "none";
}
});
document.addEventListener("click", function(event) {
const contextMenu = document.getElementById("context-menu");
if (!contextMenu.contains(event.target)) {
contextMenu.style.display = "none";
}
});
const editCostValuesButton = document.getElementById("edit-cost-values-button");
editCostValuesButton.addEventListener("click", function() {
console.log("Edit cost values executed");
const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingCostValues ? callbacks.enableEditingCostValues(costItemId) : null;
}
});
const addButton = document.getElementById("add-cost-item-button");
if (!addButton.dataset.listenerAdded) {
function addCostItemHandler(e) {
e.stopPropagation();
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null;
}
document.getElementById("context-menu").style.display = "none";
}
addButton.addEventListener("click", addCostItemHandler);
addButton.dataset.listenerAdded = "true";
}
document.getElementById("delete-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
// Implement your delete action here
console.log("Delete row:", targetRow.getAttribute("id"));
targetRow.remove();
}
});
document.getElementById("duplicate-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
// Implement your duplicate action here
console.log("Duplicate row:", targetRow.getAttribute("id"));
const newRow = targetRow.cloneNode(true);
targetRow.parentElement.appendChild(newRow);
}
});
}
static addResizer(resizer) {
let startX, startWidth, th;
resizer.addEventListener("mousedown", function(e) {
th = e.target.parentElement;
startX = e.pageX;
startWidth = th.offsetWidth;
document.addEventListener("mousemove", resizeColumn);
document.addEventListener("mouseup", stopResize);
});
function resizeColumn(e) {
const newWidth = startWidth + (e.pageX - startX);
th.style.width = newWidth + "px";
}
function stopResize() {
document.removeEventListener("mousemove", resizeColumn);
document.removeEventListener("mouseup", stopResize);
}
}
static generateColorScheme(baseColor) {
// This function generates a color scheme based on the base color
// For simplicity, we'll just lighten the base color for each level
const levels = 7; // Number of levels of nesting
const colorScheme = [];
for (let i = 0; i < levels; i++) {
colorScheme.push(CostUI.lightenColor(baseColor, i * 7));
}
return colorScheme;
}
static lightenColor(color, percent) {
// This function lightens a color by a given percentage
const num = parseInt(color.slice(1), 16),
amt = Math.round(2.55 * percent),
R = (num >> 16) + amt,
G = (num >> 8 & 0x00FF) + amt,
B = (num & 0x0000FF) + amt;
return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`;
}
static applyColorScheme(tableId, colorScheme) {
const rows = document.querySelectorAll(`#${tableId} tbody tr`);
rows.forEach((row, index) => {
const level = index % colorScheme.length; // Assuming level is determined by index for simplicity
row.style.backgroundColor = colorScheme[level];
});
}
static createColorPicker() {
const colorPicker = document.createElement("input");
colorPicker.type = "color";
colorPicker.id = "color-picker";
colorPicker.value = "#ff0000"; // Default color
const colorText = document.createElement("p");
colorText.textContent = "Select a color to change row color:";
document.getElementById("UI").appendChild(colorText);
document.getElementById("UI").appendChild(colorPicker);
colorPicker.addEventListener("input", function() {
const baseColor = colorPicker.value;
const colorScheme = CostUI.generateColorScheme(baseColor);
CostUI.applyColorScheme("cost-items", colorScheme);
});
const colorScheme = CostUI.generateColorScheme("#000000");
CostUI.applyColorScheme("cost-items", colorScheme);
}
static createCostSchedule({ data, blenderID, title, callbacks = {} }) {
const [table, tbody] = CostUI.createTable(blenderID, callbacks);
CostUI.createCostItem(data, tbody, 0, null, callbacks);
CostUI.applyExpandedState();
}
static createCostItem(data, container, nestingLevel = 0, parentID = null, callbacks = {}) {
data.forEach(obj => {
const row = CostUI.createRow(obj, nestingLevel, parentID, callbacks);
container.appendChild(row);
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
CostUI.createCostItem(obj.is_nested_by, container, nestingLevel + 1, obj.id, callbacks);
}
});
}
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
const row = CostUI.createTableRow(obj, nestingLevel, parentID);
const expandButton = CostUI.createExpandButton(obj);
const nameCell = CostUI.createNameCell(obj, nestingLevel, expandButton, callbacks);
const totalCostQuantityCell = CostUI.createTableCell(obj.TotalCostQuantity);
const unitSymbolCell = CostUI.createTableCell(obj.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(obj.TotalAppliedValue);
const totalCostCell = CostUI.createTotalCostCell(obj);
const flexContainerCell = CostUI.createFlexContainerCell(obj, callbacks);
row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell);
row.appendChild(totalAppliedValueCell);
row.appendChild(totalCostCell);
row.appendChild(flexContainerCell);
row.get_id = function() {
return this.getAttribute("id");
};
row.get_parent = function() {
const parentId = this.getAttribute("parent-id");
return parentId ? document.getElementById(parentId) : null;
};
return row;
}
static createTableRow(obj, nestingLevel, parentID) {
const row = document.createElement("tr");
row.setAttribute("id", obj.id);
row.setAttribute("parent-id", parentID);
if (nestingLevel > 0) {
row.classList.add("nested");
row.classList.add(`level-${nestingLevel}`);
}
return row;
}
static createExpandButton(obj) {
const expandButton = document.createElement("button");
expandButton.classList.add("toggle-button");
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
expandButton.textContent = ">";
} else {
expandButton.style.visibility = "hidden";
}
expandButton.addEventListener("click", function() {
CostUI.contractExpandRow.call(this, obj.id);
});
return expandButton;
}
static createNameCell(obj, nestingLevel, expandButton, callbacks) {
const nameCell = document.createElement("td");
const nameInput = document.createElement("input");
nameInput.value = obj.name ? obj.name : "Unnamed";
nameInput.addEventListener("change", function() {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
});
nameInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
}
});
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
nameCell.appendChild(expandButton);
nameCell.appendChild(nameInput);
return nameCell;
}
static createTableCell(content) {
const cell = document.createElement("td");
cell.textContent = content;
return cell;
}
static createTotalCostCell(obj) {
const totalCostCell = document.createElement("td");
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
return totalCostCell;
}
static createFlexContainerCell(obj, callbacks) {
const divFlex = document.createElement("div");
divFlex.classList.add("flex-container");
const addButton = CostUI.createAddButton(obj, callbacks);
const selectButton = CostUI.createSelectButton(obj, callbacks);
divFlex.appendChild(addButton);
divFlex.appendChild(selectButton);
const flexContainerCell = document.createElement("td");
flexContainerCell.appendChild(divFlex);
return flexContainerCell;
}
static createAddButton(obj, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
addButton.addEventListener("click", function(e) {
e.stopPropagation();
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
});
return addButton;
}
static createSelectButton(obj, callbacks) {
const selectButton = document.createElement("button");
selectButton.textContent = "Select";
selectButton.addEventListener("click", function(e) {
e.stopPropagation();
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
});
return selectButton;
}
static hideNestedRows(parentId) {
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
rows.forEach(row => {
row.classList.add("nested");
const childId = row.getAttribute('id');
if (childId) {
CostUI.hideNestedRows(childId);
}
});
}
static showNestedRows(parentId) {
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
rows.forEach(row => {
row.classList.remove("nested");
const childId = row.getAttribute('id');
if (childId && CostUI.isRowExpanded(childId)) {
CostUI.showNestedRows(childId);
}
});
}
static contractExpandRow(id) {
const rows = document.querySelectorAll(`[parent-id='${id}']`);
if (rows.length === 0) {
return;
}
let isVisible = false;
rows.forEach(row => {
if (!row.classList.contains("nested")) {
isVisible = true;
}
});
if (isVisible) {
CostUI.hideNestedRows(id);
this.textContent = ">";
CostUI.updateExpandedState(id, false);
} else {
CostUI.showNestedRows(id);
this.textContent = "^";
CostUI.updateExpandedState(id, true);
}
}
static updateExpandedState(id, isExpanded) {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
expandedState[id] = isExpanded;
localStorage.setItem('expandedState', JSON.stringify(expandedState));
}
static applyExpandedState() {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
Object.keys(expandedState).forEach(id => {
if (expandedState[id]) {
CostUI.showNestedRows(id);
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
if (toggleButton) {
toggleButton.textContent = "^";
}
} else {
CostUI.hideNestedRows(id);
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
if (toggleButton) {
toggleButton.textContent = ">";
}
}
});
}
static isRowExpanded(id) {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
return expandedState[id] || false;
}
static text(label) {
const text = document.createElement("p");
text.textContent = label;
return text;
}
static createCard(title, mainContainer, callback) {
const card = document.createElement("div");
card.classList.add("card");
const cardBody = document.createElement("div");
cardBody.classList.add("card-body");
const cardTitle = document.createElement("h5");
cardTitle.classList.add("card-title");
cardTitle.textContent = title;
const cardButton = document.createElement("button");
cardButton.classList.add("btn", "btn-primary");
cardButton.textContent = "Load";
cardButton.addEventListener("click", callback);
cardBody.appendChild(cardTitle);
cardBody.appendChild(mainContainer);
cardBody.appendChild(cardButton);
card.appendChild(cardBody);
return card;
}
static createCostValuesForm({ costItemId, costValues, callbacks }) {
// Check if the form already exists and remove it if it does
const formId = "cost-values-form-" + costItemId;
let existingForm = document.getElementById(formId);
if (existingForm) {
existingForm.remove();
}
const form = CostUI.createFormElement(formId, "cost-values-form");
const header = CostUI.createHeader("Cost Values Form");
form.appendChild(header);
const table = CostUI.createCostValuesTable(costItemId, ["Type", "Category", "Value"]);
costValues.forEach(costValue => {
const tr = CostUI.createCostvaluesRow(costItemId, costValue);
table.appendChild(tr);
});
form.appendChild(table);
const addButton = CostUI.createAddCostValueButton(costItemId, callbacks);
form.appendChild(addButton);
const submitButton = CostUI.createSubmitButton(callbacks);
form.appendChild(submitButton);
const closeButton = CostUI.createCloseButton(form);
form.appendChild(closeButton);
CostUI.makeHeaderDraggable(header, form);
document.body.appendChild(form);
return form;
}
static getCostValuesTable(costItemId) {
return document.getElementById("cost-values-table-" + costItemId);
}
static addNewCostValueRow(costItemId, costValueId) {
const table = CostUI.getCostValuesTable(costItemId);
// check if table exists
if (!table) {
console.log("Cost values table not found for cost item ID:", costItemId);
return;
}
console.log("Adding new cost value row", costValueId);
console.log(table)
const tr = CostUI.createCostvaluesRow(costItemId, {"category": "", "name": "", "applied_value": 0, "id": costValueId, "parent": costItemId});
table.appendChild(tr);
}
static createCostValuesTable(costItemId, headers) {
const table = document.createElement("table");
table.classList.add("cost-values-table");
table.id = "cost-values-table-" + costItemId;
const headerRow = document.createElement("tr");
headers.forEach(headerText => {
const th = document.createElement("th");
th.textContent = headerText;
headerRow.appendChild(th);
});
table.appendChild(headerRow);
return table;
}
static createCostvaluesRow(costItemId,costValue) {
function cleanLabel(value) {
// remove anything which is not a dot or a digit
return value.replace(/[^0-9.]/g, '');
}
if (document.getElementById(costValue.id)) {
return document.getElementById(costValue.id);
}
const tr = document.createElement("tr");
tr.isCostValue = true;
tr.parent = costItemId;
tr.id = costValue.id;
let costType = "FIXED";
if (costValue.category === "*") {
costType = "SUM";
}
else if (costValue.category && costValue.category !== "*") {
costType = "CATEGORY";
}
else if (costValue.applied_value) {
costType = "FIXED";
}
const typeCell = CostUI.createTableDropdown("type", costType);
const dropdown = typeCell.querySelector("select");
dropdown.addEventListener("change", function() {
const selectedType = this.value;
CostUI.updateRowBasedOnType(selectedType, categoryCell, valueCell1);
});
tr.appendChild(typeCell);
const categoryCell = CostUI.createTableInput("text", "category", costValue.category);
tr.appendChild(categoryCell);
let value
if (costValue.category === "*"){
value = cleanLabel(costValue.label)
}
else {
value = costValue.applied_value
}
const valueCell1 = CostUI.createTableInput("number", "value", value);
tr.appendChild(valueCell1);
// Apply initial state based on costType
CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1);
return tr;
}
static updateRowBasedOnType(type, categoryCell, valueCell1) {
if (type === "FIXED") {
categoryCell.querySelector("input").disabled = true;
categoryCell.querySelector("input").style.backgroundColor = "lightgrey";
valueCell1.querySelector("input").disabled = false;
valueCell1.querySelector("input").style.backgroundColor = "";
} else if (type === "CATEGORY") {
categoryCell.querySelector("input").disabled = false;
categoryCell.querySelector("input").style.backgroundColor = "";
valueCell1.querySelector("input").disabled = false;
valueCell1.querySelector("input").style.backgroundColor = "";
} else if (type === "SUM") {
categoryCell.querySelector("input").disabled = true;
categoryCell.querySelector("input").style.backgroundColor = "lightgrey";
valueCell1.querySelector("input").disabled = true;
valueCell1.querySelector("input").style.backgroundColor = "lightgrey";
}
}
static createTableDropdown(name, value="FIXED") {
const cell = document.createElement("td");
const dropdown = document.createElement("select");
dropdown.name = name;
const options = ["FIXED", "CATEGORY", "SUM"];
options.forEach(optionValue => {
const option = document.createElement("option");
option.value = optionValue;
option.textContent = optionValue;
if (optionValue === value) {
option.selected = true;
}
dropdown.appendChild(option);
});
cell.appendChild(dropdown);
return cell;
}
static createTableInput(type, name, value) {
const cell = document.createElement("td");
const input = document.createElement("input");
input.type = type;
input.name = name;
input.value = value;
cell.appendChild(input);
return cell;
}
static createAddCostValueButton(costItemId, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
addButton.addEventListener("click", function(e) {
e.preventDefault();
callbacks.addCostValue ? callbacks.addCostValue(costItemId) : null;
});
return addButton;
}
static createSubmitButton(callbacks) {
const submitButton = document.createElement("button");
submitButton.type = "submit";
submitButton.textContent = "Save";
submitButton.addEventListener("click", function(e) {
e.preventDefault();
const form = this.closest("form");
const costValues = [];
const rows = Array.from(form.querySelectorAll("tr")).filter(row => row.isCostValue);
rows.forEach(row => {
const costCategory = row.querySelector("input[name='category']").value;
const appliedValue = parseFloat(row.querySelector("input[name='value']").value);
const costType = row.querySelector("select[name='type']").value;
const id = parseInt(row.id);
costValues.push({ costType, costCategory, appliedValue, id: id, costItemId: row.parent });
});
const costItemId = parseInt(form.id.split("-")[3]);
console.log(form.id)
console.log("Cost values to be saved:", costItemId);
callbacks.editCostValues ? callbacks.editCostValues(costItemId, costValues) : null;
form.remove();
});
return submitButton;
}
static createFormElement(id, className) {
const form = document.createElement("form");
form.id = id;
form.classList.add(className);
form.style.position = "absolute"; // Set initial position to absolute
form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
return form;
}
static createHeader(text) {
const header = document.createElement("div");
header.classList.add("form-header");
header.textContent = text;
return header;
}
static createCloseButton(form) {
const closeButton = document.createElement("span");
closeButton.classList.add("close-button");
closeButton.innerHTML = "&times;";
closeButton.addEventListener("click", function() {
form.remove();
});
return closeButton;
}
static makeHeaderDraggable(header, form) {
header.addEventListener("mousedown", function(e) {
let offsetX = e.clientX - form.getBoundingClientRect().left;
let offsetY = e.clientY - form.getBoundingClientRect().top;
function mouseMoveHandler(e) {
form.style.left = `${e.clientX - offsetX}px`;
form.style.top = `${e.clientY - offsetY}px`;
}
function mouseUpHandler() {
document.removeEventListener("mousemove", mouseMoveHandler);
document.removeEventListener("mouseup", mouseUpHandler);
}
document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler);
});
}
}
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en" class="blender">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<link rel="stylesheet" type="text/css" href="/static/css/gantt.css" id="index-stylesheet" />
<link
rel="stylesheet"
id="tabulator-stylesheet"
href="https://unpkg.com/tabulator-tables/dist/css/tabulator_site_dark.min.css"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
src="https://unpkg.com/tabulator-tables/dist/js/tabulator.min.js"
></script>
<script
type="text/javascript"
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
></script>
<script>
var SOCKET_PORT = {{port}};
</script>
<script type="module" defer src="./static/js/cost.js"></script>
<style>
table {
width: 100%;
border-collapse: collapse;
}
.nested {
display: none;
}
.expand-collapse {
cursor: pointer;
}
.caret-cell {
border: none;
background: none;
padding: 0;
}
td {
justify-content: space-between;
}
.flex-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>
</head>
<body>
<nav>
<img
src="https://bonsaibim.org/assets/images/blender/blender-logo.png"
alt="Logo"
class="logo"
/>
<ul>
<li>
<a href="/"
><i class="fa-solid fa-table"></i> Schedules</a
>
</li>
<li>
<a href="/costing" class="active"
><i class="fa-solid fa-table"></i>Costing</a
>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
<li>
<a><i class="fa-solid fa-camera"></i> BCF Topics</a>
</li>
<li>
<a><i class="fa-solid fa-square-check"></i> IDS Audits</a>
</li>
<li>
<a><i class="fa-solid fa-hotel"></i> Facility Management</a>
</li>
</ul>
<button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
</div>
<div id="cost-schedules"></div>
<div id="UI"></div>
<div id="cost-items"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
</footer>
</body>
</html>
@@ -0,0 +1,273 @@
<!-- this is a demo web UI page -->
<!DOCTYPE html>
<html lang="en" class="blender">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bonsai Web UI</title>
<link
rel="icon"
type="image/x-icon"
href="https://bonsaibim.org/assets/images/favicon-blender.png"
/>
<!-- here we request the CSS file from the server, -->
<!-- using registered static path in the server -->
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
></script>
<script>
// here we define the port that the server is listening on.
// so that we can connect to the websocket server and request static files
// this is set when the HTML file is served by the server when opening the web UI URL.
var SOCKET_PORT = {{port}};
</script>
<!-- here we request the JS file from the server-->
<!-- using registered static path in the server -->
<script defer src="./static/js/demo.js"></script>
</head>
<body>
<!-- the navigation bar at the top of the page. -->
<nav>
<img
src="https://bonsaibim.org/assets/images/blender/blender-logo.png"
alt="Logo"
class="logo"
/>
<ul>
<li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
<li>
<a><i class="fa-solid fa-camera"></i> BCF Topics</a>
</li>
<li>
<a><i class="fa-solid fa-square-check"></i> IDS Audits</a>
</li>
<li>
<a><i class="fa-solid fa-hotel"></i> Facility Management</a>
</li>
</ul>
<button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</nav>
<!-- the main page container. -->
<div id="container">
<div id="message-container">
<!-- Here we have a text input and a button that calls the SendMessage() function-->
<input type="text" id="input-message" placeholder="Send a Message" />
<button onclick="sendMessage()">Send Message to Bonsai</button>
</div>
<!-- The following is the explanation part of the demo page. -->
<!-- I suggest reading it from the demo page iteself :) -->
<!-- we need to stop formating because formatiing messes up pre tag spacing -->
<!-- which in turn messes up code blocks display-->
<!-- prettier-ignore-start -->
<div id="explanation">
<h1 id="welcome-to-the-web-ui-demo-page">Welcome to the web UI demo page!</h1>
<p>This demo showcases how Bonsai's Web UI interacts with a WebSocket server. The demo illustrates how the Web UI establishes a connection, exchanges data with the server, and updates the UI dynamically based on incoming data.</p>
<p>You can use the above textbox to send a message to Bonsai which will be printed to the console!</p>
<p>If you prefer, you can explore the source code directly in the data/webui directory:</p>
<ul>
<li>sioserver.py</li>
<li>templates/demo.html</li>
<li>static/js/demo.js</li>
</ul>
<p>Each file contains detailed comments to help you understand the code. Alternatively, continue reading this demo page to understand how the Web UI operates</p>
<h2 id="webscoket-server-sioserverpy">WebScoket Server (sioserver.py)</h2>
<p>In the websocket server, there are two key namespaces.</p>
<ul>
<li>Blender Namespace: Connects all Bonsai instances.</li>
<li>Web Namespace: Connects all Web UI clients.</li>
</ul>
<p>These namespaces are attached to the WebSocket server, allowing event-driven communication.</p>
<pre class="hljs"><code><div><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebNamespace</span><span class="hljs-params">(socketio.AsyncNamespace)</span>:</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_connect</span><span class="hljs-params">(self, sid, environ)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_disconnect</span><span class="hljs-params">(self, sid)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_web_operator</span><span class="hljs-params">(self, sid, data)</span>:</span>
...
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlenderNamespace</span><span class="hljs-params">(socketio.AsyncNamespace)</span>:</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_connect</span><span class="hljs-params">(self, sid, environ)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_disconnect</span><span class="hljs-params">(self, sid)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_demo_data</span><span class="hljs-params">(self, sid, data)</span>:</span>
...
</div></code></pre>
<p>Each namespace defines event handlers that are triggered when a connected client emits specific events. For example, the on_demo_data function is invoked when a client emits the demo_data event.</p>
<p>The server also defines routes, such as the /demo URL, which are tied to specific request handlers.</p>
<pre class="hljs"><code><div><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">demo</span><span class="hljs-params">(request)</span>:</span>
<span class="hljs-keyword">with</span> open(<span class="hljs-string">"templates/demo.html"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:
template = f.read()
html_content = pystache.render(template, {<span class="hljs-string">"port"</span>: sio_port, <span class="hljs-string">"version"</span>: bonsai_version})
<span class="hljs-keyword">return</span> web.Response(text=html_content, content_type=<span class="hljs-string">"text/html"</span>)
app.router.add_get(<span class="hljs-string">"/demo"</span>, demo)
</div></code></pre>
<p>In this example, when the /demo URL is accessed, the server reads the demo.html template, renders it with variables like port and version, and returns the fully rendered HTML to the client's browser.</p>
<h2 id="html-file-templatesdemohtml">HTML File (templates/demo.html)</h2>
<p>When the Web UI URL is visited, the server follows these steps:</p>
<ul>
<li>Retrieves the HTML file from the templates directory.</li>
<li>uses pystache to render variables, such as SOCKET_PORT, into the HTML template.</li>
<li>Sends the fully rendered HTML page to the web browser.</li>
</ul>
<p>The SOCKET_PORT variable is injected into the HTML and later used by the JavaScript file to establish a WebSocket connection.</p>
<pre class="hljs"><code><div><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="actionscript">
<span class="hljs-keyword">var</span> SOCKET_PORT = {{port}};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</div></code></pre>
<p>After the HTML page loads, the browser automatically requests static files, such as CSS and JavaScript, from the static directory specified in the sioserver.py file.</p>
<pre class="hljs"><code><div><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/static/css/demo.css"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"demo-stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">defer</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./static/js/demo.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</div></code></pre>
<h2 id="javascript-file-staticjsdemojs">JavaScript File (static/js/demo.js)</h2>
<p>The JavaScript file is responsible for establishing and managing the WebSocket connection. It starts by ensuring the DOM is fully loaded before executing any scripts.</p>
<pre class="hljs"><code><div><span class="hljs-keyword">let</span> socket;
$(<span class="hljs-built_in">document</span>).ready(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">var</span> defaultTheme = <span class="hljs-string">"blender"</span>; <span class="hljs-comment">// Default theme to be applied</span>
<span class="hljs-keyword">var</span> theme = localStorage.getItem(<span class="hljs-string">"theme"</span>) || defaultTheme; <span class="hljs-comment">// Retrieve the stored theme or use the default</span>
setTheme(theme); <span class="hljs-comment">// Apply the theme</span>
connectSocket(); <span class="hljs-comment">// Establish WebSocket connection</span>
});
</div></code></pre>
<p>Upon page load, the script sets the theme (either from local storage or the default) and then calls connectSocket function to initiate the WebSocket connection.</p>
<h3 id="establishing-websocket-connection">Establishing WebSocket connection</h3>
<p>The connectSocket function constructs the WebSocket connection URL using the SOCKET_PORT variable and then initializes the connection.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">connectSocket</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> url = <span class="hljs-string">"ws://localhost:"</span> + SOCKET_PORT + <span class="hljs-string">"/web"</span>;
socket = io(url);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"socket: "</span>, socket);
<span class="hljs-comment">// Register socket event handlers</span>
socket.on(<span class="hljs-string">"blender_connect"</span>, handleBlenderConnect);
socket.on(<span class="hljs-string">"blender_disconnect"</span>, handleBlenderDisconnect);
socket.on(<span class="hljs-string">"theme_data"</span>, handleThemeData);
socket.on(<span class="hljs-string">"demo_data"</span>, handleDemoData);
}
</div></code></pre>
<p>The socket variable is used to manage the WebSocket connection within the web namespace. After establishing the connection, the script registers event handlers for specific events emitted by the server.</p>
<h3 id="receiving-data-from-bonsai">Receiving Data from Bonsai</h3>
<p>Event handlers process the data received from the server and manipulate the DOM to reflect the changes on the webpage. For instance, the handleDemoData function processes the demo_data event and updates the demo content on the page.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleDemoData</span>(<span class="hljs-params">demoData</span>) </span>{
<span class="hljs-built_in">console</span>.log(demoData);
<span class="hljs-keyword">const</span> message = demoData[<span class="hljs-string">"data"</span>][<span class="hljs-string">"demo_message"</span>];
<span class="hljs-keyword">const</span> blenderId = demoData[<span class="hljs-string">"blenderId"</span>];
<span class="hljs-keyword">const</span> id = <span class="hljs-string">"message-"</span> + blenderId;
<span class="hljs-keyword">const</span> messageHeader = $(<span class="hljs-string">"#"</span> + id);
<span class="hljs-keyword">const</span> messageText = <span class="hljs-string">`Bonsai instance with ID: <span class="hljs-subst">${blenderId}</span> sent the message: <span class="hljs-subst">${message}</span>`</span>;
<span class="hljs-keyword">if</span> (messageHeader[<span class="hljs-number">0</span>] === <span class="hljs-literal">undefined</span>) {
<span class="hljs-keyword">const</span> newMessageElement = $(<span class="hljs-string">"&lt;h3&gt;"</span>, { <span class="hljs-attr">id</span>: id }).text(messageText);
newMessageElement.prependTo(<span class="hljs-string">"#message-container"</span>);
} <span class="hljs-keyword">else</span> {
messageHeader.text(messageText);
messageHeader.prependTo(<span class="hljs-string">"#message-container"</span>);
}
}
</div></code></pre>
<p>we use the BlenderId, which is a unique identifier given for by the server for each client connedted to it, to differentiate between different Bonsai instances.</p>
<p>This functionality is repeated for other event handlers, such as handleBlenderConnect, handleBlenderDisconnect, and handleThemeData, where specific actions are taken based on the event type and the data provided by the server.</p>
<h3 id="sending-data-to-bonsai">Sending Data to Bonsai</h3>
<p>Data is sent to the server in response to DOM events, such as onclick. These events trigger functions that use the socket variable to emit an event to the server, passing the appropriate data for the specific web operator.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SendMessage</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> inputMessage = $(<span class="hljs-string">"#input-message"</span>).val();
<span class="hljs-keyword">const</span> msg = {
<span class="hljs-attr">sourcePage</span>: <span class="hljs-string">"demo"</span>,
<span class="hljs-comment">// blenderId: BlenderId,</span>
<span class="hljs-attr">operator</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"message"</span>,
<span class="hljs-attr">message</span>: inputMessage,
},
};
socket.emit(<span class="hljs-string">"web_operator"</span>, msg);
}
</div></code></pre>
<p>In this example, the blenderId field specifies which Bonsai instance should receive the operator. If blenderId is not set, the operator is broadcast to all instances.</p>
<p>The server forwards this operator to the appropriate Bonsai instance, where it is processed by functions like sio_listen_web_operator and check_operator_queue in tool/web.py.</p>
<h2 id="web-tool-toolwebpy">Web Tool (tool/web.py)</h2>
<p>the sio_listen_web_operator function is automatically called when the event web_operator is emitted to Bonsai. It takes the web operator and attempts to put it in a web operators queue.</p>
<pre class="hljs"><code><div><span class="hljs-meta">@classmethod</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sio_listen_web_operator</span><span class="hljs-params">(cls, data)</span>:</span>
<span class="hljs-keyword">try</span>:
web_operator_queue.put_nowait(data)
<span class="hljs-keyword">except</span> queue.Full:
<span class="hljs-keyword">pass</span>
</div></code></pre>
<p>then the check_operator_queue function is called by a timer that is run every second to retrieve operators from the queue and handle them</p>
<pre class="hljs"><code><div><span class="hljs-meta">@classmethod</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">check_operator_queue</span><span class="hljs-params">(cls)</span>:</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> bpy.context.scene.WebProperties.is_connected:
<span class="hljs-keyword">with</span> web_operator_queue.mutex:
web_operator_queue.queue.clear()
<span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-comment"># unregister timer if not connected</span>
<span class="hljs-keyword">while</span> <span class="hljs-keyword">not</span> web_operator_queue.empty():
operator = web_operator_queue.get_nowait()
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> operator:
<span class="hljs-keyword">continue</span>
<span class="hljs-keyword">if</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"csv"</span>:
cls.handle_csv_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"gantt"</span>:
cls.handle_gantt_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"drawings"</span>:
cls.handle_drawings_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"demo"</span>:
message = operator[<span class="hljs-string">"operator"</span>][<span class="hljs-string">"message"</span>]
print(<span class="hljs-string">f"Message from demo page: <span class="hljs-subst">{message}</span>"</span>)
<span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span>
</div></code></pre>
<p>Here we check the source page of the operator and call the appropriate handler for that page.</p>
</div>
<!-- prettier-ignore-end -->
</div>
<footer>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,12 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link
rel="icon"
type="image/x-icon"
href="https://bonsaibim.org/assets/images/favicon-blender.png"
/>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
@@ -53,6 +58,11 @@
<li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
@@ -149,7 +159,7 @@
</div>
</div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,12 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link
rel="icon"
type="image/x-icon"
href="https://bonsaibim.org/assets/images/favicon-blender.png"
/>
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
<link rel="stylesheet" href="/static/css/gantt.css" id="gantt-stylesheet" />
<link
@@ -22,7 +27,7 @@
<script>
var SOCKET_PORT = {{port}};
</script>
<script defer src="./static/js/gantt.js"></script>
<script type="module" defer src="./static/js/gantt.js"></script>
</head>
<body>
<nav class="no-print">
@@ -35,6 +40,11 @@
<li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li>
<a href="/sequencing" class="active"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
@@ -71,9 +81,10 @@
</button>
<div id="client-list" class="no-print"></div>
</div>
<div id="work_schedules"></div>
<div id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,12 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link
rel="icon"
type="image/x-icon"
href="https://bonsaibim.org/assets/images/favicon-blender.png"
/>
<link rel="stylesheet" href="/static/css/index.css" id="index-stylesheet" />
<link
rel="stylesheet"
@@ -44,6 +49,11 @@
><i class="fa-solid fa-table"></i> Schedules</a
>
</li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
@@ -78,7 +88,7 @@
</div>
<div id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
+31 -9
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import bpy
import uuid
@@ -116,17 +117,34 @@ class IfcStore:
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
cache_preexists = cache_path.exists()
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
except:
if os.path.exists(IfcStore.cache_path):
os.remove(IfcStore.cache_path)
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
except:
return
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
if cache_preexists:
print(f"Successfully loaded existing cache: {cache_path.name}.")
else:
print("New cache was created.")
except Exception as e:
if cache_preexists:
print(f"Failed to create a cache from existing file '{cache_path.name}': {str(e)}.")
else:
print(f"Failed to create a cache: {str(e)}.")
# No point to trying again the same operation.
return
os.remove(IfcStore.cache_path)
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
print("New cache was created.")
except Exception as e:
print(f"Failed to create a cache: {str(e)}.")
return
return IfcStore.cache
@@ -134,6 +152,8 @@ class IfcStore:
def update_cache():
if not IfcStore.cache:
return
assert IfcStore.cache_path
assert IfcStore.file
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
@@ -228,7 +248,9 @@ class IfcStore:
IfcStore.commit_link_element(data)
@staticmethod
def link_element(element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
def link_element(
element: ifcopenshell.entity_instance, obj: Union[IFC_CONNECTED_TYPE, tool.Geometry.TYPES_WITH_MESH_PROPERTIES]
) -> None:
# Please use tool.Ifc.link() instead of this method. We want to
# refactor this class and deprecate usage of IfcStore in favour of
# tools.
+19 -23
View File
@@ -715,9 +715,17 @@ class IfcImporter:
if not products:
return results
if tool.Loader.settings.should_use_cpu_multiprocessing:
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products)
iterator = ifcopenshell.geom.iterator(
settings,
self.file,
multiprocessing.cpu_count(),
include=products,
geometry_library=self.ifc_import_settings.geometry_library,
)
else:
iterator = ifcopenshell.geom.iterator(settings, self.file, include=products)
iterator = ifcopenshell.geom.iterator(
settings, self.file, include=products, geometry_library=self.ifc_import_settings.geometry_library
)
if self.ifc_import_settings.should_cache:
cache = IfcStore.get_cache()
if cache:
@@ -785,7 +793,9 @@ class IfcImporter:
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, placement_matrix))
self.link_element(product, obj)
def get_pointcloud_representation(self, product):
def get_pointcloud_representation(
self, product: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
if hasattr(product, "Representation") and hasattr(product.Representation, "Representations"):
representations = product.Representation.Representations
elif hasattr(product, "RepresentationMaps") and hasattr(product.RepresentationMaps, "RepresentationMaps"):
@@ -825,28 +835,12 @@ class IfcImporter:
def create_pointcloud(
self, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
placement_matrix = self.get_element_matrix(product)
vertex_list = []
for item in representation.Items:
if item.is_a("IfcCartesianPointList3D"):
vertex_list.extend(
mathutils.Vector(list(coordinates)) * self.unit_scale for coordinates in item.CoordList
)
elif item.is_a("IfcCartesianPointList2D"):
vertex_list.extend(
mathutils.Vector(list(coordinates)).to_3d() * self.unit_scale for coordinates in item.CoordList
)
elif item.is_a("IfcCartesianPoint"):
vertex_list.append(mathutils.Vector(list(item.Coordinates)) * self.unit_scale)
if len(vertex_list) == 0:
return None
mesh_name = tool.Geometry.get_representation_name(representation)
mesh = bpy.data.meshes.new(mesh_name)
mesh.from_pydata(vertex_list, [], [])
mesh = tool.Loader.create_point_cloud_mesh(representation)
if mesh is None:
return
tool.Ifc.link(representation, mesh)
placement_matrix = self.get_element_matrix(product)
obj = bpy.data.objects.new(tool.Loader.get_name(product), mesh)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, placement_matrix))
self.link_element(product, obj)
@@ -1487,6 +1481,7 @@ class IfcImportSettings:
self.logger: logging.Logger = None
self.input_file = None
self.diff_file = None
self.geometry_library = "opencascade"
self.should_use_cpu_multiprocessing = True
self.should_merge_materials_by_colour = False
self.should_load_geometry = True
@@ -1522,6 +1517,7 @@ class IfcImportSettings:
logger = logging.getLogger("ImportIFC")
settings.logger = logger
settings.diff_file = scene_diff.diff_json_file
settings.geometry_library = props.geometry_library
settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
settings.should_load_geometry = props.should_load_geometry
@@ -32,7 +32,6 @@ classes = (
operator.AddBcfViewpoint,
operator.BCFFileHandlerOperator,
operator.BIM_FH_import_bcf,
operator.EditBcfAuthor,
operator.EditBcfComment,
operator.EditBcfLabels,
operator.EditBcfProjectName,
+9 -1
View File
@@ -46,7 +46,15 @@ class BcfStore:
@classmethod
def set(cls, bcfxml: Union[bcf.bcfxml.BcfXml, None], filepath: str) -> None:
cls.bcfxml = bcfxml
bpy.context.scene.BCFProperties.bcf_file = filepath
props = bpy.context.scene.BCFProperties
props.bcf_file = filepath
# Set bcf_version prop on load.
if filepath or bcfxml:
bcfxml = cls.get_bcfxml()
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props.bcf_version = "2" if bcf_v2 else "3"
@classmethod
def set_by_filepath(cls, filepath: str) -> None:
File diff suppressed because it is too large Load Diff
+29 -9
View File
@@ -59,11 +59,6 @@ def updateBcfProjectName(self, context):
bpy.ops.bim.edit_bcf_project_name()
def updateBcfAuthor(self, context):
if bcfstore.BcfStore.get_bcfxml():
bpy.ops.bim.edit_bcf_author()
def updateBcfTopicName(self, context):
if bcfstore.BcfStore.get_bcfxml():
bpy.ops.bim.edit_bcf_topic_name()
@@ -189,11 +184,36 @@ class BcfTopic(PropertyGroup):
is_editable: BoolProperty(name="Edit Topic Attributes", default=False, update=updateBcfTopicIsEditable)
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
props = self
active_topic = props.active_topic
active_related_topics = active_topic.related_topics.keys()
enum_items = []
i = 0
for t in props.topics:
if t.name == active_topic.name:
continue
if t.name in active_related_topics:
continue
enum_items.append((t.name, t.title, t.description))
return enum_items
class BCFProperties(PropertyGroup):
bcf_file: StringProperty(name="BCF File")
bcf_version: EnumProperty(
name="BCF Version",
description="Currently loaded BCF project version / BCF version to use for created projects",
items=[("2", "v2.1", ""), ("3", "v3.0", "")],
default="3",
)
comment_text_width: IntProperty(name="Comment Text Width", default=40)
name: StringProperty(default="", name="Project Name", update=updateBcfProjectName)
author: StringProperty(default="john@doe.com", name="Author Email", update=updateBcfAuthor)
author: StringProperty(
default="john@doe.com",
name="Author Email",
description="Author name that will be used for added comments, topics",
)
topics: CollectionProperty(name="BCF Topics", type=BcfTopic)
active_topic_index: IntProperty(name="Active BCF Topic Index", update=refreshBcfTopic)
file_reference: StringProperty(default="", name="Reference")
@@ -203,14 +223,15 @@ class BCFProperties(PropertyGroup):
label: StringProperty(
default="", name="Label", search=lambda s, c, t: get_extensions_items(s, c, t, extensions_attr="topic_labels")
)
bim_snippet_reference: StringProperty(default="", name="Reference")
bim_snippet_reference: StringProperty(default="", name="Reference", description="URI or filepath to BimSnippet")
bim_snippet_type: StringProperty(
default="", name="Type", search=lambda s, c, t: get_extensions_items(s, c, t, extensions_attr="snippet_types")
)
bim_snippet_schema: StringProperty(default="", name="Schema")
document_reference: StringProperty(default="", name="Referenced Document")
document_reference_description: StringProperty(default="", name="Description")
related_topic: StringProperty(name="Related Topic")
document_description: StringProperty(default="", name="Document Description")
related_topic: EnumProperty(name="Related Topic", items=get_related_topics)
comment: StringProperty(default="", name="Comment")
has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False)
@@ -225,7 +246,6 @@ class BCFProperties(PropertyGroup):
self.bim_snippet_schema = ""
self.document_reference = ""
self.document_reference_description = ""
self.related_topic = ""
self.comment = ""
self.has_related_viewpoint = False
+34 -9
View File
@@ -44,12 +44,19 @@ class BIM_PT_bcf(Panel):
row = layout.row(align=True)
row.operator("bim.new_bcf_project", text="New Project")
row.operator("bim.load_bcf_project", text="Load Project")
layout.prop(props, "bcf_version")
return
row = layout.row(align=True)
row.operator("bim.save_bcf_project", icon="EXPORT", text="Save Project")
op = row.operator("bim.save_bcf_project", icon="FILE_TICK", text="Save Current Project")
op.save_current_bcf = True
row.operator("bim.save_bcf_project", icon="EXPORT", text="Save Project As...")
row.operator("bim.unload_bcf_project", text="", icon="CANCEL")
row = layout.row()
row.prop(props, "bcf_version", emboss=False)
row.enabled = False
row = layout.row()
row.prop(props, "name")
@@ -116,6 +123,10 @@ class BIM_PT_bcf_metadata(Panel):
bl_parent_id = "BIM_PT_bcf"
def draw(self, context):
# TODO: in bcf v3 documents can be added to the bcf
# without adding them to specific topic.
# Currently we just handle it the same way as in v2
# documents are accessed and managed from topics.
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
@@ -127,6 +138,8 @@ class BIM_PT_bcf_metadata(Panel):
if not bcfxml or props.active_topic_index >= len(props.topics):
layout.label(text="No BCF project is loaded")
return
bcf_verison = bcfxml.version.version_id or ""
bcf_v3 = bcf_verison.startswith("3")
topic = props.active_topic
bcf_topic = bcfxml.topics[topic.name]
@@ -195,7 +208,7 @@ class BIM_PT_bcf_metadata(Panel):
row.operator("bim.add_bcf_label")
layout.label(text="BIM Snippet:")
if topic.bim_snippet.schema:
if topic.bim_snippet.reference:
row = layout.row(align=True)
row.prop(topic.bim_snippet, "type", emboss=False)
if topic.bim_snippet.schema:
@@ -224,7 +237,7 @@ class BIM_PT_bcf_metadata(Panel):
for index, doc in enumerate(topic.document_references):
box = self.layout.box()
row = box.row(align=True)
row.prop(doc, "reference")
row.prop(doc, "reference", emboss=False)
if doc.is_external:
row.operator("bim.open_uri", icon="URL", text="").uri = doc.reference
else:
@@ -234,12 +247,17 @@ class BIM_PT_bcf_metadata(Panel):
row.operator("bim.remove_bcf_document_reference", icon="X", text="").index = index
row = box.row(align=True)
row.prop(doc, "description")
row.prop(doc, "description", emboss=False)
row = layout.row(align=True)
row.prop(props, "document_reference")
row.operator("bim.select_bcf_document_reference", icon="FILE_FOLDER", text="")
row = layout.row()
row.prop(props, "document_reference_description")
row.prop(
props, "document_reference_description", text="Reference Description" if bcf_v3 else "Document Description"
)
if bcf_v3:
row = layout.row()
row.prop(props, "document_description")
row = layout.row()
row.operator("bim.add_bcf_document_reference")
@@ -254,10 +272,14 @@ class BIM_PT_bcf_metadata(Panel):
row.operator("bim.remove_bcf_related_topic", icon="X", text="").index = index
except KeyError:
pass
row = layout.row()
row.prop(props, "related_topic")
row = layout.row()
row.operator("bim.add_bcf_related_topic")
if len(props.topics) == len(topic.related_topics) + 1:
layout.label(text="No topics to add as related.")
else:
row = layout.row()
row.prop(props, "related_topic")
row = layout.row()
row.operator("bim.add_bcf_related_topic")
class BIM_PT_bcf_comments(Panel):
@@ -293,6 +315,9 @@ class BIM_PT_bcf_comments(Panel):
author_text = "*{} ({})".format(comment.modified_author, comment.modified_date)
row = box.row(align=True)
row.label(text=author_text, icon="WORDWRAP_ON")
if comment.viewpoint:
op = row.operator("bim.activate_bcf_viewpoint", icon="SCENE", text="")
op.viewpoint_guid = comment.viewpoint
row.prop(
comment, "is_editable", icon="CHECKMARK" if comment.is_editable else "GREASEPENCIL", icon_only=True
)
@@ -79,6 +79,7 @@ classes = (
operator.SelectUnassignedProducts,
operator.UnassignCostItemQuantity,
operator.UnassignCostItemType,
operator.GenerateCostScheduleBrowser,
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
+2 -8
View File
@@ -265,16 +265,10 @@ class CostSchedulesData:
@classmethod
def cost_values(cls):
results = []
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
if not ifc_id:
return results
cost_item = tool.Ifc.get().by_id(ifc_id)
for cost_value in cost_item.CostValues or []:
label = "{0:.2f}".format(ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value))
label += " = {}".format(ifcopenshell.util.cost.serialise_cost_value(cost_value))
results.append({"id": cost_value.id(), "label": label, "name": cost_value.Name})
return results
return []
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@classmethod
def quantity_types(cls):
@@ -790,3 +790,14 @@ class AddCurrency(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.add_currency(tool.Ifc, tool.Cost)
class GenerateCostScheduleBrowser(bpy.types.Operator):
bl_idname = "bim.generate_cost_schedule_browser"
bl_label = "Generate Cost Schedule Browser"
bl_options = {"REGISTER", "UNDO"}
cost_schedule: bpy.props.IntProperty()
def execute(self, context):
core.generate_cost_schedule_browser(tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
return {"FINISHED"}
+2 -1
View File
@@ -73,7 +73,8 @@ class BIM_PT_cost_schedules(Panel):
row1.label(text="Schedule tools")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.operator("bim.export_cost_schedules", text="Export", icon="EXPORT").cost_schedule = cost_schedule["id"]
row1.operator("bim.export_cost_schedules", text="Export spreadsheet", icon="EXPORT").cost_schedule = cost_schedule["id"]
row1.operator("bim.generate_cost_schedule_browser", text="Generate spreadsheet browsser", icon="URL").cost_schedule = cost_schedule["id"]
row2 = col.row(align=True)
row2.alignment = "RIGHT"
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
+5 -1
View File
@@ -99,4 +99,8 @@ class CsvProperties(PropertyGroup):
should_show_group: BoolProperty(default=False, name="Show Grouping")
should_show_summary: BoolProperty(default=False, name="Show Summary")
should_show_formatting: BoolProperty(default=False, name="Show Formatting")
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
should_load_from_memory: BoolProperty(
default=False,
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
+11 -2
View File
@@ -90,11 +90,18 @@ class ConvertToBlender(bpy.types.Operator):
def execute(self, context):
for obj in bpy.data.objects:
if obj.library:
continue
if obj.type in {"MESH", "EMPTY"}:
tool.Ifc.unlink(obj=obj)
if obj.data:
obj.data.BIMMeshProperties.ifc_definition_id = 0
data = obj.data
if tool.Geometry.has_mesh_properties(data):
if data.library:
continue
data.BIMMeshProperties.ifc_definition_id = 0
for material in bpy.data.materials:
if material.library:
continue
tool.Ifc.unlink(obj=material)
context.scene.BIMProperties.ifc_file = ""
context.scene.BIMDebugProperties.attributes.clear()
@@ -433,9 +440,11 @@ class SelectExpressFile(bpy.types.Operator):
class PurgeHdf5Cache(bpy.types.Operator):
bl_idname = "bim.purge_hdf5_cache"
bl_label = "Purge HDF5 Cache"
bl_description = "Clean up HDF5 cache files except the ones that currently loaded"
def execute(self, context):
core.purge_hdf5_cache(tool.Debug)
self.report({"INFO"}, "HDF5 cache purged.")
return {"FINISHED"}
@@ -36,8 +36,10 @@ from . import ui, prop, operator
classes = (
operator.DemonstrateHelloWorld,
operator.DemonstrateRenameProject,
operator.SendWebUiDemoMessage,
prop.BIMDemoProperties,
ui.BIM_PT_demo,
ui.BIM_PT_webui_demo,
)
@@ -76,3 +76,18 @@ class DemonstrateRenameProject(bpy.types.Operator, tool.Ifc.Operator):
# properties (such as an input field) or data from the scene (like the
# actively selected object).
core.demonstrate_rename_project(tool.Ifc, tool.Demo, name=bpy.context.scene.BIMDemoProperties.name)
class SendWebUiDemoMessage(bpy.types.Operator):
bl_idname = "bim.send_webui_demo_message"
bl_label = "Send Web UI Demo Message"
bl_description = "Sends a demo message to the currently connected Web UI"
def execute(self, context):
# First, we need to make sure that we are connected to the Web UI
# We do that by checking the Web module properties' is_connected prop
# and calling the operator connect_websocket_server if we aren't connected to a Web UI
if not context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server(page="demo")
core.send_webui_demo_message(tool.Web, message=bpy.context.scene.BIMDemoProperties.webui_message)
return {"FINISHED"}
@@ -59,3 +59,4 @@ class BIMDemoProperties(PropertyGroup):
# an input text field in the ui.py.
message: StringProperty(name="Message")
show_hints: BoolProperty(name="Show Hints", default=False)
webui_message: StringProperty(name="Web UI Message", default="Hello, Web UI!")
+24
View File
@@ -130,3 +130,27 @@ class BIM_PT_demo(bpy.types.Panel):
if self.props.show_hints:
row = self.layout.row()
row.label(text="Name cannot be blank!")
class BIM_PT_webui_demo(bpy.types.Panel):
bl_label = "Web UI Demo"
bl_idname = "BIM_PT_webui_demo"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
# we specify that this panel is closed by default
bl_options = {"DEFAULT_CLOSED"}
# here we reference the demo panel to create a sub panel
# for the web UI demo
bl_parent_id = "BIM_PT_demo"
def draw(self, context):
self.props = context.scene.BIMDemoProperties
row = self.layout.row()
# we create an input field for the property webui_message
row.prop(self.props, "webui_message")
row = self.layout.row()
# here we create a button referencing the operator send_webui_message
row.operator("bim.send_webui_demo_message")
@@ -31,6 +31,7 @@ classes = (
operator.LoadParentDocument,
operator.LoadProjectDocuments,
operator.RemoveDocument,
operator.SelectDocumentObjects,
operator.UnassignDocument,
prop.Document,
prop.BIMDocumentProperties,
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell.api
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.document as core
from bonsai.bim.ifc import IfcStore
@@ -149,3 +150,25 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
if element:
core.unassign_document(tool.Ifc, product=element, document=document)
class SelectDocumentObjects(bpy.types.Operator):
bl_idname = "bim.select_document_objects"
bl_label = "Select Document Objects"
bl_options = {"REGISTER", "UNDO"}
document: bpy.props.IntProperty(name="Document ID", default=0)
def execute(self, context):
if not self.document or not (relating_document := tool.Ifc.get_entity_by_id(self.document)):
self.report({"INFO"}, f"No document found by id '{self.document}'.")
return {"FINISHED"}
i = 0
for element in ifcopenshell.util.element.get_referenced_elements(relating_document):
obj = tool.Ifc.get_object(element)
if not obj or obj not in context.selectable_objects:
continue
obj.select_set(True)
i += 1
self.report({"INFO"}, f"{i} objects selected.")
return {"FINISHED"}
@@ -66,6 +66,9 @@ class BIM_PT_documents(Panel):
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
elif self.props.documents and self.props.active_document_index < len(self.props.documents):
ifc_definition_id = self.props.documents[self.props.active_document_index].ifc_definition_id
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
ifc_definition_id
)
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
@@ -88,6 +88,7 @@ classes = (
operator.SelectAllDrawings,
operator.SelectAssignedProduct,
operator.SelectDocIfcFile,
operator.OpenDocumentationWebUi,
prop.Variable,
prop.Drawing,
prop.Document,
@@ -202,7 +202,9 @@ def format_distance(
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
if feet:
if feet is not None:
tx_dist += str(feet) + "'"
if not feet and not add_inches:
tx_dist += str(feet) + "'"
if feet and add_inches:
tx_dist += " - "
+306 -28
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import bpy
import json
import time
@@ -43,13 +42,12 @@ import bonsai.core.drawing as core
import bonsai.bim.module.drawing.svgwriter as svgwriter
import bonsai.bim.module.drawing.annotation as annotation
import bonsai.bim.module.drawing.sheeter as sheeter
import bonsai.bim.module.drawing.scheduler as scheduler
import bonsai.bim.module.drawing.helper as helper
import bonsai.bim.export_ifc
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional, Literal
from lxml import etree
from math import radians
from mathutils import Vector, Color, Matrix
from timeit import default_timer as timer
from bonsai.bim.module.drawing.prop import RasterStyleProperty, RASTER_STYLE_PROPERTIES_EXCLUDE
@@ -204,7 +202,12 @@ class CreateDrawing(bpy.types.Operator):
@classmethod
def poll(cls, context):
return bool(tool.Ifc.get() and tool.Drawing.is_drawing_active())
if not tool.Ifc.get():
return False
if not tool.Drawing.is_drawing_active():
cls.poll_message_set("No active drawing.")
return False
return True
def invoke(self, context, event):
# printing all drawings on shift+click
@@ -252,6 +255,7 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer.camera_projection = tuple(
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
)
self.svg_writer.calculate_scale()
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
@@ -264,7 +268,12 @@ class CreateDrawing(bpy.types.Operator):
with profile("Generate linework"):
if tool.Drawing.is_camera_orthographic():
linework_svg = self.generate_linework(context)
if self.camera.data.BIMCameraProperties.linework_mode == "OPENCASCADE":
linework_svg = self.generate_linework(context)
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
with profile("Generate annotation"):
if tool.Drawing.is_camera_orthographic():
@@ -471,11 +480,35 @@ class CreateDrawing(bpy.types.Operator):
geom_settings = ifcopenshell.geom.settings()
geom_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
geom_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
# See bug 5231 - offset no longer available in v0.8.0
absolute_placements = set()
placement_replacements = {}
if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
offset_z = 0.002 if target_view == "PLAN_VIEW" else -0.002
for product in self.file.by_type("IfcProduct"):
if not product.ObjectPlacement:
continue
absolute_placement = self.get_absolute_placement(product.ObjectPlacement)
if absolute_placement.is_a("IfcLocalPlacement"):
absolute_placements.add(absolute_placement)
transformation = np.eye(4)
transformation[2][3] = offset_z
# Don't use undo system in case we bork up a parent caller
for placement in absolute_placements:
old = placement.RelativePlacement
new = self.get_relative_placement(
ifc, transformation @ ifcopenshell.util.placement.get_local_placement(placement)
)
placement.RelativePlacement = new
placement_replacements[placement] = (old, new)
# offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
# geom_settings.offset = offset
geom_settings.set("context-ids", context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=drawing_elements
@@ -487,6 +520,186 @@ class CreateDrawing(bpy.types.Operator):
tree.add_element(elem)
drawing_elements -= processed
for placement, oldnew in placement_replacements.items():
old, new = oldnew
placement.RelativePlacement = old
ifcopenshell.util.element.remove_deep2(ifc, new)
placement_replacements = {}
def get_absolute_placement(self, object_placement):
if object_placement.PlacementRelTo:
return self.get_absolute_placement(object_placement.PlacementRelTo)
return object_placement
def get_relative_placement(self, ifc, m):
x = np.array((m[0][0], m[1][0], m[2][0]))
z = np.array((m[0][2], m[1][2], m[2][2]))
o = np.array((m[0][3], m[1][3], m[2][3]))
object_matrix = ifcopenshell.util.placement.a2p(o, z, x)
return self.create_ifc_axis_2_placement_3d(
ifc,
object_matrix[:, 3][0:3],
object_matrix[:, 2][0:3],
object_matrix[:, 0][0:3],
)
def create_ifc_axis_2_placement_3d(self, ifc, point, up, forward):
return self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint(point.tolist()),
self.file.createIfcDirection(up.tolist()),
self.file.createIfcDirection(forward.tolist()),
)
def generate_bisect_linework(self, context: bpy.types.Context, root):
camera_matrix_i = context.scene.camera.matrix_world.inverted()
group = root.find("{http://www.w3.org/2000/svg}g")
raw_width, raw_height = self.get_camera_dimensions()
x_offset = raw_width / 2
y_offset = raw_height / 2
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
for obj in context.visible_objects:
if obj.type != "MESH":
continue
if not (element := tool.Ifc.get_entity(obj)):
continue
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
continue
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
lines = []
for edge in edges:
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
coords = [start, end]
d = " ".join(
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
)
d = "M{}".format(d[1:])
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = d
group.append(g)
def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
svg_path = self.get_svg_path(cache_type="linework")
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
return svg_path
context.scene.render.engine = "BLENDER_WORKBENCH"
context.scene.render.use_freestyle = True
context.scene.svg_export.use_svg_export = True
linesets = context.view_layer.freestyle_settings.linesets
if len(linesets) == 1 and linesets[0].name == "LineSet":
context.view_layer.freestyle_settings.crease_angle = radians(140)
context.view_layer.freestyle_settings.use_culling = True
lineset = linesets[0]
lineset.edge_type_negation = "EXCLUSIVE"
lineset.select_silhouette = False
lineset.select_crease = False
lineset.select_border = False
lineset.select_edge_mark = False
lineset.select_contour = False
lineset.select_external_contour = False
lineset.select_material_boundary = False
lineset.select_suggestive_contour = True
lineset.select_ridge_valley = True
edge_mesh = bpy.data.meshes.new("Temp Merged Edges")
edge_obj = bpy.data.objects.new("Temp Merged Edges", edge_mesh)
context.scene.collection.objects.link(edge_obj)
edge_bm = bmesh.new()
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
for obj in bpy.context.view_layer.objects:
is_visible = obj.name in visible_object_names
obj.hide_render = not is_visible
if (
is_visible
and obj.type == "MESH"
and len(obj.data.edges)
and not len(obj.data.polygons)
and not obj.name.startswith("IfcAnnotation")
):
tmp_mesh = None
try:
tmp_mesh = obj.data.copy()
tmp_mesh.transform(obj.matrix_world)
edge_bm.from_mesh(tmp_mesh)
finally:
if tmp_mesh:
bpy.data.meshes.remove(tmp_mesh)
ret = bmesh.ops.extrude_edge_only(edge_bm, edges=edge_bm.edges)
verts_extruded = [e for e in ret["geom"] if isinstance(e, bmesh.types.BMVert)]
cam_z = self.camera.matrix_world.to_3x3() @ self.camera.data.view_frame(scene=None)[-1].normalized()
cam_z *= 0.001
for v in verts_extruded:
v.co += cam_z
edge_bm.to_mesh(edge_mesh)
edge_bm.free()
actual_path = svg_path[0:-4] + "0001.svg"
context.scene.render.filepath = svg_path[0:-4]
bpy.ops.render.render(write_still=False)
os.replace(actual_path, svg_path)
bpy.data.objects.remove(edge_obj)
bpy.data.meshes.remove(edge_mesh)
context.scene.render.use_freestyle = False
context.scene.svg_export.use_svg_export = False
tree = etree.parse(svg_path)
root = tree.getroot()
freestyle_width = float(root.attrib["width"])
freestyle_height = float(root.attrib["height"])
svg_width = self.svg_writer.width
svg_height = self.svg_writer.height
group = root.find(".//{http://www.w3.org/2000/svg}g")
group.attrib["class"] = "projection"
# Resize Freestyle to our proper width / height and purge all other attributes
for path in root.findall(".//{http://www.w3.org/2000/svg}path"):
for key in path.attrib:
if key == "fill":
continue
elif key != "d":
del path.attrib[key]
continue
d = path.attrib[key]
coords = d.strip().split()[1:]
new_d = "M"
for i in range(0, len(coords), 2):
x = float(coords[i][:-1])
y = float(coords[i + 1])
x = x / freestyle_width * svg_width
y = y / freestyle_height * svg_height
new_d += f" {x},{y}"
path.attrib["d"] = new_d
pass
self.generate_bisect_linework(context, root)
self.merge_linework_and_add_metadata(root)
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
return svg_path
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
@@ -573,10 +786,15 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
self.move_projection_to_bottom(root)
self.merge_linework_and_add_metadata(root)
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
self.merge_linework_and_add_metadata(root)
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
self.merge_linework_and_add_metadata(root)
if self.camera.data.BIMCameraProperties.calculate_shapely_surfaces:
if self.camera.data.BIMCameraProperties.fill_mode == "SHAPELY":
# shapely variant
group = root.find("{http://www.w3.org/2000/svg}g")
nm = group.attrib["{http://www.ifcopenshell.org/ns}name"]
@@ -661,7 +879,7 @@ class CreateDrawing(bpy.types.Operator):
path.set("class", " ".join(list(classes)))
group.insert(0, path)
if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces:
if self.camera.data.BIMCameraProperties.fill_mode == "SVGFILL":
results = etree.tostring(root).decode("utf8")
svg_data_1 = results
from xml.dom.minidom import parseString
@@ -826,7 +1044,6 @@ class CreateDrawing(bpy.types.Operator):
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
self.svg_writer.calculate_scale()
x_offset = self.svg_writer.raw_width / 2
y_offset = self.svg_writer.raw_height / 2
@@ -931,6 +1148,7 @@ class CreateDrawing(bpy.types.Operator):
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
for edge in bm.edges:
if not edge.is_manifold:
bm.free()
@@ -951,6 +1169,11 @@ class CreateDrawing(bpy.types.Operator):
except:
continue
def remove_cut_linework(self, root):
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" not in el.get("class", "").split():
el.getparent().remove(el)
def merge_linework_and_add_metadata(self, root):
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
if join_criteria:
@@ -1014,10 +1237,34 @@ class CreateDrawing(bpy.types.Operator):
if results:
for path in old_paths:
path.getparent().remove(path)
# polygonize_full will create polygons for everything, including
# interior "holes". As a result we do two passes. The first pass
# records polygon interior rings. The second pass uses this to
# check if the exterior ring matches an interior ring. If it does,
# it's a hole. Skip it!
interior_hashes = set()
for result in results:
for geom in result.geoms:
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
if isinstance(geom, shapely.Polygon):
for interior in geom.interiors:
# Sorted because coordinate ordering may differ,
# and frozenset because shapely sometimes emits
# duplicate coordinates.
interior_hashes.add(hash(frozenset(sorted(interior.coords))))
elif isinstance(geom, shapely.LineString):
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
path.attrib["d"] = d
for result in results:
for geom in result.geoms:
if isinstance(geom, shapely.Polygon):
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
if hash(frozenset(sorted(geom.exterior.coords))) in interior_hashes:
# This is a "hole", as its exterior perfectly matches an interior.
continue
d = (
"M"
+ " L".join([",".join([str(o) for o in co]) for co in geom.exterior.coords[0:-1]])
@@ -1029,9 +1276,7 @@ class CreateDrawing(bpy.types.Operator):
+ " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]])
+ " Z"
)
elif isinstance(geom, shapely.LineString):
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
path.attrib["d"] = d
path.attrib["d"] = d
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
@@ -1463,7 +1708,7 @@ class ActivateModel(bpy.types.Operator):
bl_idname = "bim.activate_model"
bl_label = "Activate Model"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Activates the model view"
bl_description = "Activate the model view, hide all annotations"
def execute(self, context):
dprops = bpy.context.scene.DocProperties
@@ -1471,10 +1716,23 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall()
# save current visibility statuses
# Preserve current visibility statuses for:
# - non-ifc objects
# - type product
# - annotations (so we won't unhide other drawings)
ifc_file = tool.Ifc.get()
visibility_status: dict[bpy.types.Object, bool] = {}
for obj in bpy.data.objects:
visibility_status[obj] = obj.hide_get()
element = tool.Ifc.get_entity(obj)
if not element:
hide = obj.hide_get()
elif element.is_a("IfcAnnotation"):
hide = True
elif element.is_a("IfcTypeProduct"):
hide = obj.hide_get()
else:
continue
visibility_status[obj] = hide
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
@@ -1557,7 +1815,9 @@ class ActivateDrawing(bpy.types.Operator):
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
if tool.Drawing.is_camera_orthographic():
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
tool.Drawing.show_decorations()
@@ -1698,7 +1958,10 @@ class ReloadDrawingStyles(bpy.types.Operator):
drawing_style.raster_style = json.dumps(style_data["raster_style"])
if current_style is not None:
camera_props.active_drawing_style_index = styles.index(current_style)
try:
camera_props.active_drawing_style_index = styles.index(current_style)
except ValueError:
self.report({"INFO"}, f"Could not find style {current_style} in EPset_Drawing.ShadingStyles.")
return {"FINISHED"}
@@ -1779,7 +2042,7 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if self.index:
index = int(self.index)
else:
index = context.active_object.data.BIMCameraProperties.active_drawing_style_index
index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index
scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style)
bpy.ops.bim.save_drawing_styles_data()
@@ -1875,7 +2138,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
def set_raster_style(self, context):
def set_raster_style(self, context: bpy.types.Context) -> None:
scene = context.scene # Do not remove. It is used in exec later
space = self.get_view_3d(context) # Do not remove. It is used in exec later
style = json.loads(self.drawing_style.raster_style)
@@ -1889,7 +2152,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
# Differences in Blender versions mean result in failures here
print(f"Failed to set shading style {path} to {value}")
def set_query(self, context):
def set_query(self, context: bpy.types.Context) -> None:
self.include_global_ids = []
self.exclude_global_ids = []
for ifc_file in context.scene.DocProperties.ifc_files:
@@ -1908,7 +2171,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if self.drawing_style.exclude_query:
self.parse_filter_query("EXCLUDE", context)
def parse_filter_query(self, mode, context):
def parse_filter_query(self, mode: Literal["INCLUDE", "EXCLUDE"], context: bpy.types.Context) -> None:
if mode == "INCLUDE":
objects = context.scene.objects
elif mode == "EXCLUDE":
@@ -1927,7 +2190,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if global_id in self.exclude_global_ids:
obj.hide_viewport = True # Note: this breaks alt-H
def get_view_3d(self, context):
def get_view_3d(self, context: bpy.types.Context) -> bpy.types.Space:
for area in context.screen.areas:
if area.type != "VIEW_3D":
continue
@@ -1935,6 +2198,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if space.type != "VIEW_3D":
continue
return space
assert False, "Space is not found."
class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -2678,8 +2942,9 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
filter_mode: bpy.props.StringProperty()
def _execute(self, context):
props = context.active_object.data.BIMCameraProperties
obj = bpy.context.scene.camera
assert obj
props = obj.data.BIMCameraProperties
element = tool.Ifc.get_entity(obj)
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
if self.filter_mode == "INCLUDE":
@@ -2857,3 +3122,16 @@ class ConvertSVGToDXF(bpy.types.Operator):
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
return {"FINISHED"}
class OpenDocumentationWebUi(bpy.types.Operator):
bl_idname = "bim.open_documentation_web_ui"
bl_label = "Open Documentation Web UI"
bl_description = "Open the documentation web UI page"
def execute(self, context):
if not context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server(page="documentation")
else:
bpy.ops.bim.open_web_browser(page="documentation")
return {"FINISHED"}
+39 -4
View File
@@ -68,6 +68,8 @@ def get_location_hint(self, context):
def update_diagram_scale(self, context):
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
@@ -94,6 +96,8 @@ def update_diagram_scale(self, context):
def update_is_nts(self, context):
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
@@ -220,6 +224,8 @@ def update_has_annotation(self, context):
def update_layer(self, context, name, value):
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
@@ -253,8 +259,12 @@ def update_should_draw_decorations(self, context):
continue
tool.Drawing.update_text_value(obj)
refresh_drawing_data()
if bpy.app.background:
return
decoration.DecorationsHandler.install(context)
else:
if bpy.app.background:
return
decoration.DecorationsHandler.uninstall()
@@ -380,8 +390,31 @@ class DocProperties(PropertyGroup):
class BIMCameraProperties(PropertyGroup):
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
linework_mode: EnumProperty(
items=[
("OPENCASCADE", "OpenCASCADE", "Slower, more accurate, with more features"),
("FREESTYLE", "Freestyle", "Faster, less accurate, no fill support"),
],
default="OPENCASCADE",
name="Linework Mode",
)
fill_mode: EnumProperty(
items=[
("NONE", "None", "Disable filling areas seen in projection"),
("SHAPELY", "Shapely", "Recommended"),
("SVGFILL", "SVGFill", "Experimental"),
],
default="NONE",
name="Fill Mode",
)
cut_mode: EnumProperty(
items=[
("BISECT", "Bisect", "Faster, more forgiving to bad geometry"),
("OPENCASCADE", "OpenCASCADE", "More technically correct"),
],
default="BISECT",
name="Cut Mode",
)
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)
@@ -400,6 +433,7 @@ class BIMCameraProperties(PropertyGroup):
filter_mode: StringProperty(name="Filter Mode", default="NONE")
include_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Include Filter")
exclude_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Exclude Filter")
update_props: BoolProperty(name="Enable Props Auto Update", default=True)
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
@@ -524,9 +558,10 @@ def get_relating_type_id(self, context):
def update_annotation_object_type(self, context):
self.relating_type_id = "0"
# changing enum doesn't trigger refresh by itself
# Refresh enum items before changing property,
# otherwise it might map to the wrong item.
AnnotationData.is_loaded = False
self.relating_type_id = "0"
def update_sheet_data(self, context):
+17 -14
View File
@@ -64,9 +64,15 @@ class BIM_PT_camera(Panel):
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = self.layout.row()
row.prop(props, "calculate_shapely_surfaces")
row = self.layout.row()
row.prop(props, "calculate_svgfill_surfaces")
row.prop(props, "linework_mode")
if props.linework_mode == "OPENCASCADE":
row = self.layout.row()
row.prop(props, "fill_mode")
row = self.layout.row()
row.prop(props, "cut_mode")
elif not hasattr(context.scene, "svg_export"):
row = self.layout.row()
row.label(text="Freestyle SVG Exporter Not Installed", icon="ERROR")
row = self.layout.row()
row.prop(props, "width")
@@ -106,11 +112,7 @@ class BIM_PT_element_filters(Panel):
@classmethod
def poll(cls, context):
return (
context.scene.camera
and context.active_object
and hasattr(context.active_object.data, "BIMCameraProperties")
)
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
def draw(self, context):
if not ElementFiltersData.is_loaded:
@@ -160,17 +162,15 @@ class BIM_PT_drawing_underlay(Panel):
@classmethod
def poll(cls, context):
return (
context.scene.camera
and context.active_object
and hasattr(context.active_object.data, "BIMCameraProperties")
)
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
def draw(self, context):
layout = self.layout
layout.use_property_split = True
camera = context.scene.camera
assert camera
dprops = context.scene.DocProperties
props = context.active_object.data.BIMCameraProperties
props = camera.data.BIMCameraProperties
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
if not DrawingsData.is_loaded:
@@ -289,6 +289,9 @@ class BIM_PT_drawings(Panel):
create_drawing_button = row.row(align=True)
create_drawing_button.operator("bim.create_drawing", text="", icon="OUTPUT")
create_drawing_button.enabled = active_drawing.ifc_definition_id > 0
# might need a different icon since the URL icon is already used by the open_drawing
row.operator("bim.open_documentation_web_ui", icon="URL", text="")
self.layout.template_list(
"BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index"
)
@@ -184,6 +184,8 @@ def create_annotation():
create_annotation_occurrence(bpy.context)
else:
object_type = props.object_type
if not bpy.ops.bim.add_annotation.poll():
return
bpy.ops.bim.add_annotation(
object_type=object_type, data_type=tool.Drawing.ANNOTATION_TYPES_DATA[object_type][-1]
)
+5 -1
View File
@@ -41,7 +41,11 @@ def get_engine(self, context):
class BIMFMProperties(PropertyGroup):
ifc_files: PointerProperty(type=MultipleFileSelect)
spreadsheet_files: CollectionProperty(name="Spreadsheets", type=StrProperty)
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
should_load_from_memory: BoolProperty(
default=False,
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
engine: EnumProperty(items=get_engine, name="Engine", options=set())
format: EnumProperty(
items=[
@@ -364,7 +364,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
# TODO: write unit tests to see how this bulk operation handles
# contradictory ifc_representation_class values and when
# ifc_representation_class is IfcTextLiteral
if not obj.data:
data = obj.data
if not tool.Geometry.is_data_supported_for_adding_representation(data):
continue
self.update_obj_mesh_representation(context, obj)
tool.Ifc.finish_edit(obj)
@@ -1653,8 +1654,9 @@ class OverrideModeSetEdit(bpy.types.Operator):
should_sync_changes_first=False,
apply_openings=False,
)
tool.Geometry.apply_item_ids_as_vertex_groups(obj)
tool.Geometry.dissolve_triangulated_edges(obj)
if isinstance(obj.data, bpy.types.Mesh):
tool.Geometry.apply_item_ids_as_vertex_groups(obj)
tool.Geometry.dissolve_triangulated_edges(obj)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
else:
obj.select_set(False)
@@ -1831,6 +1833,11 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
props.items.clear()
def add_tag(item, tag: str) -> None:
if item.tags:
item.tags += ","
item.tags += tag
if bpy.context.active_object.data and hasattr(bpy.context.active_object.data, "BIMMeshProperties"):
active_representation_id = bpy.context.active_object.data.BIMMeshProperties.ifc_definition_id
element = tool.Ifc.get().by_id(active_representation_id)
@@ -1862,6 +1869,10 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
shape_aspect = inverse.OfShapeAspect[0]
new.shape_aspect = shape_aspect.Name
new.shape_aspect_id = shape_aspect.id()
elif inverse.is_a("IfcIndexedTextureMap"):
add_tag(new, "UV")
elif inverse.is_a("IfcIndexedColourMap"):
add_tag(new, "Colour")
# sort created items
sorted_items = sorted(props.items[:], key=lambda i: (not i.shape_aspect, i.shape_aspect))
@@ -1909,7 +1920,7 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.enable_editing_representation_items()
def poll_editing_representaiton_item_style(cls, context):
def poll_editing_representation_item_style(cls, context):
if not (obj := getattr(context, "active_object", None)):
return False
props = obj.BIMGeometryProperties
@@ -1944,7 +1955,7 @@ class EnableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator
@classmethod
def poll(cls, context):
return poll_editing_representaiton_item_style(cls, context)
return poll_editing_representation_item_style(cls, context)
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
@@ -1994,30 +2005,76 @@ class DisableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operato
class UnassignRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_representation_item_style"
bl_label = "Unassign Representation Item Style"
bl_description = "Will remove the style from all selected objects"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_editing_representaiton_item_style(cls, context)
return poll_editing_representation_item_style(cls, context)
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
props.is_editing_item_style = False
active_obj = context.active_object
active_props = active_obj.BIMGeometryProperties
active_props.is_editing_item_style = False
# get active representation item
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = tool.Ifc.get_entity_by_id(representation_item_id)
if not representation_item:
self.report({"ERROR"}, f"Couldn't find representation item by id {representation_item_id}.")
# Get active representation item
active_representation_item_id = active_props.items[active_props.active_item_index].ifc_definition_id
active_representation_item = tool.Ifc.get_entity_by_id(active_representation_item_id)
if not active_representation_item:
self.report({"ERROR"}, f"Couldn't find representation item by id {active_representation_item_id}.")
return {"CANCELLED"}
tool.Style.assign_style_to_representation_item(representation_item, None)
tool.Geometry.reload_representation(obj)
# reload items ui
# Retrieve styles applied to the active representation item
active_styles = set()
if hasattr(active_representation_item, 'StyledByItem'):
for styled_by_item in active_representation_item.StyledByItem:
if hasattr(styled_by_item, 'Styles'):
active_styles.update(styled_by_item.Styles)
if not active_styles:
self.report({"ERROR"}, "Couldn't find any styles associated with the active representation item.")
return {"CANCELLED"}
# Unassign matching styles from the active object itself
for style in active_styles:
tool.Style.assign_style_to_representation_item(active_representation_item, None)
tool.Geometry.reload_representation(active_obj)
break # No need to check further if one matching style is found
# Iterate over selected objects and unassign matching styles
for obj in context.selected_objects:
if obj == active_obj:
continue # Skip the active object itself
# Get the IFC entity directly from the object
element = tool.Ifc.get_entity(obj)
if not element:
continue # Skip if no IFC entity is found
representation_item_id = element.Representation.Representations[0].Items[0].id()
representation_item = tool.Ifc.get_entity_by_id(representation_item_id)
if representation_item:
# Retrieve styles for the current representation item
styles = set()
if hasattr(representation_item, 'StyledByItem'):
for styled_by_item in representation_item.StyledByItem:
if hasattr(styled_by_item, 'Styles'):
styles.update(styled_by_item.Styles)
# Unassign matching styles
for style in styles:
if style in active_styles:
tool.Style.assign_style_to_representation_item(representation_item, None)
tool.Geometry.reload_representation(obj)
break # No need to check further if one matching style is found
# Reload UI items
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_representation_item_shape_aspect"
@@ -97,6 +97,7 @@ class RepresentationItem(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
shape_aspect: StringProperty(name="Shape Aspect")
shape_aspect_id: IntProperty(name="Shape Aspect IFC ID")
tags: StringProperty(name="Tags")
class ShapeAspect(PropertyGroup):
@@ -192,6 +192,7 @@ class BIM_PT_representation_items(Panel):
RepresentationItemsData.load()
props = context.active_object.BIMGeometryProperties
layout = self.layout
row = self.layout.row(align=True)
row.label(text=f"{RepresentationItemsData.data['total_items']} Items Found")
@@ -231,6 +232,19 @@ class BIM_PT_representation_items(Panel):
row = self.layout.row()
row.label(text=active_item.layer or "No Presentation Layer", icon="STICKY_UVS_LOC")
if active_item.name.endswith("FaceSet"):
if "UV" in active_item.tags:
text = "Has UV mapping"
else:
text = "Has no UV mapping"
layout.label(text=text, icon="UV")
if "Colour" in active_item.tags:
text = "Has colour mapping"
else:
text = "Has no colour mapping"
layout.label(text=text, icon="COLOR")
row = self.layout.row(align=True)
if props.is_editing_item_shape_aspect:
row.prop(props, "representation_item_shape_aspect", icon="SHAPEKEY_DATA", text="")
@@ -59,6 +59,9 @@ class GeoreferenceDecorator:
batch.draw(shader)
def draw_text(self, context):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
self.calculate_angles(context)
props = context.scene.BIMGeoreferenceProperties
e, n, h = [round(float(o), 7) for o in props.model_origin.split(",")]
@@ -164,6 +167,9 @@ class GeoreferenceDecorator:
blf.draw(self.font_id, line)
def draw_geometry(self, context):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
self.calculate_angles(context)
self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -39,6 +39,10 @@ classes = (
operator.ViewFromSun,
operator.RefreshIFCMaterials,
operator.UnmapMaterial,
operator.RADIANCE_OT_select_camera,
operator.RADIANCE_OT_export_material_mappings,
operator.RADIANCE_OT_import_material_mappings,
operator.RADIANCE_OT_open_spectraldb,
prop.RadianceMaterial,
prop.BIMSolarProperties,
prop.RadianceExporterProperties,
@@ -1,3 +1,22 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai 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.
#
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import os
@@ -10,6 +29,15 @@ class MATERIAL_UL_radiance_materials(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
# Adding color preview or glass icon
if item.category == "Glass":
row.label(icon="SHADING_TEXTURE")
else:
color_rect = row.row()
color_rect.prop(item, "color", text="")
color_rect.scale_x = 0.3
row.prop(item, "name", text="", emboss=False, icon_value=icon)
if item.is_mapped:
row.label(text=f"{item.category} - {item.subcategory}")
+197 -55
View File
@@ -30,11 +30,16 @@ import bonsai.tool as tool
from pathlib import Path
from typing import Union, Optional, Sequence
import json
import math
import time
import ifcopenshell
import webbrowser
import ifcopenshell.geom
import multiprocessing
from mathutils import Vector
from bonsai.bim.module.light.data import SolarData
from bpy_extras.io_utils import ExportHelper
from bpy_extras.io_utils import ImportHelper
ifc_materials = []
@@ -95,14 +100,10 @@ class ExportOBJ(bpy.types.Operator):
while True:
shape = iterator.get()
materials = shape.geometry.materials
# print(shape.geometry)
material_ids = shape.geometry.material_ids
# material_names = shape.geometry.material_names
# print(material_ids)
for material in materials:
# print(material, dir(material))
# print(material.name)
ifc_materials.append(material.name)
serialiser.write(shape)
@@ -129,10 +130,15 @@ class RadianceRender(bpy.types.Operator):
self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.")
return {"CANCELLED"}
# Get the resolution from the user input
print("Starting Radiance rendering process...")
props = context.scene.radiance_exporter_properties
resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y
context.scene.render.resolution_x = resolution_x
context.scene.render.resolution_y = resolution_y
aspect_ratio = resolution_x / resolution_y
quality = props.radiance_quality.upper()
detail = props.radiance_detail.upper()
variability = props.radiance_variability.upper()
@@ -141,6 +147,11 @@ class RadianceRender(bpy.types.Operator):
output_file_format = props.output_file_format
use_hdr = props.use_hdr
choose_hdr_image = props.choose_hdr_image
print(f"Resolution: {resolution_x}x{resolution_y}")
print(f"Quality: {quality}, Detail: {detail}, Variability: {variability}")
print(f"Output directory: {output_dir}")
if use_hdr:
hdr_image = "noon_grass_2k.hdr"
hdr_mask = "noon_grass_2k_mask.hdr"
@@ -148,30 +159,34 @@ class RadianceRender(bpy.types.Operator):
hdr_image_path = os.path.join(os.path.dirname(__file__), "HDRs", hdr_image)
hdr_mask_path = os.path.join(os.path.dirname(__file__), "HDRs", hdr_mask)
sky_map_cal_path = os.path.join(os.path.dirname(__file__), "HDRs", sky_map_cal)
# os.chdir(output_dir)
obj_file_path = os.path.join(output_dir, "model.obj")
sun_props = context.scene.BIMSolarProperties
sun_pos_props = context.scene.sun_pos_properties
sky_file_path = os.path.join(output_dir, "sky.rad")
latitude = sun_props.latitude
longitude = sun_props.longitude
timezone = sun_props.timezone
month = sun_props.month
day = sun_props.day
hour = sun_props.hour
minute = sun_props.minute
# latitude = sun_props.latitude
# longitude = sun_props.longitude
# month = sun_props.month
# day = sun_props.day
# hour = sun_props.hour
# minute = sun_props.minute
print("Sun Properties:")
print("Latitude: ", latitude)
print("Longitude: ", longitude)
print("Timezone: ", timezone)
print("Month: ", month)
print("Day: ", day)
print("Hour: ", hour)
print("Minute: ", minute)
# print("Sun Properties:")
# print("Latitude: ", latitude)
# print("Longitude: ", longitude)
# print("Timezone: ", timezone)
# print("Month: ", month)
# print("Day: ", day)
# print("Hour: ", hour)
# print("Minute: ", minute)
print("Setting up camera...")
if props.use_active_camera:
camera = context.scene.camera
else:
camera = props.selected_camera
camera = self.get_active_camera(context)
if camera is None:
self.report({"ERROR"}, "No active camera found in the scene. Please add a camera and set it as active.")
return {"CANCELLED"}
@@ -179,19 +194,35 @@ class RadianceRender(bpy.types.Operator):
# Get camera position and direction
camera_position, camera_direction = self.get_camera_data(camera)
dt = datetime(2024, month, day, hour, minute)
print(f"Camera position: {camera_position}")
print(f"Camera direction: {camera_direction}")
# sun_position = tool.Blender.get_sun_position_addon()
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
# sun_pos_props.time,
# sun_pos_props.latitude,
# sun_pos_props.longitude,
# -sun_pos_props.UTC_zone,
# sun_pos_props.month,
# sun_pos_props.day,
# sun_pos_props.year,
# )
dt = datetime(sun_pos_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute)
sky_description = pr.gensky(
dt=dt,
latitude=latitude,
longitude=longitude,
timezone=timezone,
year=2024,
sunny_with_sun=True,
sunny_without_sun=False,
cloudy=False,
ground_reflectance=0.2,
turbidity=3.0,
# azimuth=64.1,
# altitude=-26.6,
latitude=sun_props.latitude,
longitude=sun_props.longitude,
year=sun_pos_props.year,
timezone=-int(sun_props.UTC_zone),
# sunny_with_sun=False,
# sunny_without_sun=False,
# cloudy=False,
# ground_reflectance=0.2,
# turbidity=3.0,
)
sky_description_str = sky_description.decode("utf-8")
@@ -292,11 +323,7 @@ ground_glow source ground
props = context.scene.radiance_exporter_properties
if props.use_json_file:
with open(props.json_file, "r") as file:
data = json.load(file)
else:
data = props.get_mappings_dict()
data = props.get_mappings_dict()
materials_file = os.path.join(output_dir, "materials.rad")
written_materials = set()
@@ -318,9 +345,6 @@ ground_glow source ground
file.write(material)
written_materials.add(material.split()[2]) # Add material name to written set
print(data)
print(default_materials)
for style_id in all_materials:
material = next((m for m in props.materials if m.style_id == style_id), None)
if material and material.is_mapped:
@@ -351,7 +375,7 @@ ground_glow source ground
self.report({"INFO"}, "Exported Scene file to: {}".format(scene_file))
# Py Radiance Rendering code
print("Setting up Radiance scene...")
scene = pr.Scene("ascene")
material_path = os.path.join(output_dir, "materials.rad")
@@ -360,10 +384,39 @@ ground_glow source ground
scene.add_material(material_path)
scene.add_surface(scene_path)
scene.add_source(sky_file_path)
print("Setting up view...")
if camera.data.type == "PERSP":
# Perspective camera
camera_fov = camera.data.angle
# Calculate vertical FOV based on the desired aspect ratio
vertical_fov = 2 * math.atan(math.tan(camera_fov / 2) / aspect_ratio)
aview = pr.View(position=camera_position, direction=camera_direction)
aview = pr.View(
vtype="v", # Perspective view
position=camera_position,
direction=camera_direction,
vup=(0, 0, 1), # Assuming Z is up
horiz=math.degrees(camera_fov),
vert=math.degrees(vertical_fov),
)
else: # 'ORTHO'
# Orthographic camera
# Calculate the view size based on the camera's orthographic scale
ortho_scale = camera.data.ortho_scale
view_width = ortho_scale
view_height = ortho_scale / aspect_ratio
aview = pr.View(
vtype="l", # Parallel projection (orthographic)
position=camera_position,
direction=camera_direction,
vup=(0, 0, 1), # Assuming Z is up
horiz=view_width,
vert=view_height,
)
scene.add_view(aview)
print("Starting render...")
start_time = time.time()
image = pr.render(
scene,
ambbounce=1,
@@ -371,32 +424,34 @@ ground_glow source ground
quality=quality,
detail=detail,
variability=variability,
nproc=multiprocessing.cpu_count(),
)
end_time = time.time()
print(f"Render completed in {end_time - start_time:.2f} seconds")
output_hdr_path = os.path.join(output_dir, f"{output_file_name}.{output_file_format.lower()}")
print(f"Saving HDR output to: {output_hdr_path}")
if output_file_format == "HDR":
with open(output_hdr_path, "wb") as wtr:
wtr.write(image)
else:
pass
print("Applying tone mapping...")
pcond_image = pr.pcond(hdr=output_hdr_path, human=True)
tiff_path = os.path.join(output_dir, f"{output_file_name}.tiff")
print(f"Saving TIFF output to: {tiff_path}")
pr.ra_tiff(inp=pcond_image, out=tiff_path, lzw=True)
print("Radiance rendering process completed successfully.")
self.report({"INFO"}, "Radiance rendering completed. Output: {}".format(tiff_path))
return {"FINISHED"}
def get_active_camera(self, context):
if context.scene.camera:
props = context.scene.radiance_exporter_properties
if props.use_active_camera:
return context.scene.camera
for obj in context.scene.objects:
if obj.type == "CAMERA":
return obj
return None
else:
return props.selected_camera
def get_camera_data(self, camera):
# Get camera position
@@ -509,9 +564,29 @@ class RefreshIFCMaterials(bpy.types.Operator):
for render_item in style.Styles:
if render_item.is_a("IfcSurfaceStyleRendering"):
style_id = f"IfcSurfaceStyleRendering-{render_item.id()}"
ifc_materials.append(style_id)
style_name = style.Name or f"Unnamed Style {render_item.id()}"
props.add_material_mapping(style_id, style_name)
# Extract color and transparency
color = (1.0, 1.0, 1.0) # Default white
transparency = 0.0 # Default opaque
if render_item.SurfaceColour:
color = (
render_item.SurfaceColour.Red,
render_item.SurfaceColour.Green,
render_item.SurfaceColour.Blue,
)
if hasattr(render_item, "Transparency") and render_item.Transparency is not None:
transparency = render_item.Transparency
# Add material with color
material = props.add_material_mapping(style_id, style_name)
material.color = color
# If transparency is high, consider it as glass
if transparency > 0.5:
material.category = "Glass"
material.subcategory = "Clear Glass"
material.is_mapped = True
props.active_material_index = 0 if props.materials else -1
@@ -531,3 +606,70 @@ class UnmapMaterial(bpy.types.Operator):
material = props.materials[self.material_index]
props.unmap_material(material.name)
return {"FINISHED"}
class RADIANCE_OT_select_camera(bpy.types.Operator):
bl_idname = "radiance.select_camera"
bl_label = "Select Camera"
bl_description = "Select a camera from the viewport"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object is not None and context.object.type == "CAMERA"
def execute(self, context):
props = context.scene.radiance_exporter_properties
props.selected_camera = context.object
props.use_active_camera = False
return {"FINISHED"}
class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper):
bl_idname = "radiance.export_material_mappings"
bl_label = "Export Material Mappings"
bl_description = "Export material mappings to a JSON file"
filename_ext = ".json"
def execute(self, context):
props = context.scene.radiance_exporter_properties
mappings = {}
for material in props.materials:
if material.is_mapped:
mappings[material.style_id] = {
"name": material.name,
"category": material.category,
"subcategory": material.subcategory,
}
with open(self.filepath, "w") as f:
json.dump(mappings, f, indent=4)
self.report({"INFO"}, f"Material mappings exported to {self.filepath}")
return {"FINISHED"}
class RADIANCE_OT_import_material_mappings(bpy.types.Operator, ImportHelper):
bl_idname = "radiance.import_material_mappings"
bl_label = "Import Material Mappings"
bl_description = "Import material mappings from a JSON file"
filename_ext = ".json"
def execute(self, context):
props = context.scene.radiance_exporter_properties
props.import_mappings(self.filepath)
self.report({"INFO"}, f"Material mappings imported from {self.filepath}")
return {"FINISHED"}
class RADIANCE_OT_open_spectraldb(bpy.types.Operator):
bl_idname = "radiance.open_spectraldb"
bl_label = "Open SpectralDB"
bl_description = "Open the SpectralDB website for reference"
def execute(self, context):
webbrowser.open("https://spectraldb.com")
return {"FINISHED"}
+46 -19
View File
@@ -32,6 +32,7 @@ from bpy.props import (
FloatVectorProperty,
BoolProperty,
CollectionProperty,
PointerProperty,
)
import os
from bpy.types import PropertyGroup
@@ -107,6 +108,11 @@ def update_display_sun_path(self, context):
SolarDecorator.uninstall()
def update_resolution(self, context):
context.scene.render.resolution_x = self.radiance_resolution_x
context.scene.render.resolution_y = self.radiance_resolution_y
def update_sun_path():
if not SolarData.is_loaded:
SolarData.load()
@@ -142,6 +148,10 @@ def update_sun_path():
rotation_euler = Euler((elevation - pi / 2, 0, -azimuth))
rotation_quaternion = rotation_euler.to_quaternion()
props.azimuth = azimuth
props.elevation = elevation
props.UTC_zone = zone
if sun_vector.z < 0:
bpy.context.scene.display.light_direction = mat @ Vector((0, 0, 1))
else:
@@ -162,14 +172,11 @@ class RadianceMaterial(PropertyGroup):
category: StringProperty(name="Category")
subcategory: StringProperty(name="Subcategory")
is_mapped: BoolProperty(name="Is Mapped", default=False)
color: FloatVectorProperty(name="Color", subtype="COLOR", default=(1.0, 1.0, 1.0), min=0.0, max=1.0, size=3)
class RadianceExporterProperties(PropertyGroup):
def update_json_file(self, context):
if self.json_file:
self.json_file = bpy.path.abspath(self.json_file)
def update_output_dir(self, context):
if self.output_dir:
self.output_dir = bpy.path.abspath(self.output_dir)
@@ -184,8 +191,26 @@ class RadianceExporterProperties(PropertyGroup):
item.style_id = style_id
item.category = ""
item.subcategory = ""
item.color = (1.0, 1.0, 1.0) # Default white
return item
def import_mappings(self, filepath):
with open(filepath, "r") as f:
mappings = json.load(f)
for style_id, mapping in mappings.items():
material = self.get_material_mapping(mapping["name"])
if material:
material.style_id = style_id
material.category = mapping["category"]
material.subcategory = mapping["subcategory"]
material.is_mapped = True
else:
new_material = self.add_material_mapping(style_id, mapping["name"])
new_material.category = mapping["category"]
new_material.subcategory = mapping["subcategory"]
new_material.is_mapped = True
def get_material_mapping(self, style_name):
return next((item for item in self.materials if item.name == style_name), None)
@@ -213,12 +238,6 @@ class RadianceExporterProperties(PropertyGroup):
item.subcategory = ""
item.is_mapped = False
use_json_file: BoolProperty(
name="Upload JSON",
description="Toggle between uploading a JSON file and using in-UI material mapping",
default=False,
)
is_exporting: bpy.props.BoolProperty(
name="Is Exporting", description="Whether the OBJ export is in progress", default=False
)
@@ -237,6 +256,7 @@ class RadianceExporterProperties(PropertyGroup):
("Plant", "Plant", ""),
("Exterior", "Exterior", ""),
("Color Swatch", "Color Swatch", ""),
("Glass", "Glass", ""),
]
def update_material_mapping(self, context):
@@ -272,10 +292,10 @@ class RadianceExporterProperties(PropertyGroup):
)
radiance_resolution_x: IntProperty(
name="X", description="Horizontal resolution of the output image", default=1920, min=1
name="X", description="Horizontal resolution of the output image", default=1920, min=1, update=update_resolution
)
radiance_resolution_y: IntProperty(
name="Y", description="Vertical resolution of the output image", default=1080, min=1
name="Y", description="Vertical resolution of the output image", default=1080, min=1, update=update_resolution
)
output_dir: StringProperty(
name="Output Directory",
@@ -291,13 +311,6 @@ class RadianceExporterProperties(PropertyGroup):
subtype="FILE_PATH",
update=lambda self, context: self.update_ifc_file(context),
)
json_file: StringProperty(
name="JSON File",
description="Path to the JSON file",
default="",
subtype="FILE_PATH",
update=lambda self, context: self.update_json_file(context),
)
radiance_quality: EnumProperty(
name="Quality",
@@ -349,6 +362,17 @@ class RadianceExporterProperties(PropertyGroup):
default="Noon",
)
use_active_camera: BoolProperty(
name="Use Active Camera", description="Use the active camera in the scene", default=True
)
selected_camera: PointerProperty(
type=bpy.types.Object,
name="Camera",
description="Select a camera to use for rendering",
poll=lambda self, object: object.type == "CAMERA",
)
class BIMSolarProperties(PropertyGroup):
sites: EnumProperty(items=get_sites, name="Sites")
@@ -363,6 +387,9 @@ class BIMSolarProperties(PropertyGroup):
sun_position: FloatVectorProperty(name="Sun Position", subtype="XYZ", default=(0, 0, 0))
sun_path_origin: FloatVectorProperty(name="Sun Path Origin", subtype="XYZ", default=(0, 0, 0))
sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path_size)
azimuth: FloatProperty(name="Azimuth")
elevation: FloatProperty(name="Elevation")
UTC_zone: FloatProperty(name="UTC Zone")
display_shadows: BoolProperty(
name="Display Shadows",
default=False,
@@ -1,4 +1,7 @@
{
"Glass": {
"Clear Glass": "void BRTDfunc clear_glass\n10\n sr_clear_r sr_clear_g sr_clear_b\n st_clear_r st_clear_g st_clear_b\n 0 0 0\n glaze1.cal\n0\n19\n 0 0 0\n 0 0 0\n 0 0 0\n 1\n 0.074 0.077 0.079\n 0.074 0.077 0.079\n 0.862 0.890 0.886\n"
},
"Wall": {
"White Painted Room Walls": "void plastic white_painted_room_walls \n0\n0\n5 0.8316 0.8116 0.7226 0.0036 0.2",
"White Painted Corridor Walls": "void plastic white_painted_corridor_walls \n0\n0\n5 0.8143 0.7984 0.715 0.0039 0.2",
+41 -33
View File
@@ -50,43 +50,52 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
row.prop(props, "output_dir")
row = layout.row()
layout.prop(props, "use_json_file")
layout.label(text="Info: Unmapped materials default to white")
row = layout.row()
row.template_list("MATERIAL_UL_radiance_materials", "", props, "materials", props, "active_material_index")
row.operator("radiance.open_spectraldb", text="", icon="WORLD") # Globe icon
if len(props.materials) > 0:
col = layout.column(align=True)
col.prop(props, "category")
if props.category:
col.prop(props, "subcategory")
if props.use_json_file:
row = layout.row()
row.prop(props, "json_file")
if props.active_material_index >= 0 and props.active_material_index < len(props.materials):
active_material = props.materials[props.active_material_index]
if active_material.category and active_material.subcategory:
layout.label(
text=f"Mapped: {active_material.name} to {active_material.category} - {active_material.subcategory}"
)
else:
layout.label(text=f"Select category and subcategory for: {active_material.name}")
if not props.use_json_file:
row = layout.row()
layout.label(text="Info: Unmapped materials default to white")
row = layout.row()
row.template_list("MATERIAL_UL_radiance_materials", "", props, "materials", props, "active_material_index")
if len(props.materials) > 0:
col = layout.column(align=True)
col.prop(props, "category")
if props.category:
col.prop(props, "subcategory")
if props.active_material_index >= 0 and props.active_material_index < len(props.materials):
active_material = props.materials[props.active_material_index]
if active_material.category and active_material.subcategory:
layout.label(
text=f"Mapped: {active_material.name} to {active_material.category} - {active_material.subcategory}"
)
else:
layout.label(text=f"Select category and subcategory for: {active_material.name}")
row = layout.row()
row.operator("bim.refresh_ifc_materials", text="Refresh IFC Materials")
row = layout.row()
row.operator("radiance.import_material_mappings", text="Import Mappings", icon="IMPORT")
row.operator("radiance.export_material_mappings", text="Export Mappings", icon="EXPORT")
row = layout.row()
row.operator("bim.refresh_ifc_materials", text="Refresh IFC Materials")
layout.separator()
row = layout.row()
row.label(text="Resolution")
row.prop(props, "radiance_resolution_x", text="X")
row = layout.row()
row.label(text="")
layout.label(text="Step 1: Export geometry for simulation")
row = layout.row()
row.operator("export_scene.radiance", text="Export Geometry for Simulation")
layout.separator()
box = layout.box()
box.label(text="Camera Settings")
row = box.row()
row.prop(props, "use_active_camera")
if not props.use_active_camera:
row = box.row()
row.prop(props, "selected_camera")
row.operator("radiance.select_camera", text="", icon="EYEDROPPER")
row = box.row(align=True)
row.label(text="Resolution")
row.prop(props, "radiance_resolution_x", text="X")
row.prop(props, "radiance_resolution_y", text="Y")
row = layout.row()
@@ -115,8 +124,7 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
row.prop(props, "choose_hdr_image")
row = layout.row()
row.operator("export_scene.radiance", text="Export Geometry for Simulation")
layout.label(text="Step 2: Run the simulation")
row = layout.row()
row.operator("render_scene.radiance", text="Radiance Render")
row.enabled = not props.is_exporting
+4 -19
View File
@@ -33,7 +33,6 @@ from . import (
window,
opening,
mep,
pie,
workspace,
profile,
sverchok_modifier,
@@ -119,6 +118,7 @@ classes = (
mep.RegenerateDistributionElement,
prop.SnapMousePoint,
prop.PolylinePoint,
prop.PolylineMeasurement,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -138,17 +138,12 @@ classes = (
ui.BIM_MT_model,
ui.BIM_PT_Grids,
grid.BIM_OT_add_object,
stair.BIM_OT_add_object,
stair.BIM_OT_add_clever_stair,
stair.BIM_OT_add_stair,
stair.AddStair,
stair.CancelEditingStair,
stair.FinishEditingStair,
stair.EnableEditingStair,
stair.RemoveStair,
pie.OpenPieClass,
pie.PieAddOpening,
pie.VIEW3D_MT_PIE_bim,
pie.VIEW3D_MT_PIE_bim_class,
sverchok_modifier.CreateNewSverchokGraph,
sverchok_modifier.UpdateDataFromSverchok,
sverchok_modifier.DeleteSverchokGraph,
@@ -220,13 +215,8 @@ def register():
bpy.types.VIEW3D_MT_mesh_add.append(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")
kmi = km.keymap_items.new("wm.call_menu_pie", "E", "PRESS", shift=True)
kmi.properties["name"] = "VIEW3D_MT_PIE_bim"
addon_keymaps.append((km, kmi))
workspace.load_custom_icons()
def unregister():
if not bpy.app.background:
@@ -252,9 +242,4 @@ def unregister():
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_mesh_add.remove(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
workspace.unload_custom_icons()
+286 -63
View File
@@ -21,11 +21,14 @@ import gpu
import gpu_extras
import bmesh
import bonsai.tool as tool
from math import sin, cos, radians
from math import sin, cos, radians, degrees, atan2, acos
from bpy.types import SpaceView3D
import math
from bpy_extras import view3d_utils
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
def transparent_color(color, alpha=0.1):
@@ -295,7 +298,7 @@ class ProfileDecorator:
return points, listEdg
class WallPolylineDecorator:
class PolylineDecorator:
is_installed = False
handlers = []
mouse_pos = None
@@ -303,13 +306,20 @@ class WallPolylineDecorator:
input_type = None
angle_snap_mat = None
angle_snap_loc = None
use_default_container = False
instructions = None
snap_info = None
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_panel, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_on_screen_menu, (context,), "WINDOW", "POST_PIXEL")
)
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@@ -336,15 +346,37 @@ class WallPolylineDecorator:
cls.axis_start = start
cls.axis_end = end
@classmethod
def set_axis_rectangle(cls, corners):
cls.axis_rectangle = [*corners]
@classmethod
def set_use_default_container(cls, value=False):
cls.use_default_container = value
@classmethod
def set_plane(cls, plane_origin, plane_normal):
cls.plane_origin = plane_origin
cls.plane_normal = plane_normal
@classmethod
def set_instructions(cls, instructions):
cls.instructions = instructions
@classmethod
def set_snap_info(cls, snap_info):
cls.snap_info = snap_info
@classmethod
def calculate_distance_and_angle(cls, context, is_input_on):
try:
polyline_data = context.scene.BIMModelProperties.polyline_point
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
last_point_data = polyline_data[len(polyline_data) - 1]
except:
last_point_data = None
default_container_elevation = 0
last_point_data = None
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
@@ -354,55 +386,140 @@ class WallPolylineDecorator:
last_point = Vector((0, 0, 0))
if is_input_on:
snap_vector = Vector(
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
if cls.use_default_container:
snap_vector = Vector(
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
)
else:
snap_vector = Vector(
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), float(cls.input_panel["Z"]))
)
else:
if cls.use_default_container:
snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
else:
snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
second_to_last_point = None
if len(polyline_data) > 1:
second_to_last_point_data = polyline_data[len(polyline_data) - 2]
second_to_last_point = Vector(
(second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z)
)
else:
snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
# Creates a fake "second to last" point away from the first point but in the same x axis
# this allows to calculate the angle relative to x axis when there is only one point
second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
distance = (snap_vector - last_point).length
x_axis_edge = (
Vector((0, 0)),
Vector((1, 0)),
)
current_axis = (
Vector((last_point.x, last_point.y)),
Vector((snap_vector.x, snap_vector.y)),
)
if distance > 0:
angle = tool.Cad.angle_edges(x_axis_edge, current_axis, degrees=True, signed=True)
angle = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True)
# Round angle to the nearest 0.05
angle = round(angle / 0.05) * 0.05
if cls.input_panel:
cls.input_panel["X"] = str(round(snap_vector.x, 4))
cls.input_panel["Y"] = str(round(snap_vector.y, 4))
cls.input_panel["D"] = str(round(distance, 4))
cls.input_panel["A"] = str(round(angle, 4))
cls.input_panel["X"] = str(round(snap_vector.x, 3))
cls.input_panel["Y"] = str(round(snap_vector.y, 3))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(snap_vector.z, 3))
cls.input_panel["D"] = str(round(distance, 3))
cls.input_panel["A"] = str(round(angle, 3))
return cls.input_panel
return cls.input_panel
@classmethod
def calculate_x_and_y(cls, context):
def calculate_area(cls, context):
try:
polyline_data = context.scene.BIMModelProperties.polyline_point
except:
return cls.input_panel
if len(polyline_data) < 3:
return cls.input_panel
points = []
for data in polyline_data:
points.append(Vector((data.x, data.y, data.z)))
if points[0] == points[-1]:
points = points[1:]
# TODO move this to CAD
# Calculate the normal vector of the plane formed by the first three vertices
v1, v2, v3 = points[:3]
normal = (v2 - v1).cross(v3 - v1).normalized()
# Check if all points are coplanar
is_coplanar = True
tolerance = 1e-6 # Adjust this value as needed
for v in points:
if abs((v - v1).dot(normal)) > tolerance:
is_coplanar = False
if is_coplanar:
area = 0
for i in range(len(points)):
j = (i + 1) % len(points)
area += points[i].cross(points[j]).dot(normal)
area = abs(area) / 2
else:
area = 0
if "AREA" in list(cls.input_panel.keys()):
cls.input_panel["AREA"] = str(round(area, 4))
return cls.input_panel
@classmethod
def calculate_x_y_and_z(cls, context):
try:
polyline_data = context.scene.BIMModelProperties.polyline_point
last_point_data = polyline_data[len(polyline_data) - 1]
last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
except:
return
last_point = Vector((0, 0, 0))
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
if len(polyline_data) > 1:
second_to_last_point_data = polyline_data[len(polyline_data) - 2]
second_to_last_point = Vector(
(second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z)
)
else:
# Creates a fake "second to last" point away from the first point but in the same x axis
# this allows to calculate the angle relative to x axis when there is only one point
second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
distance = float(cls.input_panel["D"])
if distance < 0 or distance > 0:
angle_degrees = float(cls.input_panel["A"])
angle_radians = radians(360 - angle_degrees) # Substracting from 360 to make it clockwise
x = last_point[0] + distance * cos(angle_radians)
y = last_point[1] + distance * sin(angle_radians)
angle = radians(float(cls.input_panel["A"]))
rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True)
coords = rot_vector * distance + last_point
x = coords[0]
y = coords[1]
z = coords[2]
if cls.input_panel:
cls.input_panel["X"] = str(round(x, 4))
cls.input_panel["Y"] = str(round(y, 4))
cls.input_panel["X"] = str(round(x, 3))
cls.input_panel["Y"] = str(round(y, 3))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(z, 3))
return cls.input_panel
cls.input_panel["X"] = str(round(last_point.x, 3))
cls.input_panel["Y"] = str(round(last_point.y, 3))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(last_point.z, 3))
return cls.input_panel
def draw_batch(self, shader_type, content_pos, color, indices=None):
@@ -411,8 +528,25 @@ class WallPolylineDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text(self, context):
texts = ["X coord:", "Y coord:", "Distance:", "Angle:"]
@classmethod
def format_input_panel_units(cls, context, value):
unit_system = tool.Drawing.get_unit_system()
if unit_system == "IMPERIAL":
precision = context.scene.DocProperties.imperial_precision
factor = 3.28084
else:
precision = None
factor = 1
if context.scene.unit_settings.length_unit == "MILLIMETERS":
factor = 1000
return format_distance(
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
)
def draw_input_panel(self, context):
texts = {"D": "Distance: ", "A": "Angle: ", "X": "X coord: ", "Y": "Y coord: ", "Z": "Z coord:", "AREA": "Area: "}
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
blf.size(self.font_id, 12)
@@ -422,14 +556,76 @@ class WallPolylineDecorator:
color_highlight = self.addon_prefs.decorator_color_special
offset = 20
new_line = 20
keys = list(self.input_panel.keys())
for i, text in enumerate(texts):
if keys[i] == self.input_type:
for i, (key, value) in enumerate(self.input_panel.items()):
if key != "A" and key != self.input_type:
value = float(value)
formatted_value = self.format_input_panel_units(context, value)
else:
formatted_value = value
if key not in list(texts.keys()):
continue
if key == self.input_type:
blf.color(self.font_id, *color_highlight)
else:
blf.color(self.font_id, *color)
blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] + offset - (new_line * i), 0)
blf.draw(self.font_id, text + self.input_panel[keys[i]])
blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] - (new_line * i), 0)
blf.draw(self.font_id, texts[key] + formatted_value)
def draw_measurements(self, context):
region = context.region
rv3d = region.data
measurement_prop = context.scene.BIMModelProperties.polyline_measurement
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 1
blf.size(self.font_id, 12)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color)
for i in range(len(measurement_prop)):
if i == 0:
continue
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
value = measurement_prop[i].dim
value = float(value)
formatted_value = self.format_input_panel_units(context, value)
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
blf.draw(self.font_id, "d: " + formatted_value)
if i == 1:
continue
pos_angle = measurement_prop[i - 1].position
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
blf.draw(self.font_id, "a: " + measurement_prop[i].angle)
def draw_on_screen_menu(self, context):
region = context.region
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 2
blf.size(self.font_id, 12)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color)
text_w, text_h = blf.dimensions(0, self.instructions)
position = (region.width / 2) - (text_w / 2)
blf.position(self.font_id, position, 10, 0)
blf.draw(self.font_id, self.instructions)
text_w, text_h = blf.dimensions(0, self.snap_info)
position = (region.width / 2) - (text_w / 2)
blf.position(self.font_id, position, 30, 0)
blf.draw(self.font_id, self.snap_info)
def __call__(self, context):
@@ -452,26 +648,28 @@ class WallPolylineDecorator:
gpu.state.point_size_set(10)
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
# Point related to the mouse
mouse_point = [Vector((snap_prop.x, snap_prop.y, snap_prop.z))]
if snap_prop.snap_type in ["Face", "Plane"]:
self.draw_batch("POINTS", mouse_point, decorator_color_unselected)
else:
self.draw_batch("POINTS", mouse_point, (1.0, 0.6, 0.0, 1.0))
# When a point is above the plane it projects the point
# to the plane and creates a line
try:
snap_ref = context.scene.BIMModelProperties.snap_mouse_ref[0]
ref_point = [Vector((snap_ref.x, snap_ref.y, snap_ref.z))]
except:
ref_point = None
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
projection_point = []
if snap_prop.snap_type != "Plane" and snap_prop.z != 0:
self.line_shader.uniform_float("lineWidth", 1.0)
projection_point = [Vector((snap_prop.x, snap_prop.y, default_container_elevation))]
self.draw_batch("POINTS", projection_point, decorator_color_unselected)
edges = [[0, 1]]
self.draw_batch("LINES", mouse_point + projection_point, (1.0, 0.6, 0.0, 1.0), edges)
if self.use_default_container:
# When a point is above the plane it projects the point
# to the plane and creates a line
if snap_prop.snap_type != "Plane" and snap_prop.z != 0:
self.line_shader.uniform_float("lineWidth", 1.0)
projection_point = [Vector((snap_prop.x, snap_prop.y, default_container_elevation))]
self.draw_batch("POINTS", projection_point, decorator_color_unselected)
edges = [[0, 1]]
self.draw_batch("LINES", mouse_point + projection_point, (1.0, 0.6, 0.0, 1.0), edges)
# Polyline with selected points
# Create polyline with selected points
polyline_data = context.scene.BIMModelProperties.polyline_point
polyline_points = []
polyline_edges = []
@@ -482,20 +680,45 @@ class WallPolylineDecorator:
for i in range(len(polyline_points) - 1):
polyline_edges.append([i, i + 1])
self.line_shader.uniform_float("lineWidth", 2.0)
self.draw_batch("POINTS", polyline_points, decorator_color_selected)
if len(polyline_points) > 1:
self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_edges)
# Line between last polyline point and mouse
edges = [[0, 1]]
if polyline_points:
if snap_prop.snap_type != "Plane" and snap_prop.z != 0 and projection_point:
self.draw_batch("LINES", [polyline_points[-1]] + projection_point, decorator_color_unselected, edges)
else:
self.draw_batch("LINES", [polyline_points[-1]] + mouse_point, decorator_color_unselected, edges)
# Line for angle axis snap
if snap_prop.snap_type == "Axis":
self.line_shader.uniform_float("lineWidth", 0.75)
self.draw_batch("LINES", [self.axis_start, self.axis_end], decorator_color_unselected, [(0, 1)])
try:
self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)])
except:
pass
# Area highlight
if "AREA" in list(self.input_panel.keys()):
if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0:
edges = []
for i in range(1, len(polyline_points) - 1):
edges.append((0, i, i + 1))
self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges)
# Mouse points
if snap_prop.snap_type in ["Face", "Plane"]:
self.draw_batch("POINTS", mouse_point, decorator_color_unselected)
else:
self.draw_batch("POINTS", mouse_point, (1.0, 0.6, 0.0, 1.0))
if ref_point:
self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0))
# Line between last polyline point and mouse
edges = [[0, 1]]
if polyline_points:
if snap_prop.snap_type != "Plane" and projection_point:
self.draw_batch("LINES", [polyline_points[-1]] + projection_point, decorator_color_unselected, edges)
else:
self.draw_batch("LINES", [polyline_points[-1]] + mouse_point, decorator_color_unselected, edges)
# Draw polyline with selected points
self.line_shader.uniform_float("lineWidth", 2.0)
self.draw_batch("POINTS", polyline_points, decorator_color_selected)
if len(polyline_points) > 1:
self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_edges)
+9 -5
View File
@@ -40,7 +40,8 @@ import json
import collections
def update_door_modifier_representation(context: bpy.types.Context, obj: bpy.types.Object) -> None:
def update_door_modifier_representation(context: bpy.types.Context) -> None:
obj = context.active_object
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
@@ -494,6 +495,10 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Door"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
@@ -530,10 +535,9 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_door"
bl_label = "Add Door"
bl_options = {"REGISTER"}
obj: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
@@ -554,7 +558,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
pset=pset,
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))},
)
update_door_modifier_representation(context, obj)
update_door_modifier_representation(context)
return {"FINISHED"}
@@ -608,7 +612,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_door_modifier_representation(context, obj)
update_door_modifier_representation(context)
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
+1 -1
View File
@@ -82,7 +82,7 @@ class BIM_OT_add_object(Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
add_object(self, context)
-88
View File
@@ -1,88 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai 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.
#
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
class OpenPieClass(bpy.types.Operator):
bl_idname = "bim.open_pie_class"
bl_label = "Open Pie Class"
bl_description = "Assign the IFC Class to the selected objects"
@classmethod
def poll(cls, context):
if not context.active_object and not context.selected_objects:
cls.poll_message_set("No object selected.")
return False
return True
def execute(self, context):
bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_bim_class")
return {"FINISHED"}
class PieAddOpening(bpy.types.Operator):
bl_idname = "bim.pie_add_opening"
bl_label = "Add Opening"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
if len(context.selected_objects) == 2:
opening_name = None
obj_name = None
for obj in context.selected_objects:
if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id:
opening_name = obj.name
elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id:
opening_name = obj.children[0].name
else:
obj_name = obj.name
bpy.ops.bim.add_opening(obj=obj_name, opening=opening_name)
return {"FINISHED"}
class VIEW3D_MT_PIE_bim(bpy.types.Menu):
bl_label = "Geometry"
def draw(self, context):
pie = self.layout.menu_pie()
pie.operator("bim.edit_object_placement")
pie.operator("bim.update_representation").ifc_representation_class = ""
pie.operator("bim.pie_add_opening")
pie.operator("bim.open_pie_class", text="Assign IFC Class")
pie.operator("bim.aggregate_assign_object", text="Assign Aggregation")
pie.operator("bim.aggregate_unassign_object", text="Unassign Aggregation")
class VIEW3D_MT_PIE_bim_class(bpy.types.Menu):
bl_label = "Class"
def draw(self, context):
pie = self.layout.menu_pie()
pie.operator("bim.assign_class", text="IfcWall").ifc_class = "IfcWall"
pie.operator("bim.assign_class", text="IfcSlab").ifc_class = "IfcSlab"
pie.operator("bim.assign_class", text="IfcStair").ifc_class = "IfcStair"
pie.operator("bim.assign_class", text="IfcDoor").ifc_class = "IfcDoor"
pie.operator("bim.assign_class", text="IfcWindow").ifc_class = "IfcWindow"
pie.operator("bim.assign_class", text="IfcColumn").ifc_class = "IfcColumn"
pie.operator("bim.assign_class", text="IfcBeam").ifc_class = "IfcBeam"
@@ -144,8 +144,10 @@ class AddConstrTypeInstance(bpy.types.Operator):
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
props.relating_type_id = str(self.relating_type_id)
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
@@ -56,10 +56,13 @@ class DumbProfileGenerator:
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
props = bpy.context.scene.BIMModelProperties
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.depth = props.extrusion_depth
self.rotation = 0
self.location = Vector((0, 0, 0))
@@ -111,6 +111,10 @@ class PolylinePoint(PropertyGroup):
y: bpy.props.FloatProperty(name="Y")
z: bpy.props.FloatProperty(name="Z")
class PolylineMeasurement(PropertyGroup):
dim: bpy.props.StringProperty(name="Dimension")
angle: bpy.props.StringProperty(name="Angle")
position: bpy.props.FloatVectorProperty(name="Decorator Position", size=3)
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
@@ -204,7 +208,9 @@ class BIMModelProperties(PropertyGroup):
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
snap_mouse_point: bpy.props.CollectionProperty(type=SnapMousePoint)
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
polyline_point: bpy.props.CollectionProperty(type=PolylinePoint)
polyline_measurement: bpy.props.CollectionProperty(type=PolylineMeasurement)
class BIMArrayProperties(PropertyGroup):
@@ -283,6 +283,10 @@ class BIM_OT_add_railing(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Railing"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
@@ -499,6 +499,10 @@ class BIM_OT_add_roof(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Roof"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
@@ -135,10 +135,13 @@ class DumbSlabGenerator:
)
props = bpy.context.scene.BIMModelProperties
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.depth = sum(thicknesses) * unit_scale
self.width = 3
self.length = 3

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