Compare commits

..

293 Commits

Author SHA1 Message Date
Bruno Perdigão 290849d43a Fix snap to axis after 5649708ca 2025-01-20 14:59:04 -03:00
Andrej730 5e930972d6 User 'data' folder #5564
Bonsai now creates a user data folder that allows you overriding Bonsai data files. E.g. you can put you own `default.css` as `data/assets/default.css` in that folder and it will override the `default.css` Bonsai is using for the drawings. Previously you could have change internal `default.css` but Bonsai would always restore the original one on every update / reinstallation.

You can also store your .ifc files in data/libraries or data/templates and Bonsai will ensure they're loaded and you won't need to worry about Bonsai removing them later.

E.g. on Windows this path is `C:\Users\xxx\AppData\Roaming\bonsai\bonsai\data`/

You can also find path in the preferences - https://i.imgur.com/kwMc0JU.png
2025-01-20 19:32:59 +05:00
Bruno Perdigão c08d25de42 Change Bonsai snap target UI to column. 2025-01-20 11:31:58 -03:00
Bruno Perdigão 5649708ca4 Add support and UI for Bonsai snap target.
The user can now select snap target for Bonsai polyline tool.
The options are shown in the bottom of Blender's Snapping panel.
2025-01-20 10:56:22 -03:00
Bruno Perdigão 9e81a3da17 Rename "Edge-Intersection" to "Edge Intersection" 2025-01-20 10:56:22 -03:00
Bruno Perdigão b509374c27 Add initial curve snapping functionality 2025-01-20 10:56:22 -03:00
Gorgious56 704bb27cf5 You can now user ALT + Click to ensure an object is unlinked from non-IFC collections when assigning a new container 2025-01-20 14:25:55 +01:00
Dion Moult 7b62b035c9 Fix #5964. Only get the unit assignment from the project to prevent ambiguity.
I really don't know why I didn't do this before.
2025-01-20 23:50:03 +11:00
Dion Moult c5fe80db7b Make georeferencing decorator use soft limits. Don't scale font. 2025-01-20 23:48:51 +11:00
Andrej730 3ffc0bc39c append_asset to use custom file.add to prevent duplicating profiles and materials #5925 2025-01-20 16:23:31 +05:00
Andrej730 0a266bba5d Fix #6000 2025-01-20 16:16:39 +05:00
Andrej730 ecf0d255f7 Roll back build from a65aa92 #6001 2025-01-20 16:16:39 +05:00
Andrej730 c0499a2c88 black . 2025-01-20 16:16:38 +05:00
John Yani 3ad247b9a7 Automatic build and deploy bonsai docs on push (#5994)
* Create docs-deployment.yml

* Update docs-deployment.yml

Add CNAME

* Update docs-deployment.yml

* Create docs-deployment-unstable.yml

* Add requirements.txt
2025-01-20 21:13:21 +11:00
Bruno Postle 13b93a9ea8 Move cache_dir from site-packages to local folder (#5856)
Previously cache_dir was located under site-packages, which is managed
by blender and could be read-only. Now location defaults to eg.
~/.cache/bonsai or equivalent location on other platforms. See #5564

Note this adds a dependency on the platformdirs python module
2025-01-20 13:14:51 +05:00
Dion Moult ebdfe2bccd Fix #5319. Workaround for upstream bpypolyskel bug where dormer ridges were not flat. 2025-01-20 17:17:37 +11:00
Dion Moult 942c3fec87 Add missing bin directory 2025-01-20 14:35:50 +11:00
Dion Moult 651c24bbfe See #5799. Fix segfault in upstream Blender when unlinking material objects in specific scenario.
I can't explain this one so I'm pretty embarrassed to commit this, but
it crashes (see situation in #5799) on my machine and I can't recreate
it outside this scenario. In any case I think as a general strategy we
should:

 1. Minimise reliance on msgbus
 2. Msgbus listeners themselves should be wary of invalid state (e.g. if
object is now unlinked)
 3. Not clear listeners (don't overreach)
2025-01-20 14:01:11 +11:00
Dion Moult b2213592ea Fix #5799. Also purge presentation style assignment when purging unused styles. 2025-01-20 13:59:00 +11:00
Dion Moult c311b65af5 See #5799. Allow merging unnamed styles.
Styles are special because their names aren't critically semantic. It's
more to do with the style definition.
2025-01-20 13:58:39 +11:00
Dion Moult e8b097e2cb See #5799. Remove slow / unnecessary material criticals that also have a risk of crashing
In general Blender on my machine at least sometimes has huge lag with
material previews.
2025-01-20 13:57:07 +11:00
Dion Moult 193ff1cd0c Mention builds website in docs. 2025-01-20 12:15:49 +11:00
Dion Moult a65aa92c87 See #5999. Revert to older bot build before speed regression. 2025-01-20 12:15:36 +11:00
Dion Moult 6af19bc98d Fix 3x slower major speed regression introduced in 98a9550260
The commit that introduced the problem was trying to optimise rendering
dense meshes with the opening decorator. However, it introduced more
calculation in the initial project load and stored more data on every
object. This resulted in models loading 3x slower for me which is pretty
major. The original problem needs further investigation.
2025-01-20 11:24:19 +11:00
Dion Moult 5769c0d442 Fix #5754. Cosmetic bug where UI was not refreshed when using advanced mode causing incomplete decorators to be drawn. 2025-01-20 00:25:29 +11:00
Dion Moult 5c058dff3e Fix #5766. Bump IOS. 2025-01-19 21:49:51 +11:00
Dion Moult f86e414055 Forgot to add a placement for new elements which is invalid. 2025-01-19 21:38:52 +11:00
Dion Moult c17febac0c Fix #5996. Bug where getting selected or active objects was not clear.
Sometimes we want to fetch selected objects, and that includes the
active object, even if the active object isn't actually highlighted in
the viewport (albiet rare, I think?).

Conversely sometimes we want to get the active object, even if it isn't
actually highlighted. The tool.Blender functions now have kwargs to
distinguish between these.
2025-01-19 17:17:56 +11:00
Dion Moult 2d811a6786 Fix fetching layers in item mode. Apparently the inverse name changes slightly.
Maybe because an item has a single layer whereas the shape is meant to
have many layers?
2025-01-19 17:09:32 +11:00
Dion Moult 6eed5607c4 Fix #5972. Purge old non-item-based boolean code 2025-01-19 17:09:02 +11:00
Dion Moult 0b46a31c25 Add item menu is now tailored for CSG representations.
I find this a bit of a strange behaviour in IFC. Imagine you start with
a Tessellation representation type. You're only allowed meshy things, no
solid extrusion. And vice versa if you have a SweptSolid representation
type.

Now imagine you suddenly create a boolean. Now, you can mix both
tessellations and solids together.
2025-01-19 16:58:10 +11:00
Dion Moult 4d7f078f8b Ensure item decorator is updated when editing things in item mode. 2025-01-19 16:56:31 +11:00
Dion Moult 046f7cbfd2 See #5972. You can now select booleans recursively. 2025-01-19 13:54:40 +11:00
Bruno Postle 4152641d1f Ensure files in libs/bin are executable
When blender extracts wheels it doesn't preserve executable permissions
2025-01-18 21:17:43 +00:00
Bruno Postle ec55c8183f remove duplicate 'libs' folder
The Bonsai installer contained a 'libs' folder in both the ZIP bundle
and the bonsai wheel, but only the one in the wheel was actually used
2025-01-18 19:59:57 +00:00
Bruno Postle b54741dd0b Merge branch 'v0.8.0' of github.com:IfcOpenShell/IfcOpenShell into v0.8.0 2025-01-18 19:18:15 +00:00
Bruno Postle b4190876b0 Actually put ifcmerge in the linux ZIP installer 2025-01-18 19:16:50 +00:00
Bruno Perdigão 864eeb42e9 Add filling objectcs to OverrideMove 2025-01-18 15:15:54 -03:00
Bruno Perdigão 7fcb7c6433 Allow moving the current aggregate/nest when aggregate/nest mode is active 2025-01-18 12:17:28 -03:00
Bruno Perdigão 015d8f6722 Add nest mode operators to workspace UI. 2025-01-18 12:06:00 -03:00
Bruno Perdigão c07c2d7692 Complement changes from abd05e939a 2025-01-18 11:48:37 -03:00
Dion Moult a57a0eb4eb See #5972. Prioritise clipping results where possible. 2025-01-19 01:09:11 +11:00
Dion Moult e40644c4f6 See #5972. You can now remove boolean operations in the stack without deleting operands. 2025-01-19 01:08:47 +11:00
Dion Moult 8c746b1a2c Validate representation type items when leaving item mode. 2025-01-19 00:30:30 +11:00
Dion Moult 8e98aecc0b Document add boolean API 2025-01-19 00:30:30 +11:00
Bruno Postle 34a2d95524 Bonsai offers to install Git and ifcmerge (#5961)
The ZIP installer now contains a `libs/bin` folder that contains
`ifcmerge`. Bonsai on `register()` adds this folder to the system `PATH`
for the current Blender session. Advantage of this is that we don't have
to fiddle with the registry on windows, or install files to
`~/.local/bin` on Linux, it _should_ work on Darwin, updates are
automatic, and we have a mechanism to ship other executables if
required.

(Note that ifcmerge.exe increases the size of the Windows Bonsai ZIP
download by about 7MB)

If Git isn't installed on Windows, the Git panel now offers to install
it from the Windows Package Manager Community Repository using `winget`.
2025-01-18 10:22:45 +00:00
Dion Moult a12427e44a See #5972. Add tests for adding boolean and check against recursive booleans and invalid boolean items. 2025-01-18 21:13:54 +11:00
Dion Moult 35734f4b67 Guess representation type from items based on where rule
Previously we never implemented this in a very strict way so it was
prone to mistakes.
2025-01-18 18:35:13 +11:00
Dion Moult c54ea1e5d2 Shape builder can now create blocks 2025-01-18 18:34:40 +11:00
Bruno Perdigão abd05e939a Add nest decorator 2025-01-17 23:00:05 -03:00
Bruno Perdigão d02c5ca1e6 Rename OverridaAggregateMove to OverrideMove.
It now deals with nests too.
2025-01-17 22:58:29 -03:00
Bruno Perdigão a5afba03ab Add "nest mode" similar to "aggregate mode". 2025-01-17 22:47:11 -03:00
Bruno Perdigão 3f914cdf27 Remove unused handler from AggregateModeDecorator 2025-01-17 22:43:05 -03:00
Bruno Perdigão 1d03e916f3 Fix after c4f85dffa4 2025-01-17 17:45:21 -03:00
Bruno Perdigão fd279234f3 Fix #5818 2025-01-17 16:41:01 -03:00
Bruno Perdigão a44439655c Install Bonsai decorators from Viewport Overlays in load_post. 2025-01-17 16:23:40 -03:00
Bruno Perdigão c4f85dffa4 Fix OverrideMoveAggregate to work for multiple layers of aggregates. 2025-01-17 15:51:26 -03:00
Bruno Perdigão 8d63845f3d Change AddSlabFromWalls to consistently create walls in a counterclockwise direction.
This guarantees that the external face of the walls aligns with the edges of the slab.
2025-01-17 15:09:57 -03:00
Thomas Krijnen e0e24948be Update build_rocky.yml - safe.directory 2025-01-17 14:07:36 +01:00
Thomas Krijnen 84420a8973 Don't allow empty sha for add_commit_sha 2025-01-17 13:57:14 +01:00
Dion Moult 7ee055f4f3 See #5972. You can now add half space solid items in item mode. 2025-01-17 23:02:32 +11:00
Andrej730 1f436bcec6 replace date picker util operators with wm.context_set_string
Since they were just setting a scene property which can be done by a default operator.
2025-01-17 17:00:06 +05:00
Andrej730 468c1a2580 Highlight currently selected date in date picker
Example - https://imgur.com/a/ItNg9sZ
2025-01-17 17:00:06 +05:00
Andrej730 e8cb9e7f3c Date picker to support IfcDateTime
See https://imgur.com/a/9a7JfMO
2025-01-17 17:00:06 +05:00
Andrej730 fa98a7e8f7 black . 2025-01-17 17:00:06 +05:00
Andrej730 686a5e7387 Fix #5988 after 6fb0f5e63a 2025-01-17 15:24:02 +05:00
Dion Moult e1da08579c See #5972. You can now add booleans in item editing mode, with support for all three boolean operators. 2025-01-17 20:55:27 +11:00
Dion Moult 0a87e92dcf Move geoference visualisation UI into panel header to be consistent. 2025-01-17 20:54:06 +11:00
Dion Moult 24b3cfc067 Fix #5897. Fix #5488. Fix #5498. Bug where you couldn't edit newly added annotation.
The annotation adding code was a bit of a mess, spread around
create_annotation, create_annotation_occurrence, and
bpy.ops.bim.add_annotation. I've now consolidated it all into
core.add_annotation and updated it to work with the new item editing
mode (basically new objects need to reload their representation to
populate item_ids). Things are still messy, but a bit less now.
2025-01-17 20:53:29 +11:00
Thomas Krijnen c163b260cf Try solve #5989 2025-01-17 10:45:21 +01:00
Andrej730 9e50c89c01 Work schedule UI - use cached data instead of accesing IFC 2025-01-16 18:30:44 +05:00
Andrej730 b28a3b6e32 Basic header for work schedule panel header #5928
Example - https://i.imgur.com/9thZQCX.png
2025-01-16 18:30:44 +05:00
Andrej730 9573628bf6 fix typo in sequence/prop.py and use more consistent name for props
Issue was that taskcolumns_enum doesn't exist
2025-01-16 18:30:44 +05:00
Andrej730 5f034bf4a9 bim.copy_railing_parameters - add poll message 2025-01-16 17:36:12 +05:00
Andrej730 de6e54040a FilledOpeningGenerator - update only parts instead of the full decomposition
similar to 0662b0c
2025-01-16 17:36:12 +05:00
Andrej730 50b39fdd10 bim.clone_opening - update affected representations 2025-01-16 17:36:12 +05:00
Andrej730 eef04fb91c bim.clone_opening - always show in opening's ui
Otherwise it was hidden until user guesses that 2 objects need to be selected for it to appear.
Also, not sure if it's useful operator anymore - it seems just duplicating an opening and then adding it does the same thing.
2025-01-16 17:36:11 +05:00
Andrej730 a383f60915 Show voided elements in UI
E.g. show voided wall when window or wall's opening is selected.
Example - https://i.imgur.com/jxjWPeE.png
2025-01-16 17:36:11 +05:00
Andrej730 44131a7fe5 Select filling objects from UI
Example - https://i.imgur.com/BijTwgo.png
2025-01-16 17:36:11 +05:00
Andrej730 c66f2131e2 bim.select_entity - move to bim module
as it can be generally useful, not just in tester
2025-01-16 17:36:11 +05:00
Andrej730 035999d0c3 bim.update_openings_focus - remove unnecessary ifc operator 2025-01-16 17:36:11 +05:00
Andrej730 61d68b74ca bim.add_filled_opening - remove unused operator 2025-01-16 17:36:10 +05:00
Andrej730 133459faec typo 2025-01-16 17:36:10 +05:00
Andrej730 6fb0f5e63a typing 2025-01-16 17:36:10 +05:00
Andrej730 2859e9632e black . 2025-01-16 17:36:10 +05:00
Andrej730 521a8183e7 Fix #5953 after d6e6a6d 2025-01-16 17:36:09 +05:00
Dion Moult a63f67ad32 Fix #5841. Incorrect axis usage when generating LAYER2 elements from polyline. 2025-01-16 23:19:11 +11:00
Dion Moult aeba2885e4 Fix #5984. Incorrect axis usage when generating slabs. 2025-01-16 23:11:51 +11:00
Dion Moult f50ae2faa5 See #5972. You can now load a hierarchy to visualise the boolean result tree. 2025-01-16 22:58:17 +11:00
Dion Moult bd739f8bfd Fix undo bug when marking booleans as manual 2025-01-16 22:39:37 +11:00
Dion Moult 5b2243089d See #5972. You can now delete boolean operands in item editing mode. 2025-01-16 22:10:28 +11:00
Dion Moult 2fe4680ca1 See #5972. Boolean operands are now loaded in item editing mode and are editable. 2025-01-16 21:28:05 +11:00
Taku Makoni 21624d6065 Wrong Import Package (#5982) 2025-01-16 12:20:59 +11:00
Taku Makoni b9f40342b2 Grammatical error 2025-01-16 11:52:09 +11:00
Bruno Perdigão 3e879c6156 Change how new objects added are handled in aggregate mode.
When in aggregate mode, new objects will now be automatically assigned to
the current aggregate.
2025-01-15 16:11:27 -03:00
Bruno Perdigão c360b91f4a Remove constraints from the aggregates. 2025-01-15 16:11:27 -03:00
Andrej730 23b1a71cba Support adding duplicated existing boolean
Example - https://imgur.com/a/rRQz9dx
2025-01-15 18:41:21 +05:00
Andrej730 5d17554d04 bim.add_boolean - support adding multiple booleans at once
Example - https://imgur.com/a/IWDqMRT
2025-01-15 18:41:20 +05:00
Andrej730 1f3fc3c2b9 bim.add_openings - display in UI if multiple potential openings are selected 2025-01-15 18:41:20 +05:00
Andrej730 d6da8a7ee1 Support duplicating potential openings and potential/existing booleans 2025-01-15 18:41:20 +05:00
Andrej730 cfb4cd8af9 bim.add_potential_opening - remove unused code
draw_settings() was never used
2025-01-15 18:41:20 +05:00
Andrej730 7eff4db4eb reuse add_tracked_opening 2025-01-15 18:41:19 +05:00
Andrej730 689688c76b Fix error selecting material usage in Materials UI
Previously it would error, now it selects related profile / layer set.
2025-01-15 18:41:19 +05:00
Andrej730 0ea513818b Support assigning material sets as usages from UI #5933
It's on alt+click in Materials UI, see example - https://imgur.com/a/8gP7iAZ
2025-01-15 18:41:19 +05:00
Andrej730 2fdcb41c4e black . 2025-01-15 18:41:19 +05:00
Dion Moult 65c99c57cd Prefill class / product type if using a particular BIM tool for convenience, and hide fields. 2025-01-15 22:04:58 +11:00
Dion Moult fe54b24f5f Fix bug where box decoration when adding empty geometry failed 2025-01-15 22:04:24 +11:00
Dion Moult 7fdfdbf8b4 Fix #5971. You can now specify a name and description when adding objects. Clean naming and description is critical! 2025-01-15 20:57:19 +11:00
Dion Moult 53906d824a Fix #5977. Silly mistake I forgot all([]) is still True. 2025-01-15 20:47:56 +11:00
Thomas Krijnen a687a90039 Break out multiple tasks per representation when rep is directly reused #5978 2025-01-15 10:24:52 +01:00
Thomas Krijnen c37fd6ae0b Don't throw exception but set to $ when encountering invalid instance name #5680 2025-01-15 10:08:31 +01:00
Dion Moult 06e8e2b35c Remove obsolete separate_verts_layer when marking edge angles for roofs
New roof system automatically handles this scenario (using poke
operator) so no need to mark it.
2025-01-15 19:46:34 +11:00
Dion Moult fd51b9a9e9 Fix #5968. Fix #4040. Rewrote roof generation using per face extrusions and cutting planes instead of edge sliding.
WARNING! The roof generation is now quite significantly different. This
WILL change your geometry when you refresh roofs from existing models.

 - The roof profile now always represents the top of eave. This better
represents how things are built rather than bottom of eave which was
previous.
 - The roof therefore always grows down from the profile. The rafter
edge angle can only be acute. This means that the roof will never grow
larger than the footprint profile that you've drawn. (before, the
footprint was not guaranteed to match).
 - The roof thickness is now the actual thickness of the roof, not the
"vertical dimension" of the roof. This means that the roof thickness can
now match intended layer thicknesses instead of you needing to do math
to work it out.

I've rewritten the internals of how roofs were generated to be hopefully
a lot simpler but as a tradeoff it's more restrictive. After the
skeleton is generated, non-uniform angles would be handled through
vertex splitting and moving. This works in simple scenarios but fails in
more complex ones. The new approach only handles non-uniform angles on
triangular faces. These faces are really easy to handle compared to
ngons, but are also more robust. The rafter edge angle is also now
handled using a clipping plane, which is a lot, lot simpler than vertex
sliding math.

Each face is now processed separately and then merged at the end. This
means that if a face has a different angle, it will now correctly
represent the different thickness at that portion of the roof.

In the process consistent roof thickness / rafter angle bugs were fixed.
2025-01-15 19:43:39 +11:00
Andrej730 3f2f296d5f Fix error with Shift-E #5975
E.g. two objects are selected - tesselation and profile and tesellation being active object. Using s-e with deselect tesselation object as it doens't have a usage but Bonsai would still try to run bim.enable_editing_extrusion_profile on it leading to the error.

Also fix possible issues for hotkey executed without active object (it seems all operators in hotkey do require active object)
2025-01-14 17:42:03 +05:00
Andrej730 020937835a Fix issue enabling ifcsverchok if sverchok installed under a different name #5913 2025-01-14 17:42:03 +05:00
Andrej730 a52e3615d6 Map ifc2x3 IfcAnnotationOccurrence to IfcStyledItems in ifc4 #5917 2025-01-14 17:41:58 +05:00
Andrej730 adb61c5a16 bonsai makefile - use new option for old no-clobber behaviour
Apparently (https://debbugs.gnu.org/cgi/bugreport.cgi?bug=62572) coreutils have changed `mv -n` to throw an error now instead of working silently, therefore our build was failing on Mac when Github Action switched from Ubuntu 22 to 24.
`--update=none` seems to restore the old behaviour.
2025-01-14 17:41:58 +05:00
Andrej730 a50973ede7 typing 2025-01-14 17:41:58 +05:00
Andrej730 74ca86678a ifcfm cobie - color code ods file tabs too 2025-01-14 13:48:50 +05:00
Gorgious56 ac2e69bd66 Don't close type manager when using the enum search feature while adding a new type
Also reduce the attribute name a bit
2025-01-14 08:44:21 +01:00
Dion Moult 595a3b3f87 Preselect IFC class when adding elements for convenience. 2025-01-14 14:22:15 +11:00
Dion Moult 48f534e7c1 Say "Type" instead of None in the general BIM tool. 2025-01-14 14:13:39 +11:00
Dion Moult 1ed770d454 Fix #5921. Bug when parsing IDS XML of facets with no attributes. 2025-01-14 13:36:33 +11:00
Dion Moult ec600ec72e IfcTester can now read from XML strings in addition to opening files on disk 2025-01-14 13:35:45 +11:00
Dion Moult adcc23541a Fix bug where starting a new file in an existing session would prompt an error.
Bpy.context.active_object is not available in-between loading / starting
a new file.
2025-01-14 13:35:01 +11:00
Dion Moult 9a77c94758 Fix #5967. Incorrect logic in the for/else block when adding new elements.
The for/else block was too greedy, meaning that if nothing was selected,
it would still try to create slabs from walls. I find for/else confusing
so I restructured the section to do more early returns and unindent the
logic.
2025-01-14 12:20:22 +11:00
Moritz Amberger e89d195f9b moved full_schema, is_strcit, should_expand, should_get_inverse, should_get_psets. should_get_geometry and should_skip_geometry_data to the Patcher class definition. deleted those parameters from the patch definition so that users are able to set them by themselves. removed the hardcoded tmp-File for the db_file name 2025-01-14 09:30:34 +11:00
tim 228e9d0571 Refactor detect_icon_color_mode to accept attribute path suffix
•	Removed bpy.context.preferences.themes[0] from the input and hardcoded it in the function.
	•	color_path now only requires the attribute path suffix (e.g., "user_interface.wcol_regular.text").
	•	Updated error handling to default to "dm" instead of "lm".
2025-01-14 09:22:53 +11:00
tim 7280669725 Typo 2025-01-14 09:22:53 +11:00
tim 83b8bb153d One function for model and cad custom icons
Apply detect_icon_color_mode function to model and cad workspaces
2025-01-14 09:22:53 +11:00
tim 88891409fd New icon color mode function
New function for determining which icon color mode to use: dark (dm) or light (lm)
2025-01-14 09:22:53 +11:00
tim b6ba1aaa8c Hide shortcuts from header 2025-01-14 09:22:22 +11:00
ti ffb3d2d6e9 check for None 2025-01-14 09:22:03 +11:00
Dion Moult efede9eb03 No idea why Github actions is failing for MacOS. All code looks good to me. 2025-01-14 00:14:03 +11:00
Andrej730 ac315e8a55 ifcfm cobie 2.4 - color code excel tabs
example - https://i.imgur.com/tSeqyjj.png
2025-01-13 17:53:02 +05:00
Andrej730 b7c18221f4 get_bbox - optimize by using numpy arrays 2025-01-13 17:53:02 +05:00
Andrej730 7a49f1611e get_shape_matrix - small optimization
reuse existing matrix instead of recreating new
2025-01-13 17:53:02 +05:00
Andrej730 5c0a7af830 cobie24 - implement Coordinate tab #5926 2025-01-13 17:53:02 +05:00
Andrej730 a968b61d02 np_matrix_to_euler, np_matrix_normalized 2025-01-13 17:53:01 +05:00
Andrej730 0ac9d739f8 remove unused code 2025-01-13 17:53:01 +05:00
Andrej730 74c6480808 bim.add_element to select added object 2025-01-13 17:53:01 +05:00
Andrej730 0601802005 typing 2025-01-13 17:53:01 +05:00
Dion Moult 9784fb1c49 Pyodide example app should use IfcShapeRepresentation to be valid 2025-01-13 23:12:49 +11:00
Gorgious56 039dcbaefa Stair Modifier : You can now set a total length target that will update individual tread runs automatically. You can lock the total length target and modify tread run or number of treads to keep the same global dimensions. 2025-01-13 12:04:13 +01:00
Gorgious56 1cd17fd5f8 Merge pull request #5947 from IfcOpenShell/update_type_manager
Update Type Manager
2025-01-13 09:40:42 +01:00
Gorgious56 2f0f9fe08b Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into update_type_manager 2025-01-13 09:38:41 +01:00
Gorgious56 50ed75fcc2 Fix potential bug where a class doesn't have a predefined type 2025-01-13 09:32:23 +01:00
Gorgious56 9eaf41c9f0 The filter search field now also filters description and predefined type 2025-01-13 09:28:56 +01:00
Dion Moult 581a674654 Fix #5952. Aggressively block scaling of IFC elements to prevent user confusion. 2025-01-13 18:01:25 +11:00
Dion Moult e29f686d3f Update setting literal from old name of "debug" to new name "debug-boolean". 2025-01-13 15:45:27 +11:00
Dion Moult 8cf7057abc Fix #5954. Special automatic movement of features shouldn't then also move children of those features.
If you move a wall, and that wall has features (e.g. openings), it's
desirable to also move those openings (because they are invisible). This
is a special exception to `should_transform_children` because the
definition of the feature (opening) is inherently tied to the parent
(wall).

What wasn't considered is that this would typically then also move
subchildren of the features (e.g. fills like doors). I'm surprised
nobody caught this earlier.

I did also consider another approach where if you move a wall, it moves
all unfilled openings, and if you move a door which fills a opening, it
moves the opening too. Intuitively it sounds nice, but it doesn't work
because:

 - Openings can have multiple fillings. If you move all fillings, they
all fight to move the openings.
 - All logic about children goes one way: a placement may have child
placements relative to it. This breaks the convention (if moving a door
instead moves its opening) which can make brains explode.
 - It starts to conflate rules about relative / referenced placements
with spatial decomposition. We assume all IFCs are valid and follows the
convention of relative placement but we cannot guarantee this. This also
leads to brain explosion.
2025-01-13 15:44:52 +11:00
Dion Moult 316b9554d4 Fix #5959. Bug where default selection basket in selector query was not added per facet list.
Previously the code looked for additive facets (entity, instance)
anywhere in the query, and then only added the default basket once at
the beginning of the query. This change makes it look for additive
facets in order (that way you could start with a default selection, and
then later use an additive facet) and does it per facet list (so you can
omit the additive facet at the beginning of each list).
2025-01-13 12:46:44 +11:00
tim 95fa6f39c4 New furniture icon
Chair icon for furniture
2025-01-13 10:47:35 +11:00
Thomas Krijnen 65ffbe081b Pyodide app readme 2025-01-12 15:50:19 +01:00
Thomas Krijnen 936984dd31 Update publish-pyodide-demo-app.yml 2025-01-12 15:16:57 +01:00
Thomas Krijnen 590c6eed0a Update publish-pyodide-demo-app.yml 2025-01-12 15:12:44 +01:00
Thomas Krijnen a696c7bbb5 Update publish-pyodide-demo-app.yml 2025-01-12 15:10:59 +01:00
Thomas Krijnen 6ff1d06938 Create publish-pyodide-demo-app.yml 2025-01-12 15:07:44 +01:00
Thomas Krijnen 6deb7121cc demo-app/CNAME 2025-01-12 15:00:54 +01:00
Thomas Krijnen 6aec2f7a02 pyodide/app-demo 2025-01-12 14:50:20 +01:00
Dion Moult 0eb987fc07 Black 2025-01-12 22:43:41 +11:00
Dion Moult 04092d19be Refactor common function for parametric objects into tool.Model 2025-01-12 22:43:34 +11:00
Dion Moult 8364c87777 Fix #5941. Context override when adding objects should only have one object. This is a temporary fix.
The proper fix is to refactor add_door and friends into core. That way
it can be properly reused instead of this hacky context override.
2025-01-12 22:39:32 +11:00
Dion Moult 2d38d8520c Fix regression where you couldn't move spatial elements from the new aggregate move override. 2025-01-12 21:47:42 +11:00
Dion Moult 03d3b1e52c Changed my mind, remove AssignConstituentFractions patch. See #5441. 2025-01-12 17:51:32 +11:00
Dion Moult 79fe1bfcfc Minor fix from new patch recipe in PR #5441 (src no longer required) 2025-01-12 17:45:43 +11:00
Louis Trümpler 59c1d7fa54 Add AssignConstituentFractions recipe 2025-01-12 17:41:50 +11:00
tim f0e1be765d New Roof and Railing icons
https://github.com/IfcOpenShell/IfcOpenShell/issues/5375
2025-01-12 17:38:57 +11:00
Lucas Nascimento a2bfc006b7 black formatting 2025-01-12 17:25:21 +11:00
Lucas Nascimento dff9f56ce3 black formatting and changes in shader typing 2025-01-12 17:25:21 +11:00
LM-Nascimento 80996dee1f Update unit.py 2025-01-12 17:25:21 +11:00
Lucas Nascimento a329de9f71 licence comment 2025-01-12 17:25:21 +11:00
Lucas Nascimento 1918138f68 cleanup 2025-01-12 17:25:21 +11:00
Lucas Nascimento aa21fe9060 separate shader file from decoration data 2025-01-12 17:25:21 +11:00
Lucas Nascimento 6eee445502 Cleaning up and type hints 2025-01-12 17:25:21 +11:00
Lucas Nascimento 3eb29af496 seems to be working 2025-01-12 17:25:21 +11:00
Lucas Nascimento 2912580087 shit ton of changes 2025-01-12 17:25:21 +11:00
Lucas Nascimento 477725529b now with shader for point loads 2025-01-12 17:25:21 +11:00
Lucas Nascimento d915dc9df0 update decorator to match shader + changes to text rendering 2025-01-12 17:25:21 +11:00
Lucas Nascimento 53a2b87c97 updates to shader class 2025-01-12 17:25:21 +11:00
Lucas Nascimento d10b327c5f Initial commit, able to show structural curve action on the sample file of IFC documentation 2025-01-12 17:25:21 +11:00
Radek Hlaváček 293cd83a9c Update report.html
Adding "Skipped" grey colored container for specification and requirement if no applicable element is found
2025-01-12 17:14:22 +11:00
ti 07817ec6fc updated restriction types 2025-01-12 17:13:36 +11:00
c4rlosdias d1ad082979 Add derived unit 2025-01-12 17:12:20 +11:00
Thomas Krijnen ba9693519b Remove mathutils import from shape_builder.py 2025-01-12 17:05:32 +11:00
Dion Moult 41362e1582 Fix #5950. IfcTester now considers optional requirement facets as per latest documentation.
The IDS documentation used to state that "optional" on requirement
facets was "for information only" and didn't affect the pass / fail
state. This was changed a while ago (which I missed) to a new behaviour:
both no values and passing values will pass.
2025-01-12 16:28:14 +11:00
Dion Moult 8e77011dda Fix #5949. Remove material set psets panel in favour of merging into material manager psets panel.
Previously, to edit a single IfcMaterial pset, you'd do so in the
material manager. In contrast, to edit a IfcMaterialSet pset, you'd do
so only when assigned to an object. Not only is it bad to edit psets in
two locations, it also means you can't edit psets until you've first
assigned the material set to an object which is bad. Now it's less code,
and material / material set psets are in the same spot.

There still more consolidation work to do, to allow editing of sets in
the material manager and to figure out how to show item psets, but it's
a step in the right direction.
2025-01-12 15:45:02 +11:00
Dion Moult 02632440b0 See #5949. Only show material psets and classifications if actually editing in the material manager.
Previously, you could disable editing and it would still show irrelevant
psets or classifications.
2025-01-12 15:45:02 +11:00
Bruno Postle 0a4ab32810 Fix missing carriage return
SyntaxWarning: invalid escape sequence '\I'
2025-01-11 13:36:54 +00:00
Dion Moult c446aab9a0 Fix #5951. Regression when removing mathutils dependency from shape builder and parametric stairs were expecting Vector and instead got numpy. 2025-01-11 15:39:13 +11:00
Dion Moult 2e7cbb759e Fix #5955. Regression in 0983f812fd where you couldn't remove a style that wasn't being edited. 2025-01-11 14:51:18 +11:00
Gorgious56 68f175fc71 Fix bug when trying to add an ifc type occurence from the toolbar in the properties editor 2025-01-10 15:42:37 +01:00
Gorgious56 a428169a13 Fix bug when duplicating an ifc type 2025-01-10 15:29:31 +01:00
Gorgious56 7db0196791 Fix bug where the thumbnail of the active class wasn't updated 2025-01-10 15:17:26 +01:00
Gorgious56 4f4c557f61 Another attempt.
Went back to previous toolbar : icon + type name + dot grid icon which all launch the type manager.
    Validating the type manager popup now only closes it. Functionally is the same thing as clicking Cancel. Don't really know what to do to fix it. Does cancel make sense in the type manager ? Do we want to be able to undo what is possible to be done from within the type manager ?
    You can change the active type either with the dropdown in the popup and in the grid flow. I changed it so that changing the type in the dropdown jumps to the corresponding page. BTW first time using github copilot to figure out the maths and it worked magically :) . I'm not against removing the dropdown but I feel like it's nice to see a plain list to choose from.
    You can input a specific page number or scrub the page number field. Might be usefull if user has dozens of pages to flip through.
    Right now the search is very simple, it does not implement fuzzy search or anything fancy with *
2025-01-10 14:24:33 +01:00
Dion Moult d88adf2d02 See #5938. Correctly detect negative extrusion depths and directions. 2025-01-10 22:48:11 +11:00
Dion Moult 956b7dcb92 Fix #5941. Regression in ae22eec9 where active object wasn't considered when adding a parametric door. 2025-01-10 21:50:52 +11:00
Dion Moult 0a32149be3 Fix #5944. You can now tab / edit IfcAnnotationFillArea with the new item editing mode. 2025-01-10 19:49:04 +11:00
Gorgious56 aafb6fc426 Add search bar with simple filter function to the type manager 2025-01-10 09:44:12 +01:00
Dion Moult cac3d7f699 Fix #5939. Fix #2751. IfcPatch no longer needs the (rarely used) input src arg by default. 2025-01-10 16:59:45 +11:00
Dion Moult 83add56e6c Fix #5949. When duplicating objects, copy over IFC links (shape representation ID, item_ids, edge_item_ids) in a more reliable way 2025-01-10 15:22:53 +11:00
Dion Moult e4412e2f51 Black 2025-01-10 15:21:18 +11:00
Dion Moult 4a1422d292 New annotations should not go into the unsorted collection 2025-01-10 14:13:28 +11:00
Stephen Boddy 1e448cb170 Add mini-guide to setting up IDE for debugging 2025-01-10 12:34:56 +11:00
Bruno Perdigão 07bb7e68dd Fix #5932 2025-01-09 16:14:11 -03:00
Bruno Perdigão cf054e11a1 Refactor: improve preview decorators for other products, following 420a4cfdc 2025-01-09 15:59:15 -03:00
Bruno Perdigão 6b8024f835 Refactor: Improve profile preview decorator to prevent GPU context errors 2025-01-09 15:59:15 -03:00
Bruno Perdigão 66577fd30d Prevent polyline profile from being called by Column Tool 2025-01-09 15:59:15 -03:00
Bruno Perdigão 0124b7a0e9 Allow polyline profile for IfcCoveringType 2025-01-09 15:59:15 -03:00
Bruno Perdigão 737402cae4 Fix profile scale issue in profile preview decorator. 2025-01-09 15:59:15 -03:00
Bruno Perdigão 8b1d4ca5d4 Implement polyline tool for profiles. 2025-01-09 15:59:15 -03:00
Gorgious56 a6e953a550 Update Type Manager
Add back the type dropdown and looking glass icon in the toolbar
Clicking on a type in the manager no longer closes the popup anymore.
A visual indictor is displayed on the active type
Changed the parameters icon in the individual type interface

Fix bug in "Slab from Walls" Shift + A code
2025-01-09 17:45:40 +01:00
Dion Moult 40dcfd450f See #5940. Fix bug where using preserve existing in IfcCSV didn't fully purge extra rows, possibly leading to orphaned data (which LibreOffice will render as 0 if it is a number value type) 2025-01-09 18:25:00 +11:00
Dion Moult e220b6981e See #5940. Fix bug where ODS and XLSX didn't correctly check for whether summaries existed and sometimes printed out "None" rows 2025-01-09 18:24:09 +11:00
Dion Moult 84931fbbd2 See #5941. Don't show mode changes if an object is unselectable.
Unselectable things should be uneditable. That's what the lock/unlock is
for.
2025-01-09 17:40:52 +11:00
Gorgious56 2d5bc49ee0 Going in and out of AGGREGATE mode doesn't overwrite other objects' display type anymore 2025-01-08 16:53:59 +01:00
Gorgious56 d4ff789688 You can now mass enable edition, disable edition and validate edition of Ifc Entity Attributes.
This highlights a potential bug that already existed due to the way the attribute system is designed. Where the user mass-edits attributes from different IFC types, resulting in discrepancies due to the same attributes having a different index. eg IfcWindow has the Tag attribute at index 3, IfcDoor at index 4. So using the ALT modifier feature from vanilla Blender to mass-modify similarly named attributes on the Tag attribute of the IfcWindow will modify the PredefinedType attribute on the IfcDoor (index 3). For this reason I added the ALT modifier to the buttons so mass-operations are a conscious effort for more knowledgeable users. Might reconsider if problems arise but this is IMO a good QOL feature.

Also fix floating point precision errors on Pset float values stored as strings (re last commit)
2025-01-08 11:29:06 +01:00
Gorgious56 5c3f0f0df9 Fix floating precision artifacts when displaying float values in the Attributes panel
Minor fixes on previous commit
2025-01-08 10:28:27 +01:00
Bruno Perdigão 519e667464 Fix bug with bim.aggregate_assign_new_objects_in_aggregate_mode. 2025-01-07 18:17:32 -03:00
Bruno Perdigão e3abfd60cb Typo. 2025-01-07 18:16:59 -03:00
Bruno Perdigão 864e8d3a48 Typo. 2025-01-07 18:05:23 -03:00
Bruno Perdigão d2b05ea3dc Fix bug related to constraints and aggregate duplication 2025-01-07 18:00:47 -03:00
Thomas Krijnen d063bdbbf1 wasm wheel fixes 2025-01-07 20:56:16 +01:00
Thomas Krijnen a42a49e088 wasm wheel fixes 2025-01-07 20:47:25 +01:00
Gorgious56 ae22eec958 Door modifier operators now apply to all elligible selected objects. This makes it way easier to modify a single attribute in several unrelated parametric doors thanks to the ALT + input feature from vanilla Blender. This allows for mass-removing, mass-removing, mass-editing, mass-validating, and mass-cancelling edition of door modifiers.
Also allow UNDO for some door operators which seems like they should support it
And Fix #5937 : minor typo
2025-01-07 17:43:43 +01:00
Bruno Perdigão b657eee564 Change extrusion direction when converting to extrusion in Representation Utilities. 2025-01-07 12:41:15 -03:00
Thomas Krijnen 9d1daa4dc5 ifcpatch - try both out of tree and subpackage imports #5939 2025-01-07 15:43:44 +01:00
Bruno Perdigão dbf33283ab Add Bonsai decorators toggle to Blender Viewport Overlays.
https://imgur.com/auQcp2Y
2025-01-07 11:32:14 -03:00
Gorgious56 0983f812fd You can now use the Asset Browser to set a Material Asset as an external style.
To do so Right Click on a material in the Asset Browser then `Set To External Style`. The style editor must be loaded in the properties editor.

Also run black on a file from previous commit and fix a bug soft-locking the style editor when removing a style that is being edited
2025-01-07 14:55:17 +01:00
Thomas Krijnen 3ced6e7bb1 wasm wheel fixes 2025-01-07 13:23:10 +01:00
Richard Brice 933d845472 Fixes IfcPolynomialCurve when used as IfcCurveSegment.ParentCurve in horizontal alignments 2025-01-06 14:36:37 -08:00
Gorgious56 8313c05884 Merge pull request #5930 from Gorgious56/improve_modifiers
Modifiers QOL
2025-01-06 12:10:26 +01:00
Gorgious56 f6e588b48c You can now copy roof parameters from active to selected objects 2025-01-06 11:35:17 +01:00
Gorgious56 547c5fb8a9 TAB now applies modifier parameters on the active object. ESCAPE now cancels editing modifier parameters on the active object. 2025-01-06 11:06:41 +01:00
Gorgious56 570e29cfe4 Add operator to copy railing modifier parameters (#3413)
* You can now copy railing properties from active to selected

Hitting ESCAPE cancels editing railing path
2025-01-06 10:10:54 +01:00
Thomas Krijnen 2e35b07fa6 Fetch boost from github releases in build scripts 2025-01-06 10:05:59 +01:00
Thomas Krijnen 1b6ee26854 Don't fail on empty ReferencedSource in IfcTester 2025-01-05 14:40:27 +01:00
Thomas Krijnen 7b21858cd4 Inform about supported reporter types in IfcTester 2025-01-05 14:37:41 +01:00
Thomas Krijnen 12800bbeee IFCPATCH_RECIPE_DIR to allow loading out of tree recipes 2025-01-05 11:07:48 +01:00
Gorgious56 3a7488d54b Fix #3261 : You can now purge unused openings that do not intersect with their related building element.
It's a naive implementation and very inefficient. Moreover there may be false positives because it's testing overlapping the evaluated mesh element with all openings applied.
2025-01-03 17:37:46 +01:00
Gorgious56 7bfa926e6d Fix error when instantiating a wire mesh 2025-01-03 16:41:05 +01:00
Gorgious56 fd7a7d44c3 Special geometry data on meshes are also loaded as geometry attributes.
No functional change apart from in dissolve_triangulated_edges where it will try to load the attribute if it can. I believe it will be easier in the long term to use the builtin Attributes system. Unfortunately Curves don't support Attributes yet. Also, attributes are more lightweight compared to custom properties.
2025-01-03 16:12:51 +01:00
Gorgious56 fe6a6d1a0d You can now disable aggregate mode with ESCAPE in the 3D Viewport
- Fix minor typo
2025-01-03 15:04:28 +01:00
Gorgious56 98a9550260 Fix slowness when rendering dense meshes with the opening decorator
It uses a custom boolean attribute on the mesh to decide whether an edge should be displayed or not. I think later on it can be used to get rid of the dictionary accessors which make the blend file size skyrocket.
2025-01-03 14:12:16 +01:00
Gorgious56 fe253af5ae Don't draw triangulation artifact edges in the opening decorator 2025-01-03 13:04:37 +01:00
Gorgious56 e933ff08f8 Use relevant tool.Geometry functions for querying openings relations 2025-01-03 12:22:08 +01:00
Gorgious56 25af639dae Fix memory leaks due to orphaned meshes in the opening and boolean operations 2025-01-03 11:30:42 +01:00
Gorgious56 5b3e3fc646 Fix not being able to apply or hide specific openings 2025-01-03 10:54:54 +01:00
Gorgious56 57dbea2974 Fix #3854 : Assigning a class to a potential opening removes it from the openings pool
Also fix an error in previous commit
2025-01-03 10:33:41 +01:00
Gorgious56 cc3074cdcc #5919 : Add an option to set the opacity of the rest of the model when modifying openings
Option is located in the preferences
2025-01-03 10:01:36 +01:00
Gorgious56 4c9697d260 Consolidate logic for toggling display of openings 2025-01-03 09:59:47 +01:00
Gorgious56 8dfcea0e24 Fix internal openings list not being cleared when applying an opening 2025-01-03 09:42:55 +01:00
Gorgious56 f0fac7ad8b Reorganize addon preferences layout
Hopefully it's a little bit less daunting
2025-01-03 09:40:08 +01:00
Bruno Perdigão f6c34ac196 Refactor enable_constraints for performance reasons.
`context.view_layer.update()` was being called for every object in the loop.
Now it is just called after all the constraints are enabled.
2025-01-02 17:11:43 -03:00
Bruno Perdigão 5ebaaa6c8c Minor fix to disable_aggregate_mode(). 2025-01-02 17:11:43 -03:00
Bruno Perdigão 9838a54173 Minor fix in aggregate decorator. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 2c3c0b9d95 Improve performance for TAB into aggregate mode.
Use disable/enable constraint instead of `constraint.apply`. It keeps the
parts location with matrix calculation
2025-01-02 17:11:43 -03:00
Bruno Perdigão 548f6e0566 Improve performance for bim.aggregate_assing_new_objects_in_aggregate_mode()
Add constrain only to new objects instead of rebuilding all constraints from scratch.
2025-01-02 17:11:43 -03:00
Bruno Perdigão 3874d19caa Allow aggregate to me moved while in aggregate mode without moving its parts. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 87fee0b850 Add features for handling newly created objects in aggregate mode
- Highlight new objects in red when in aggregate mode
- Introduce an operator to add new objects to the current aggregate, accessible via a button in the header menu
2025-01-02 17:11:43 -03:00
Bruno Perdigão 3bccc993b7 Small fix in aggregate decorator. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 31ae2c5437 Create UI operators for aggregate mode in model workspace. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 85c48f6a63 Small refactor of aggregate decorator. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 833cc0604c Improve performance in aggregate mode by removing hide_select.
Additionally, update rendering of objects outside the aggregate to use
 'WIRE' instead of 'BOUNDS'.
2025-01-02 17:11:43 -03:00
Bruno Perdigão 3dd83598e9 Prevent TAB from editing objects outside the aggregate in aggregate mode. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 3050abdb87 Small fix. 2025-01-02 17:11:43 -03:00
Bruno Perdigão f7c4b13d0a Aggregate decorator refactor. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 91d3b8161e Integrate aggregate mode toggle with Bonsai's TAB edit mode override. 2025-01-02 17:11:43 -03:00
Bruno Perdigão e9cc205ddd Enable aggregate decorator by default on file startup. 2025-01-02 17:11:43 -03:00
Bruno Perdigão c48f50a67e Small fix in aggregate move selection. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 6d9ee8828e Small fix 2025-01-02 17:11:43 -03:00
Bruno Perdigão 28c95a7d9a Remove debug print statements. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 9a6ff7c37c Add bounding box decorator over aggregates. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 226ea10de8 Add aggregate mode decorator. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 3c590f097a Prevent aggregate objects to be edited by TAB when not in aggregate mode. 2025-01-02 17:11:43 -03:00
Bruno Perdigão cbdbd5bd83 Add operator to toggle aggregate mode on and off. 2025-01-02 17:11:43 -03:00
Bruno Perdigão ccdf1cee0a Hide selection of objects outside the aggregate when in "aggregate edit mode". 2025-01-02 17:11:43 -03:00
Bruno Perdigão 21d3b1d472 Small fix 2025-01-02 17:11:43 -03:00
Bruno Perdigão 0c7a0a6bda Add aggregate decorator. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 3dcded819f Add new way to move objects that are a part of an aggregate.
Now, when trying to move an object that is a part of an aggregate, the
`OverrideMoveAggregate` will be called, meaning that it will move the whole
aggregation. It works by creating constraints and moving the aggregate instead
of the selected object.

It also introduces the idea of editing an aggregate (no hotkey yet). When active, the user
will be able to move the parts of the aggregate instead of the whole aggregation: https://imgur.com/IcCGtPN
2025-01-02 17:11:43 -03:00
Bruno Perdigão 6f11f74aac Apply aggregate constraints instead of removing. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 0daf4c9324 Remove constraints when object is unassigned from the aggregate. 2025-01-02 17:11:43 -03:00
Bruno Perdigão 7f838f81b4 Add aggregate constraints to import_ifc.py 2025-01-02 17:11:43 -03:00
Bruno Perdigão cf9446c4a5 Constrain part objects to aggregate object when creating aggregations. 2025-01-02 17:11:43 -03:00
Richard Brice 26ba761c68 Refactors alignment geometry 2025-01-02 11:10:56 -08:00
Gorgious56 bb9be861c8 Fix blend file being saved twice in a row with CTRL-S
This caused a noticeable hiccup when a large file is saved twice on a network drive. Also it messed up the backup system.
2025-01-02 14:32:29 +01:00
Gorgious56 ad815b697e Fix #5570 : Regen button now appears in the toolbar for windows and doors 2025-01-02 14:12:26 +01:00
Gorgious56 ca90ddc0b4 Fix 3134 : Flip shortcut is now displayed in the toolbar for doors and windows 2025-01-02 14:02:00 +01:00
Gorgious56 fb35e80be0 Editing the representation of an opening now updates the gpu shader accordingly 2025-01-02 13:06:14 +01:00
Gorgious56 21a62116a8 You can now hide all openings by pressing ESCAPE in the 3D Viewport
- Also fix CTD on UNDO after pressing ESCAPE due to previous commit
2025-01-02 11:17:48 +01:00
Gorgious56 c2708b13d9 Minor fix on previous commit to correctly go out of ifc edit mode without saving representation 2025-01-02 10:54:55 +01:00
Gorgious56 b949efe8ba Fix #5849 : Use Keymap item instead of modal operator for override escape in 3D viewport
- Enables the vanilla Blender autosave feature back, since a running modal operator deactivates it
- ESC is not used by any other vanilla keymap in the 3D viewport (may be conflict with other addons)
- Hitting ESC over another type of editor (eg enum field in the properties editor) will not trigger disabling ifc item mode
- You can now hit ESC in IFC edit mode to cancel editing the geometry and go back to ifc object mode
2025-01-02 10:27:52 +01:00
Thomas Krijnen 463289e462 pyodide build (#5916) 2024-12-31 16:09:54 +01:00
260 changed files with 10190 additions and 3100 deletions
+4 -2
View File
@@ -25,7 +25,9 @@ jobs:
- name: Build
run: |
find .. -maxdepth 2
VERSION=`cat IfcOpenShell/VERSION`
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/meta.yaml
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/setup.py
echo '#!/usr/bin/bash' > script.sh
echo 'cd pyodide' > script.sh
echo 'make && pip install ./pyodide-build' >> script.sh
@@ -38,7 +40,7 @@ jobs:
chmod +x script.sh
sed -i s/--tty// pyodide/run_docker
pyodide/run_docker ./script.sh
mv dist/IfcOpenShell-0.8.0-py3-none-any.whl dist/ifcopenshell_python-v0.8.0+${GITHUB_SHA:0:7}-cp312-cp312-emscripten_3_1_58_wasm32.whl
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp312-cp312-emscripten_3_1_58_wasm32.whl
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
+1
View File
@@ -17,6 +17,7 @@ jobs:
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils
git config --global --add safe.directory '*'
- name: Install aws cli
run: |
@@ -0,0 +1,38 @@
name: Build and Deploy Unstable Documentation
on:
push:
branches:
- main # Trigger the workflow on pushes to the main branch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
cd src/bonsai/docs # Navigate to the docs directory
pip install -r requirements.txt # Install dependencies from requirements.txt
- name: Build documentation
run: |
cd src/bonsai/docs # Navigate to the docs directory
make html # Build the documentation
- name: Deploy to GitHub Pages (Unstable)
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} # SSH key for deployment
external_repository: IfcOpenShell/bonsaibim_org_docs_unstable # Target repository
publish_branch: main # Branch to deploy to
cname: docs-unstable.bonsaibim.org # Custom domain for unstable docs
publish_dir: src/bonsai/docs/_build/html # Directory containing built docs
+36
View File
@@ -0,0 +1,36 @@
name: Build and Deploy Stable Documentation
on:
workflow_dispatch: # Manual trigger
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
cd src/bonsai/docs
pip install -r requirements.txt # Run pip install from the docs directory
- name: Build documentation
run: |
cd src/bonsai/docs
make html
- name: Deploy to GitHub Pages (Stable)
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: IfcOpenShell/bonsaibim_org_docs
publish_branch: main
cname: docs.bonsaibim.org
publish_dir: src/bonsai/docs/_build/html
@@ -0,0 +1,48 @@
name: Deploy Pyodide Demo App to GitHub Pages
permissions:
id-token: write
pages: write
on:
push:
paths:
- 'src/pyodide/**'
- '.github/workflows/publish-pyodide-demo-app.yml'
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v3
with:
path: src/pyodide/demo-app/
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+2
View File
@@ -98,3 +98,5 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# Brickschema
src/bonsai/bonsai/bim/schema/Brick.ttl
bonsaiDecoratorForLoads.code-workspace
dev_environment.bat
+3
View File
@@ -20,3 +20,6 @@
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
path = src/ifcopenshell-python/ifcopenshell/simple_spf
url = https://github.com/IfcOpenShell/step-file-parser
[submodule "src/pyodide/demo-app/wheels"]
path = src/pyodide/demo-app/wheels
url = https://github.com/IfcOpenShell/wasm-wheels
+1
View File
@@ -6,6 +6,7 @@ IfcOpenShell
<img src="https://github.com/IfcOpenShell/IfcOpenShell/assets/88302/34901387-e2dd-4a0c-8e38-9ffc32a66cde">
</p>
IfcOpenShell is an open source ([LGPL]) software library for working with Industry Foundation Classes ([IFC]). Complete
parsing support is provided for [IFC2x3 TC1], [IFC4 Add2 TC1], IFC4x1, IFC4x2, and [IFC4x3 Add2]. Extensive geometric support
is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending with support for arbitrary IFC schemas
+8
View File
@@ -1148,6 +1148,7 @@ if(ADD_COMMIT_SHA)
message("git found: ${GIT_EXECUTABLE} with version ${GIT_VERSION_STRING}")
execute_process(
COMMAND ${GIT_EXECUTABLE} branch -a --contains HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE git_branches
OUTPUT_STRIP_TRAILING_WHITESPACE
)
@@ -1165,11 +1166,18 @@ if(ADD_COMMIT_SHA)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE git_sha
OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(STATUS "IfcOpenShell branch: \"${git_branch}\"")
message(STATUS "IfcOpenShell commit: \"${git_sha}\"")
if ("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
message(FATAL_ERROR "Unable to determine commit sha and/or branch")
endif()
add_definitions(-DIFCOPENSHELL_BRANCH=${git_branch})
add_definitions(-DIFCOPENSHELL_COMMIT=${git_sha})
endif()
+2 -2
View File
@@ -307,7 +307,7 @@ if platform.system() == "Darwin":
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
OCE_LOCATION = f"https://github.com/tpaviot/oce/archive/OCE-{OCE_VERSION}.tar.gz"
BOOST_LOCATION = f"https://boostorg.jfrog.io/artifactory/main/release/{BOOST_VERSION}/source/"
BOOST_LOCATION = f"https://github.com/boostorg/boost/releases/download/boost-{BOOST_VERSION}/"
# Helper functions
@@ -747,7 +747,7 @@ if "boost" in targets:
download_url=BOOST_LOCATION,
# don't remember what this is, but fail on 1.86
# patch="./patches/boost/boostorg_regex_62.patch",
download_name=f"boost_{BOOST_VERSION_UNDERSCORE}.tar.bz2"
download_name=f"boost-{BOOST_VERSION}-b2-nodocs.tar.gz"
)
if "wasm" in flags:
# only supported on nix for now
+2 -2
View File
@@ -1,11 +1,11 @@
from setuptools import setup, find_packages
setup(name='IfcOpenShell',
setup(name='ifcopenshell',
version='0.8.0',
description='IfcOpenShell is an open source (LGPL) software library for working with the Industry Foundation Classes (IFC) file format.',
author='Thomas Krijnen',
author_email='thomas@aecgeeks.com',
url='http://ifcopenshell.org',
packages=find_packages(),
package_data={'': ['*.so']},
package_data={'': ['*.so', '*.json']},
)
+15 -4
View File
@@ -85,7 +85,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=a2538fd
OLD:=f425794
.PHONY: bump
bump:
ifndef NEW
@@ -177,6 +177,8 @@ endif
# Is not platform specific but has platform specific dependencies.
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download python-socketio[asyncio_client] $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download aiohttp $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
# Required to access platform specific paths
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download platformdirs --dest=./wheels
# Required by light module
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pytz --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download tzfpy $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
@@ -226,8 +228,16 @@ endif
)" bdist_wheel
cp -r build/bpypolyskel-master/dist/*.whl build/wheels/
# Required for Desktop icon and file association
cp -r bonsai/libs/desktop build/bonsai/libs/
# folder for executable files
mkdir -p build/bonsai/libs/bin
# required for three-way git merging
ifeq ($(PLATFORM), win)
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2024-06-24/ifcmerge.zip
cd build/bonsai/libs/bin && unzip ifcmerge.zip && rm ifcmerge.zip
else
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge
endif
# Generate translations module for Bonsai build
git clone https://github.com/IfcOpenShell/bonsai-translations.git build/working
@@ -330,7 +340,7 @@ endif
# Use '-n' as on macos x86-64 doesn't have a binary wheel and it autoincludes 'cp311'.
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/-cp39-/-cp$(PYNUMBER)-/"); \
mv -n "$$prev_whl_name" "$$whl_name";
mv --update=none "$$prev_whl_name" "$$whl_name";
ifneq ($(PLATFORM), linux)
# Safeguard: in case one of `pip download` will break,
@@ -348,6 +358,7 @@ endif
rm -rf build/bonsai/bim/
rm -rf build/bonsai/core/
rm -rf build/bonsai/tool/
rm -rf build/bonsai/libs/
ifeq ($(IS_STABLE), TRUE)
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-$(BLENDER_PLATFORM).zip ./bonsai
Binary file not shown.
+10
View File
@@ -99,6 +99,7 @@ classes = [
operator.BIM_OT_add_section_plane,
operator.BIM_OT_delete_object,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_entity,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.BIM_OT_multiple_file_selector,
@@ -115,6 +116,7 @@ classes = [
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
operator.SelectDataDir,
operator.SelectCacheDir,
operator.SelectIfcFile,
operator.SelectSchemaDir,
operator.SelectURIAttribute,
@@ -136,6 +138,7 @@ classes = [
prop.BIMMeshProperties,
prop.BIMFacet,
prop.BIMFilterGroup,
prop.BIMSnapProperties,
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.BIM_UL_topics,
@@ -191,6 +194,8 @@ classes = [
# TODO: move this somewhere else and clean it up
ui.BIM_PT_section_plane,
ui.BIM_PT_section_with_cappings,
ui.BIM_PT_decorators_overlay,
ui.BIM_PT_snappping,
]
for mod in modules.values():
@@ -223,6 +228,7 @@ def register():
bpy.app.handlers.load_post.append(handler.load_post)
bpy.app.handlers.load_post.append(handler.loadIfcStore)
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
bpy.types.Scene.BIMSnapProperties = bpy.props.PointerProperty(type=prop.BIMSnapProperties)
bpy.types.Screen.BIMAreaProperties = bpy.props.CollectionProperty(type=prop.BIMAreaProperties)
bpy.types.Screen.BIMTabProperties = bpy.props.PointerProperty(type=prop.BIMTabProperties)
bpy.types.Collection.BIMCollectionProperties = bpy.props.PointerProperty(type=prop.BIMCollectionProperties)
@@ -257,6 +263,10 @@ def register():
icons = icon_preview
bpy.app.translations.register("bonsai", translations_dict)
import bonsai.tool as tool
tool.Blender.ensure_bin_in_path()
def unregister():
global icons
+21 -3
View File
@@ -30,6 +30,10 @@ from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos, degrees
from typing import Union, Callable
@@ -134,7 +138,7 @@ def update_bim_tool_props():
if AuthoringData.data["active_material_usage"] == "LAYER2":
x_angle = get_x_angle(extrusion)
axis = tool.Model.get_wall_axis(obj)["reference"]
props.extrusion_depth = extrusion.Depth * si_conversion * cos(x_angle)
props.extrusion_depth = abs(extrusion.Depth * si_conversion * cos(x_angle))
props.length = (axis[1] - axis[0]).length
props.x_angle = x_angle
@@ -305,8 +309,6 @@ def load_post(scene):
# After appending the workspace to ensure BIM viewport is affected.
subscribe_to_viewport_shading_changes()
bpy.ops.bim.override_escape("INVOKE_DEFAULT")
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
@@ -318,3 +320,19 @@ def load_post(scene):
if tool.Ifc.get() and bpy.data.is_saved:
bpy.context.scene.BIMProperties.has_blend_warning = True
# Bonsai overlays
georeference_props = bpy.context.scene.BIMGeoreferenceProperties
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = bpy.context.scene.BIMModelProperties
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
if aggregate_props.aggregate_decorator:
AggregateDecorator.install(bpy.context)
if nest_props.nest_decorator:
NestDecorator.install(bpy.context)
if model_props.show_wall_axis:
WallAxisDecorator.install(bpy.context)
if model_props.show_slab_direction:
SlabDirectionDecorator.install(bpy.context)
+27 -3
View File
@@ -98,9 +98,10 @@ def draw_attribute(
if attribute.is_uri:
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.data_path = attribute.path_from_id("string_value")
elif attribute.special_type == "DATE":
elif attribute.special_type in ("DATE", "DATETIME"):
op = layout.operator("bim.datepicker", text="", icon="TIME")
op.target_prop = attribute.path_from_id("string_value")
op.include_time = attribute.special_type == "DATETIME"
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
@@ -170,6 +171,8 @@ def import_attribute(
new.is_uri = True
elif attribute.type_of_attribute()._is("IfcDate"):
new.special_type = "DATE"
elif attribute.type_of_attribute()._is("IfcDateTime"):
new.special_type = "DATETIME"
elif data_type == "boolean":
new.bool_value = False if new.is_null else bool(data[attribute.name()])
elif data_type == "integer":
@@ -254,11 +257,25 @@ def export_attributes(
ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties]
def get_display_value(value: str, float_decimal_precision: int = 6) -> str:
"""
This will get rid of the floating point precision artifacts in float values stored as a string
"""
try:
digits = len(value.split(".")[1])
value = float(value)
if digits > 6: # Maximal decimal float precision
value = round(value, float_decimal_precision)
except (ValueError, IndexError): # Not castable to a float or no decimal places (eg integer)
pass
return str(value)
def prop_with_search(
layout: bpy.types.UILayout,
data: ENUM_ITEMS_DATA,
prop_name: str,
should_click_ok_to_validate: bool = False,
should_click_ok: bool = False,
original_operator_path: Optional[str] = None,
**kwargs: Any,
):
@@ -271,7 +288,7 @@ def prop_with_search(
row.context_pointer_set(name="data", data=data)
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
op.prop_name = prop_name
op.should_click_ok_to_validate = should_click_ok_to_validate
op.should_click_ok = should_click_ok
op.original_operator_path = original_operator_path or ""
except TypeError: # Prop is not iterable
pass
@@ -315,6 +332,13 @@ def get_enum_items(
return items
def draw_expandable_panel(layout, context, label: str, ui_func, default_closed: bool = True):
header, panel = layout.panel(label, default_closed=default_closed)
header.label(text=label)
if panel:
ui_func(panel, context)
def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip_props: tuple[str, ...] = ()) -> None:
"""Method converts property group values from si to current ifc project units
+3 -5
View File
@@ -121,7 +121,8 @@ class IfcStore:
if IfcStore.cache is None and IfcStore.path:
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")
os.makedirs(bpy.context.scene.BIMProperties.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
@@ -161,7 +162,7 @@ class IfcStore:
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")
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
@@ -404,9 +405,6 @@ class IfcStore:
obj.BIMStyleProperties.ifc_definition_id = 0
else: # bpy.types.Object
obj.BIMObjectProperties.ifc_definition_id = 0
# NOTE: in theory this will also remove listeners added by other addons
# though never had a report when this would be a problem.
bpy.msgbus.clear_by_owner(obj)
@staticmethod
def execute_ifc_operator(operator: tool.Ifc.Operator, context: bpy.types.Context, is_invoke=False) -> set[str]:
+2 -18
View File
@@ -260,8 +260,6 @@ class IfcImporter:
self.profile_code("Setup arrays")
tool.Project.load_linked_models_from_ifc()
self.profile_code("Load linked models")
self.lock_scales()
self.profile_code("Lock objects scales")
self.add_project_to_scene()
self.profile_code("Add project to scene")
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
@@ -895,9 +893,9 @@ class IfcImporter:
tool.Loader.set_unit_scale(self.unit_scale)
def set_units(self):
if not (units := self.file.by_type("IfcUnitAssignment")):
if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext):
return # Geometry is optional in IFC
for unit in units[0].Units:
for unit in assignment.Units:
if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT":
if unit.is_a("IfcSIUnit"):
bpy.context.scene.unit_settings.system = "METRIC"
@@ -1118,20 +1116,6 @@ class IfcImporter:
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
def lock_scales(self) -> None:
elements = set(self.file.by_type("IfcProduct"))
while elements:
element = elements.pop()
if not getattr(element, "HasOpenings", False):
continue
voided_elements = tool.Aggregate.get_parts_recursively(element)
voided_elements.add(element)
elements.difference_update(voided_elements)
for element in voided_elements:
if not (obj := tool.Ifc.get_object(element)):
continue
tool.Geometry.lock_scale(obj)
class IfcImportSettings:
def __init__(self):
@@ -30,7 +30,11 @@ classes = (
operator.BIM_OT_aggregate_unassign_object,
operator.BIM_OT_break_link_to_other_aggregates,
operator.BIM_OT_select_linked_aggregates,
operator.BIM_OT_disable_aggregate_mode,
operator.BIM_OT_toggle_aggregate_mode_local_view,
prop.BIMObjectAggregateProperties,
prop.Objects,
prop.BIMAggregateProperties,
ui.BIM_PT_aggregate,
ui.BIM_PT_linked_aggregate,
)
@@ -38,7 +42,9 @@ classes = (
def register():
bpy.types.Object.BIMObjectAggregateProperties = bpy.props.PointerProperty(type=prop.BIMObjectAggregateProperties)
bpy.types.Scene.BIMAggregateProperties = bpy.props.PointerProperty(type=prop.BIMAggregateProperties)
def unregister():
del bpy.types.Object.BIMObjectAggregateProperties
del bpy.types.Scene.BIMAggregateProperties
@@ -0,0 +1,330 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2024 Bruno Perdigão <contact@brunopo.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 blf
import bpy
import gpu
import ifcopenshell
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
from bonsai.bim.module.geometry.decorator import ItemDecorator
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
def create_bounding_box(objs):
# Initialize the bounding box coordinates
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
max_x, max_y, max_z = float("-inf"), float("-inf"), float("-inf")
# Iterate over the selected objects
for obj in objs:
if not obj.data:
continue
# Get the object's bounding box coordinates
bbox_world = [obj.matrix_world @ Vector(b) for b in obj.bound_box]
obj_min = []
obj_min.append(min([b[0] for b in bbox_world]))
obj_min.append(min([b[1] for b in bbox_world]))
obj_min.append(min([b[2] for b in bbox_world]))
obj_max = []
obj_max.append(max([b[0] for b in bbox_world]))
obj_max.append(max([b[1] for b in bbox_world]))
obj_max.append(max([b[2] for b in bbox_world]))
# Update the overall bounding box coordinates
min_x = min(min_x, min(obj_min[0], obj_max[0]))
min_y = min(min_y, min(obj_min[1], obj_max[1]))
min_z = min(min_z, min(obj_min[2], obj_max[2]))
max_x = max(max_x, max(obj_min[0], obj_max[0]))
max_y = max(max_y, max(obj_min[1], obj_max[1]))
max_z = max(max_z, max(obj_min[2], obj_max[2]))
indices = [
(min_x, min_y, min_z),
(max_x, min_y, min_z),
(max_x, max_y, min_z),
(min_x, max_y, min_z),
(min_x, min_y, max_z),
(max_x, min_y, max_z),
(max_x, max_y, max_z),
(min_x, max_y, max_z),
]
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)]
return indices, edges
class AggregateDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_aggregate, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "u_ViewProjectionMatrix")
shader_info.push_constant("FLOAT", "u_Scale")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "FLOAT", "arcLength")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.push_constant("VEC4", "color")
shader_info.vertex_source(
"void main()"
"{"
" v_ArcLength = arcLength;"
" gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f);"
"}"
)
shader_info.fragment_source(
"void main()" "{" " if (step(sin(v_ArcLength * u_Scale), 0.4) == 1) discard;" " FragColor = color;" "}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
def draw_custom_batch(self, coords, color):
shader = self.dotted_line_shader()
arc_lengths = [0]
for a, b in zip(coords[:-1], coords[1:]):
arc_lengths.append(arc_lengths[-1] + (a - b).length)
batch = batch_for_shader(
shader,
"LINE_STRIP",
{"position": coords, "arcLength": arc_lengths},
)
matrix = bpy.context.region_data.perspective_matrix
shader.uniform_float("color", color)
shader.uniform_float("u_ViewProjectionMatrix", matrix)
shader.uniform_float("u_Scale", 25)
batch.draw(shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_aggregate(self, context):
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
decorator_color_error = self.addon_prefs.decorator_color_error
decorator_color_unselected = self.addon_prefs.decorator_color_unselected
decorator_color_background = self.addon_prefs.decorator_color_background
theme = context.preferences.themes.items()[0][1]
selected_object_color = (*theme.view_3d.object_active, 1)
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
aggregates = []
if not (selected_objects := context.selected_objects):
return
for obj in selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
return
parts = ifcopenshell.util.element.get_parts(element)
if parts:
aggregates.append(obj)
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregates.append(tool.Ifc.get_object(aggregate))
aggregates = set(aggregates)
for aggregate in aggregates:
self.line_shader.uniform_float("lineWidth", 1.0)
color = decorator_color_unselected
if aggregate in selected_objects:
color = selected_object_color
size = aggregate.empty_display_size
location = aggregate.location
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
self.line_shader.uniform_float("lineWidth", 0.5)
self.draw_batch("LINES", indices, color, edges)
line = (Vector(indices[0]), location)
self.draw_custom_batch(line, decorator_color_unselected)
class AggregateModeDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_aggregate_name, (context,), "WINDOW", "POST_PIXEL")
)
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
)
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_aggregate_name(self, context):
if context.mode == "EDIT_MESH":
return
region = context.region
rv3d = region.data
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
font_size = tool.Blender.scale_font_size(12)
blf.size(self.font_id, font_size)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorator_color_selected
if aggregate_obj in context.selected_objects:
color = self.addon_prefs.decorator_color_selected
blf.color(self.font_id, *color)
text = aggregate_obj.name
text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, aggregate_obj.location)
text_length = blf.dimensions(self.font_id, text)
text_coords[0] -= text_length[0] / 2
text_coords[1] -= 20
blf.position(self.font_id, text_coords[0], text_coords[1], 0)
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
blf.draw(self.font_id, text)
def draw_aggregate_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
theme = context.preferences.themes.items()[0][1]
selected_object_color = (*theme.view_3d.object_active, 1)
color = self.addon_prefs.decorator_color_selected
if aggregate_obj in context.selected_objects:
color = selected_object_color
size = aggregate_obj.empty_display_size
location = aggregate_obj.location
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
if parts:
for part in parts:
part_obj = tool.Ifc.get_object(part)
if part.is_a("IfcElementAssembly"):
self.line_shader.uniform_float("lineWidth", 1.0)
size = part_obj.empty_display_size
location = part_obj.location
color = self.addon_prefs.decorator_color_unselected
if part_obj in context.selected_objects:
color = selected_object_color
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
color = self.addon_prefs.decorator_color_selected
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
self.line_shader.uniform_float("lineWidth", 0.5)
self.draw_batch("LINES", indices, color, edges)
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
import bonsai.core.spatial
from bonsai.bim.ifc import IfcStore
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
@@ -66,6 +67,10 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
relating_obj=relating_obj,
related_obj=obj,
)
props = context.scene.BIMAggregateProperties
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
new_editing_obj = props.editing_objects.add()
new_editing_obj.obj = obj
except core.IncompatibleAggregateError:
self.report({"ERROR"}, f"Cannot aggregate {obj.name} to {relating_obj.name}")
except core.AggregateRepresentationError:
@@ -365,3 +370,33 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
obj.select_set(True)
return {"FINISHED"}
class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
bl_idname = "bim.disable_aggregate_mode"
bl_label = "Disable Aggregate Mode"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
bl_idname = "bim.toggle_aggregate_mode_local_view"
bl_label = "Toggle Aggregate Mode Local View"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMAggregateProperties
objs = [o.obj for o in props.editing_objects]
if props.in_aggregate_mode:
if context.space_data.local_view:
bpy.ops.view3d.localview()
else:
for obj in objs:
obj.select_set(True)
bpy.ops.view3d.localview()
return {"FINISHED"}
@@ -32,6 +32,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from bonsai.bim.module.aggregate.decorator import AggregateDecorator, AggregateModeDecorator
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
@@ -73,6 +74,20 @@ def poll_related_object(self: "BIMObjectAggregateProperties", related_obj: bpy.t
return True
def update_aggregate_decorator(self, context):
if self.aggregate_decorator:
AggregateDecorator.install(bpy.context)
else:
AggregateDecorator.uninstall()
def update_aggregate_mode_decorator(self, context):
if self.in_aggregate_mode:
AggregateModeDecorator.install(bpy.context)
else:
AggregateModeDecorator.uninstall()
class BIMObjectAggregateProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object)
@@ -82,3 +97,20 @@ class BIMObjectAggregateProperties(PropertyGroup):
type=bpy.types.Object,
poll=poll_related_object,
)
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
class BIMAggregateProperties(PropertyGroup):
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
editing_objects: CollectionProperty(type=Objects)
not_editing_objects: CollectionProperty(type=Objects)
aggregate_decorator: BoolProperty(
name="Display Aggregate",
default=False,
update=update_aggregate_decorator,
)
@@ -47,6 +47,9 @@ class BIM_PT_aggregate(Panel):
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text="Aggregate Decorator")
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
if not AggregateData.is_loaded:
AggregateData.load()
@@ -28,20 +28,33 @@ import bonsai.core.attribute as core
from bonsai.bim.ifc import IfcStore
def get_objs_for_operation(operator_properties, context):
if operator_properties.obj:
return [bpy.data.objects[operator_properties.obj]]
if operator_properties.mass_operation:
return context.selected_objects[:]
return [context.active_object]
class EnableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects[self.obj]
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def enable_editing_attribute_on_obj(self, obj):
props = obj.BIMAttributeProperties
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
assert element
if not element:
return
has_inherited_predefined_type = False
if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)):
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
@@ -67,31 +80,48 @@ class EnableEditingAttributes(bpy.types.Operator):
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
props.is_editing_attributes = True
def execute(self, context):
for obj in get_objs_for_operation(self, context):
self.enable_editing_attribute_on_obj(obj)
return {"FINISHED"}
class DisableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes"
bl_description = "ALT + Left Click to disable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def execute(self, context):
obj = bpy.data.objects.get(self.obj)
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def disable_editing_attributes_on_obj(self, obj):
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
def execute(self, context):
for obj in get_objs_for_operation(self, context):
self.disable_editing_attributes_on_obj(obj)
return {"FINISHED"}
class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_attributes"
bl_label = "Edit Attributes"
bl_description = "ALT + Left Click to edit attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def _execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj)
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def edit_attributes_on_obj(self, obj):
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
@@ -112,6 +142,11 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
if (name := tool.Loader.get_name(product)) and obj.name != name:
obj.name = name
bpy.ops.bim.disable_editing_attributes(obj=obj.name)
def _execute(self, context):
self.file = IfcStore.get_file()
for obj in get_objs_for_operation(self, context):
self.edit_attributes_on_obj(obj)
return {"FINISHED"}
+4 -10
View File
@@ -20,6 +20,7 @@ import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
def draw_ui(context, layout, attributes):
@@ -30,21 +31,18 @@ def draw_ui(context, layout, attributes):
if props.is_editing_attributes:
row = layout.row(align=True)
op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
op.obj = obj.name
op = row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
op.obj = obj.name
bonsai.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
else:
row = layout.row()
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
op.obj = obj.name
for attribute in attributes:
row = layout.row(align=True)
row.label(text=attribute["name"])
# row.label(text=attribute["value"])
op = row.operator("bim.select_similar", text=attribute["value"], icon="NONE", emboss=False)
value = bonsai.bim.helper.get_display_value(attribute["value"])
op = row.operator("bim.select_similar", text=value, icon="NONE", emboss=False)
op.key = attribute["name"]
# TODO: reimplement, see #1222
@@ -62,11 +60,7 @@ class BIM_PT_object_attributes(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
return tool.Ifc.get_entity(context.active_object)
def draw(self, context):
if not AttributesData.is_loaded:
+1 -1
View File
@@ -616,7 +616,7 @@ class AddBcfViewpoint(bpy.types.Operator):
old_file_format = blender_render.image_settings.file_format
blender_render.image_settings.file_format = "PNG"
old_filepath = blender_render.filepath
blender_render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
blender_render.filepath = tool.Blender.get_data_dir_path("snapshot.png").__str__()
bpy.ops.render.opengl(write_still=True)
with open(blender_render.filepath, "rb") as f:
snapshot = f.read()
-1
View File
@@ -31,4 +31,3 @@ class BIMCadProperties(PropertyGroup):
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
gable_roof_separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
+3 -20
View File
@@ -26,24 +26,10 @@ from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
from typing import Union
# TODO duplicate code in cad/workspace and model/workspace
def check_display_mode():
global display_mode
try:
theme = bpy.context.preferences.themes["Default"]
text_color = theme.user_interface.wcol_menu_item.text
if sum(text_color) < 2.6:
display_mode = "lm"
else:
display_mode = "dm"
except:
display_mode = "dm"
def load_custom_icons():
global custom_icon_previews
global custom_icon_previews, display_mode
if display_mode is None:
check_display_mode()
display_mode = tool.Blender.detect_icon_color_mode("user_interface.wcol_tool.text")
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
custom_icon_previews = bpy.utils.previews.new()
@@ -236,7 +222,6 @@ class CadHotkey(bpy.types.Operator):
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
self.layout.row().prop(props, "gable_roof_separate_verts")
elif self.hotkey == "S_V":
if not tool.Geometry.is_profile_object_active():
@@ -285,9 +270,7 @@ class CadHotkey(bpy.types.Operator):
and RoofData.data["pset_data"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(
angle=self.props.gable_roof_edge_angle, separate_verts=self.props.gable_roof_separate_verts
)
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
def hotkey_S_T(self):
bpy.ops.bim.cad_mitre()
@@ -266,7 +266,7 @@ class ExecuteIfcClash(bpy.types.Operator):
context.scene.render.resolution_x = 480
context.scene.render.resolution_y = 270
context.scene.render.image_settings.file_format = "PNG"
context.scene.render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
context.scene.render.filepath = tool.Blender.get_data_dir_path("shapshot.png").__str__()
bpy.ops.render.opengl(write_still=True)
with open(context.scene.render.filepath, "rb") as f:
return ("snapshot.png", f.read())
@@ -313,10 +313,8 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
if not tool.Ifc.get():
return False
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
return True
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
return True
return False
def draw(self, context):
@@ -713,9 +713,9 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
return {"CANCELLED"}
plural_object_type = f"{object_type.lower()}s"
if merged_data:
print(f"Merged {plural_object_type}:")
for element_type, element_names in merged_data.items():
print(f"- {element_type}: {', '.join(element_names)}")
names = ", ".join([n or "Unnamed" for n in element_names])
print(f"- {element_type}: {names}")
merged = sum(len(v) for v in merged_data.values())
msg = " See system console for details." if merged else ""
@@ -22,6 +22,7 @@ import math
import bmesh
import bonsai.tool as tool
import ifcopenshell.util.element
from pathlib import Path
from mathutils import Vector, Matrix
from typing import Optional
@@ -43,7 +44,7 @@ class Annotator:
font = bpy.data.fonts.get("OpenGost TypeB TT")
if not font:
font = bpy.data.fonts.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
)
font.name = "OpenGost Type B TT"
obj.data.font = font
+1 -2
View File
@@ -82,8 +82,7 @@ class SheetsData:
@classmethod
def titleblocks(cls):
files = Path(os.path.join(bpy.context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
files = [str(f.stem) for f in files]
files = [p.stem for p in tool.Blender.get_data_dir_paths(Path("templates") / "titleblocks", "*.svg")]
if tool.Ifc.get():
project = tool.Ifc.get().by_type("IfcProject")[0]
@@ -28,6 +28,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.drawing.helper as helper
from pathlib import Path
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
@@ -2022,9 +2023,9 @@ class DecorationsHandler:
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
drawing_font_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", drawing_font)
if os.path.exists(drawing_font_path):
font_id = blf.load(drawing_font_path)
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
if drawing_font_path.is_file():
font_id = blf.load(drawing_font_path.__str__())
for decorator in self.decorators.values():
decorator.font_id = font_id
@@ -276,7 +276,6 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer = svgwriter.SvgWriter()
self.svg_writer.human_scale = self.human_scale
self.svg_writer.scale = self.scale
self.svg_writer.data_dir = context.scene.BIMProperties.data_dir
self.svg_writer.camera = self.camera
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
self.svg_writer.camera_projection = tuple(
@@ -730,7 +729,7 @@ class CreateDrawing(bpy.types.Operator):
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
ifc_cache_path = os.path.join(context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
@@ -1423,27 +1422,33 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_annotation"
bl_label = "Add Annotation"
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.StringProperty()
data_type: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.scene.camera
return tool.Ifc.get() and context.scene.camera
@classmethod
def description(cls, context, operator):
return operator.description or ""
def _execute(self, context):
drawing = tool.Ifc.get_entity(context.scene.camera)
if not drawing:
props = bpy.context.scene.BIMAnnotationProperties
if not (drawing := tool.Ifc.get_entity(context.scene.camera)):
self.report({"WARNING"}, "Not a BIM camera")
return
r = core.add_annotation(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, object_type=self.object_type)
if isinstance(r, str):
self.report({"WARNING"}, r)
obj = core.add_annotation(
tool.Ifc,
tool.Collector,
tool.Drawing,
drawing=drawing,
object_type=props.object_type,
relating_type=tool.Ifc.get().by_id(int(props.relating_type_id)) if props.relating_type_id != "0" else None,
enable_editing=True,
)
if props.object_type == "IMAGE":
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -1661,7 +1666,6 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.add_drawing(reference, drawing, sheet)
tool.Drawing.import_sheets()
@@ -1691,7 +1695,6 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
sheet = tool.Drawing.get_reference_document(reference)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.remove_drawing(reference, sheet)
tool.Ifc.run("document.remove_reference", reference=reference)
@@ -1754,7 +1757,6 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = scene.BIMProperties.data_dir
references = sheet_builder.build(sheet)
raster_references = [tool.Ifc.get_relative_uri(r) for r in references["RASTER"]]
@@ -2178,7 +2180,7 @@ class ReloadDrawingStyles(bpy.types.Operator):
json_path = Path(tool.Ifc.resolve_uri(rel_path))
if not json_path.exists():
ootb_resource = Path(context.scene.BIMProperties.data_dir) / "assets" / "shading_styles.json"
ootb_resource = tool.Blender.get_data_dir_path(Path("assets") / "shading_styles.json")
print(
f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. "
f"Default BBIM resource will be copied from {ootb_resource}"
@@ -2576,7 +2578,6 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.add_document(reference, schedule, sheet)
tool.Drawing.import_sheets()
@@ -2643,7 +2644,6 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.add_document(reference, extref, sheet)
tool.Drawing.import_sheets()
@@ -3073,7 +3073,6 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
attributes={"Location": tool.Drawing.get_default_titleblock_path(titleblock)},
)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.change_titleblock(sheet, titleblock)
tool.Drawing.import_sheets()
@@ -22,6 +22,7 @@ import bpy
import string
import svgwrite
import openpyxl
import bonsai.tool as tool
from bonsai.bim.module.drawing.svgwriter import SvgWriter
from odf.opendocument import load as load_ods
@@ -77,7 +78,7 @@ class Scheduler:
ifc_file_path = os.path.dirname(IfcStore.path)
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
if not os.path.exists(stylesheet_path):
stylesheet_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "schedule.css")
stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css")
with open(stylesheet_path, "r") as stylesheet:
css = stylesheet.read()
@@ -26,6 +26,7 @@ import urllib.parse
import xml.etree.ElementTree as ET
import bonsai.tool as tool
import ifcopenshell.util.geolocation
from pathlib import Path
from xml.dom import minidom
from mathutils import Vector
import re
@@ -38,7 +39,6 @@ XLINK = "{http://www.w3.org/1999/xlink}"
class SheetBuilder:
def __init__(self):
self.data_dir = None
self.scale = "NTS"
def create(self, layout_path: str, titleblock_name: str) -> None:
@@ -49,7 +49,9 @@ class SheetBuilder:
root.attrib["version"] = "1.1"
sheet_dir = os.path.dirname(layout_path)
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
ootb_titleblock_path = tool.Blender.get_data_dir_path(
Path("templates") / "titleblock" / (titleblock_name + ".svg")
)
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
os.makedirs(sheet_dir, exist_ok=True)
@@ -241,7 +243,7 @@ class SheetBuilder:
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
ootb_title = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "view-title.svg")
ootb_title = tool.Blender.get_data_dir_path(Path("assets") / "view-title.svg")
shutil.copy(ootb_title, title_path)
title_tree = ET.parse(title_path)
@@ -492,7 +494,9 @@ class SheetBuilder:
return group
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
ootb_titleblock_path = tool.Blender.get_data_dir_path(
Path("templates") / "titleblocks" / (titleblock_name + ".svg")
)
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
sheet_dir = os.path.dirname(sheet_path)
@@ -38,6 +38,7 @@ from bonsai.bim.module.drawing.data import DecoratorData
from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector
from typing import Optional, Self
from pathlib import Path
class External(svgwrite.container.Group):
@@ -98,7 +99,7 @@ class SvgWriter:
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
if not os.path.exists(resource_path):
resource_basename = os.path.basename(resource_path)
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
ootb_resource = tool.Blender.get_data_dir_path(Path("assets") / resource_basename)
print(
f"WARNING. Couldn't find {resource} for the drawing by the path: {resource_path}. Default BBIM resource will be copied from {ootb_resource}"
)
@@ -20,7 +20,9 @@
import os
import bpy
import bonsai.core.type
import bonsai.core.drawing as core
import bonsai.tool as tool
import ifcopenshell.util.representation
from bonsai.bim.module.drawing.data import DecoratorData, AnnotationData
from bonsai.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
@@ -161,12 +163,12 @@ def create_annotation_occurrence(context):
obj = tool.Drawing.create_annotation_object(drawing, object_type)
obj.name = relating_type.Name
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing), object_type)
relating_type_repr = tool.Drawing.get_annotation_representation(relating_type)
relating_type_rep = tool.Drawing.get_annotation_representation(relating_type)
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type=object_type,
should_add_representation=not bool(relating_type_repr),
should_add_representation=not relating_type_rep,
context=ifc_context,
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
@@ -175,27 +177,23 @@ def create_annotation_occurrence(context):
tool.Ifc.run("group.assign_group", group=tool.Drawing.get_drawing_group(drawing), products=[element])
tool.Collector.assign(obj)
tool.Blender.select_and_activate_single_object(context, obj)
if relating_type_repr is None and props.object_type == "IMAGE":
if relating_type_rep is None and props.object_type == "IMAGE":
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
def create_annotation():
props = bpy.context.scene.BIMAnnotationProperties
if props.relating_type_id != "0":
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]
if representation := ifcopenshell.util.representation.get_representation(element, ifc_context):
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if props.object_type == "IMAGE":
bpy.ops.bim.add_reference_image(
"INVOKE_DEFAULT", use_existing_object_by_name=bpy.context.active_object.name
)
if obj.data and not relating_type_rep:
tool.Drawing.enable_editing(obj)
class AnnotationToolUI:
@@ -294,21 +292,30 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
def hotkey_S_T(self):
props = bpy.context.scene.BIMAnnotationProperties
annotation_type = props.object_type
object_type = props.object_type
if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for tagging.")
if object_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
self.report({"ERROR"}, f"Annotation type {object_type} is not supported for tagging.")
return
related_objects = bpy.context.selected_objects
for related_object in related_objects:
create_annotation()
obj = bpy.context.active_object
bpy.ops.object.mode_set(mode="OBJECT")
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
obj = core.add_annotation(
tool.Ifc,
tool.Collector,
tool.Drawing,
drawing=tool.Ifc.get_entity(bpy.context.scene.camera),
object_type=object_type,
relating_type=(
tool.Ifc.get().by_id(int(props.relating_type_id)) if props.relating_type_id != "0" else None
),
enable_editing=False,
)
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
def hotkey_S_A(self):
create_annotation()
if bpy.ops.bim.add_annotation.poll():
bpy.ops.bim.add_annotation()
def hotkey_S_E(self):
if not bpy.context.active_object:
@@ -18,9 +18,11 @@
import bpy
from . import ui, prop, operator
from bpy.app.handlers import persistent
classes = (
operator.AddCurvelikeItem,
operator.AddHalfSpaceSolidItem,
operator.AddMeshlikeItem,
operator.AddRepresentation,
operator.AddSweptAreaSolidItem,
@@ -50,6 +52,8 @@ classes = (
operator.OverrideMeshSeparate,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
operator.OverrideMove,
operator.OverrideMoveMacro,
operator.OverrideOriginSet,
operator.OverrideOutlinerDelete,
operator.OverridePasteBuffer,
@@ -87,13 +91,28 @@ classes = (
addon_keymaps = []
@persistent
def block_scale(scene):
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
def register():
bpy.app.handlers.depsgraph_update_pre.append(block_scale)
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate")
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
operator.OverrideMoveMacro.define("BIM_OT_override_move")
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
@@ -116,6 +135,8 @@ def register():
"bim.object_duplicate_move_linked_aggregate_macro", "D", "PRESS", ctrl=True, shift=True
)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_move_macro", "G", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
@@ -138,6 +159,10 @@ def register():
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")
kmi = km.keymap_items.new("bim.override_escape", "ESC", "PRESS")
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
addon_keymaps.append((km, kmi))
@@ -148,6 +173,8 @@ def register():
def unregister():
bpy.app.handlers.depsgraph_update_pre.remove(block_scale)
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
@@ -78,9 +78,6 @@ class ViewportData:
modes.append(edit_mode)
elif element.is_a("IfcGridAxis"):
modes.append(edit_mode)
elif tool.Blender.Modifier.is_editing_parameters(obj):
# This should go BEFORE the modifiers
pass
elif tool.Blender.Modifier.is_roof(element):
modes.append(edit_mode)
elif tool.Blender.Modifier.is_railing(element):
@@ -36,15 +36,19 @@ class ItemDecorator:
if cls.is_installed:
cls.uninstall()
obj_is_selected = {}
obj_is_boolean = {}
objs = {}
for item_obj in context.scene.BIMGeometryProperties.item_objs:
if obj := item_obj.obj:
objs[obj.name] = cls.get_obj_data(obj)
obj_is_selected[obj.name] = obj.select_get()
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
obj_is_boolean[obj.name] = [i for i in tool.Ifc.get().get_inverse(item) if i.is_a("IfcBooleanResult")]
handler = cls()
handler.objs = objs
handler.obj_is_selected = obj_is_selected
handler.obj_is_boolean = obj_is_boolean
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@@ -54,6 +58,8 @@ class ItemDecorator:
verts = []
edges = []
tris = []
special_verts = []
special_edges = []
if len(obj.data.loop_triangles) > 0:
verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]
@@ -78,7 +84,26 @@ class ItemDecorator:
]
edges = bbox_edges
verts.extend(bbox_verts)
return {"verts": verts, "edges": edges, "tris": tris}
if "HalfSpaceSolid" in obj.name:
# Arrow shape
special_verts = [
tuple(obj.matrix_world @ Vector((0, 0, 0))),
tuple(obj.matrix_world @ Vector((0, 0, 0.5))),
tuple(obj.matrix_world @ Vector((0.05, 0, 0.45))),
tuple(obj.matrix_world @ Vector((-0.05, 0, 0.45))),
tuple(obj.matrix_world @ Vector((0, 0.05, 0.45))),
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
]
special_edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
return {
"verts": verts,
"edges": edges,
"tris": tris,
"special_verts": special_verts,
"special_edges": special_edges,
}
@classmethod
def uninstall(cls):
@@ -138,7 +163,7 @@ class ItemDecorator:
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 1.0)
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
@@ -154,6 +179,14 @@ class ItemDecorator:
continue
self.draw_batch("LINES", data["verts"], selected_elements_color, data["edges"])
self.draw_batch("TRIS", data["verts"], transparent_color(selected_elements_color), data["tris"])
else:
self.draw_batch("LINES", data["verts"], decorator_color_background, data["edges"])
self.draw_batch("LINES", data["special_verts"], selected_elements_color, data["special_edges"])
elif self.obj_is_boolean[obj_name]:
self.draw_batch("LINES", data["verts"], special_elements_color, data["edges"])
self.draw_batch("TRIS", data["verts"], transparent_color(special_elements_color), data["tris"])
self.draw_batch("LINES", data["special_verts"], special_elements_color, data["special_edges"])
else:
self.draw_batch(
"LINES", data["verts"], transparent_color(unselected_elements_color, alpha=0.2), data["edges"]
)
self.draw_batch("TRIS", data["verts"], transparent_color(special_elements_color), data["tris"])
self.draw_batch("LINES", data["special_verts"], special_elements_color, data["special_edges"])
@@ -454,7 +454,7 @@ class Helper:
x_axis = (mesh.vertices[loop[0]].co - center).normalized()
else:
x_axis = (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).normalized()
z_axis = profile_face.polygons[0].normal.normalized()
z_axis = profile_face.polygons[0].normal.normalized() * -1
y_axis = z_axis.cross(x_axis).normalized()
matrix = Matrix((x_axis, y_axis, z_axis))
matrix.normalize()
+289 -32
View File
@@ -983,7 +983,9 @@ class OverrideDuplicateMove(bpy.types.Operator):
OverrideDuplicateMove.duplicate_item(self, obj)
continue
linked_non_ifc_object = linked and not element
tracked_opening_type = tool.Model.get_tracked_opening_type(obj)
is_tracked_opening = bool(tracked_opening_type)
keep_data_linked = linked and not element and not is_tracked_opening
# Prior to duplicating, sync the object placement to make decomposition recreation more stable.
if tool.Ifc.is_moved(obj):
@@ -1000,11 +1002,17 @@ class OverrideDuplicateMove(bpy.types.Operator):
if tool.Ifc.is_edited(obj, ignore_scale=True):
tool.Ifc.edit(new_obj)
if obj.data and not linked_non_ifc_object:
if obj.data and not keep_data_linked:
# assure root.copy_class won't replace the previous mesh globally
temp_data = obj.data.copy()
new_obj.data = temp_data
# Unlink from previous boolean element
# and keep object tracked for decorations.
if is_tracked_opening:
new_obj.data.BIMMeshProperties.ifc_boolean_id = 0
tool.Root.add_tracked_opening(new_obj, tracked_opening_type)
if obj == context.active_object:
self.new_active_obj = new_obj
for collection in obj.users_collection:
@@ -1012,7 +1020,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
obj.select_set(False)
new_obj.select_set(True)
if linked_non_ifc_object:
if not element:
continue
# clear object's collection so it will be able to have it's own
@@ -1846,19 +1854,22 @@ class OverridePasteBuffer(bpy.types.Operator):
class OverrideEscape(bpy.types.Operator):
bl_idname = "bim.override_escape"
bl_label = "Override Escape"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.report({"INFO"}, "Operator executed")
return {"FINISHED"}
def modal(self, context, event):
if event.type == "ESC" and context.scene.BIMGeometryProperties.mode == "ITEM":
if context.scene.BIMGeometryProperties.mode == "ITEM":
tool.Geometry.disable_item_mode()
return {"PASS_THROUGH"}
def invoke(self, context, event):
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
elif context.scene.BIMGeometryProperties.mode == "EDIT":
bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT", should_save=False)
tool.Geometry.disable_item_mode()
elif context.scene.BIMModelProperties.openings:
bpy.ops.bim.hide_all_openings()
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
bpy.ops.bim.disable_aggregate_mode()
elif active_object := context.active_object:
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
pass
return {"FINISHED"}
class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
@@ -1878,9 +1889,25 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
selected_objs = context.selected_objects # Purposely exclude active object
if self.has_aggregates(selected_objs):
if not context.scene.BIMAggregateProperties.in_aggregate_mode:
bonsai.core.aggregate.enable_aggregate_mode(tool.Aggregate, context.active_object)
return {"FINISHED"}
if self.has_nests(selected_objs):
if not context.scene.BIMNestProperties.in_nest_mode:
bonsai.core.nest.enable_nest_mode(tool.Nest, context.active_object)
return {"FINISHED"}
if len(selected_objs) == 1 and context.active_object == selected_objs[0]:
self.handle_single_object(context, context.active_object)
elif len(selected_objs) == 0:
if context.scene.BIMAggregateProperties.in_aggregate_mode:
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
if context.scene.BIMNestProperties.in_nest_mode:
bonsai.core.nest.disable_nest_mode(tool.Nest)
return {"FINISHED"}
tool.Geometry.disable_item_mode()
elif len(selected_objs) > 1:
self.handle_multiple_selected_objects(context)
@@ -1889,6 +1916,11 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
if obj == context.scene.BIMGeometryProperties.representation_obj:
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
obj.select_set(False)
self.report(
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
)
elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element:
@@ -1908,13 +1940,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.enable_editing_boundary_geometry()
elif element.is_a("IfcGridAxis"):
self.enable_edit_mode(context)
elif tool.Blender.Modifier.is_editing_parameters(obj):
# This should go BEFORE the modifiers
self.report({"INFO"}, "Can't edit while modifier parameters are being modified")
elif tool.Blender.Modifier.is_roof(element):
bpy.ops.bim.enable_editing_roof_path()
elif tool.Blender.Modifier.is_railing(element):
bpy.ops.bim.enable_editing_railing_path()
elif tool.Blender.Modifier.try_applying_edit_mode(obj, element):
pass
else:
bpy.ops.bim.import_representation_items()
elif tool.Geometry.is_representation_item(obj):
@@ -1962,6 +1989,13 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif item.is_a("IfcAnnotationFillArea"):
tool.Model.import_annotation_fill_area(item, obj=obj)
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif tool.Geometry.is_curvelike_item(item):
tool.Model.import_curve(item, obj=obj)
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
@@ -1980,6 +2014,30 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
context.scene.BIMGeometryProperties.mode = "EDIT"
context.scene.BIMGeometryProperties.is_changing_mode = False
def has_aggregates(self, objs):
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
parts = ifcopenshell.util.element.get_parts(element)
if (aggregate or parts) and not bpy.context.scene.BIMAggregateProperties.in_aggregate_mode:
return True
else:
return False
def has_nests(self, objs):
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
nest = ifcopenshell.util.element.get_nest(element)
components = ifcopenshell.util.element.get_components(element)
if (nest or components) and not bpy.context.scene.BIMNestProperties.in_nest_mode:
return True
else:
return False
class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Switch from Edit to Item or Object mode"
@@ -2010,7 +2068,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
context.scene.BIMGeometryProperties.mode = "OBJECT"
context.scene.BIMGeometryProperties.is_changing_mode = False
if context.active_object:
if context.active_object and self.should_save:
element = tool.Ifc.get_entity(context.active_object)
if element and element.is_a("IfcRelSpaceBoundary"):
return bpy.ops.bim.edit_boundary_geometry()
@@ -2103,10 +2161,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry.import_item(obj)
elif item.is_a("IfcSweptAreaSolid"):
ProfileDecorator.uninstall()
profile = tool.Model.export_profile(obj)
if not profile:
if not (profile := tool.Model.export_profile(obj)):
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
@@ -2163,6 +2218,26 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
product=element,
representation=new_footprint,
)
elif item.is_a("IfcAnnotationFillArea"):
ProfileDecorator.uninstall()
if not (profile := tool.Model.export_annotation_fill_area(obj)):
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
ProfileDecorator.install(bpy.context)
self.enable_edit_mode(bpy.context)
return
for inverse in tool.Ifc.get().get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
obj.data.BIMMeshProperties.ifc_definition_id = profile.id()
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
elif tool.Geometry.is_curvelike_item(item):
ProfileDecorator.uninstall()
new = tool.Model.export_curves(obj)
@@ -2220,7 +2295,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
class FlipObject(bpy.types.Operator):
bl_idname = "bim.flip_object"
bl_label = "Flip Object"
bl_description = "Flip object's local axes, keep the position"
bl_description = "Flip Element about its local axes, keep the position"
bl_options = {"REGISTER", "UNDO"}
flip_local_axes: bpy.props.EnumProperty(
@@ -2639,8 +2714,19 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
data = obj.data
assert data
item_ids = data["ios_item_ids"] if "ios_item_ids" in data else data["ios_edges_item_ids"]
for item_id in set(item_ids):
queue = list(set(item_ids))
processed_ids = set()
boolean_ids = set()
while queue:
item_id = queue.pop()
if item_id in processed_ids:
continue # In theory, an item can be used multiple times (e.g. in a boolean stack).
item = tool.Ifc.get().by_id(item_id)
if item.is_a("IfcBooleanResult"):
queue.append(item.FirstOperand.id())
queue.append(item.SecondOperand.id())
boolean_ids.add(item.SecondOperand.id())
continue
item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}")
item_mesh.BIMMeshProperties.ifc_definition_id = item_id
@@ -2657,11 +2743,9 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
if not tool.Geometry.is_movable(item):
tool.Geometry.lock_object(item_obj)
if "IfcOpeningElement" in obj.name:
item_obj.display_type = "WIRE"
item_obj.select_set(True) # so you can quickly hit tab again, for edit mode
context.view_layer.objects.active = item_obj
processed_ids.add(item_id)
tool.Root.reload_item_decorator()
@@ -2719,8 +2803,6 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator):
obj.show_in_front = True
new = props.item_objs.add()
new.obj = obj
tool.Root.reload_item_decorator()
tool.Geometry.lock_object(obj)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -2744,6 +2826,7 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Root.reload_item_decorator()
class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
@@ -2795,6 +2878,7 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
tool.Root.reload_item_decorator()
class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator):
@@ -2866,3 +2950,176 @@ class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator):
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
class AddHalfSpaceSolidItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_half_space_solid_item"
bl_label = "Add Half Space Solid Item"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
if (
not tool.Blender.get_selected_objects()
or not (active_obj := tool.Blender.get_active_object())
or not tool.Geometry.is_representation_item(active_obj)
):
# In theory we can have half space solid as a top level item but let's not go there today.
self.report({"ERROR"}, "Select an item to apply the half space solid to.")
return {"CANCELLED"}
props = context.scene.BIMGeometryProperties
mesh = bpy.data.meshes.new("Tmp")
obj = bpy.data.objects.new("Tmp", mesh)
scene = bpy.context.scene
scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
new = props.item_objs.add()
new.obj = obj
matrix = props.representation_obj.matrix_world.copy()
matrix.translation = context.scene.cursor.location
local_matrix = props.representation_obj.matrix_world.inverted() @ matrix
obj.show_in_front = True
obj.matrix_world = matrix
tool.Geometry.record_object_position(obj)
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
local_z = ifc_file.createIfcDirection((0.0, 0.0, 1.0))
local_x = ifc_file.createIfcDirection((1.0, 0.0, 0.0))
point = ifc_file.createIfcCartesianPoint((np.array(local_matrix.translation) / unit_scale).tolist())
placement = ifc_file.createIfcAxis2Placement3D(point, local_z, local_x)
plane = ifc_file.createIfcPlane(placement)
item = ifc_file.createIfcHalfSpaceSolid(plane, AgreementFlag=False)
representation = tool.Geometry.get_active_representation(props.representation_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
# TODO refactor to core and not rely on selection
tool.Blender.select_and_activate_single_object(context, active_obj)
tool.Blender.select_object(obj)
bpy.ops.bim.add_boolean()
class OverrideMoveMacro(bpy.types.Macro):
bl_idname = "bim.override_move_macro"
bl_label = "IFC Move Aggregate"
bl_options = {"REGISTER", "UNDO"}
class OverrideMove(bpy.types.Operator):
bl_idname = "bim.override_move"
bl_label = "IFC Move"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
# Deep magick from the dawn of time
if IfcStore.get_file():
IfcStore.execute_ifc_operator(self, context)
if self.new_active_obj:
context.view_layer.objects.active = self.new_active_obj
return {"FINISHED"}
return {"FINISHED"}
def _execute(self, context):
# Get filling objects
selection = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
selection.append(obj)
for rel in getattr(element, "HasOpenings", []) or []:
opening = rel.RelatedOpeningElement
for rel2 in getattr(opening, "HasFillings", []) or []:
filling = rel2.RelatedBuildingElement
selection.append(tool.Ifc.get_object(filling))
for obj in selection:
obj.select_set(True)
self.new_active_obj = obj
# Get aggregates
props = context.scene.BIMAggregateProperties
not_editing_objs = [o.obj for o in props.not_editing_objects]
aggregates_to_move = []
for obj in context.selected_objects:
self.new_active_obj = None
if obj in not_editing_objs:
obj.select_set(False)
continue
if obj == props.editing_aggregate:
continue
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
continue
parts = ifcopenshell.util.element.get_parts(element)
if parts:
aggregates_to_move.append(tool.Ifc.get_object(element))
continue
if not parts and props.in_aggregate_mode:
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregates_to_move.append(tool.Ifc.get_object(aggregate))
obj.select_set(False)
aggregates_to_move = set(aggregates_to_move)
if aggregates_to_move:
for obj in aggregates_to_move:
obj.select_set(True)
for part in tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(obj)):
part_obj = tool.Ifc.get_object(part)
part_obj.select_set(True)
self.new_active_obj = obj
return {"FINISHED"}
# Get nests
props = context.scene.BIMNestProperties
not_editing_objs = [o.obj for o in props.not_editing_objects]
nests_to_move = []
for obj in context.selected_objects:
self.new_active_obj = None
if obj in not_editing_objs:
obj.select_set(False)
continue
if obj == props.editing_nest:
continue
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
continue
components = ifcopenshell.util.element.get_components(element)
if components:
nests_to_move.append(tool.Ifc.get_object(element))
continue
if not components and props.in_nest_mode:
continue
nest = ifcopenshell.util.element.get_nest(element)
if nest:
nests_to_move.append(tool.Ifc.get_object(nest))
obj.select_set(False)
nests_to_move = set(nests_to_move)
if nests_to_move:
for obj in nests_to_move:
obj.select_set(True)
for component in tool.Nest.get_components_recursively(tool.Ifc.get_entity(obj)):
component_obj = tool.Ifc.get_object(component)
component_obj.select_set(True)
self.new_active_obj = obj
return {"FINISHED"}
return {"FINISHED"}
@@ -129,12 +129,7 @@ class RepresentationItem(PropertyGroup):
class RepresentationItemObject(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
obj: PointerProperty(type=bpy.types.Object)
verts: StringProperty(name="Verts")
edges: StringProperty(name="Edges")
special_verts: StringProperty(name="Special Verts")
special_edges: StringProperty(name="Special Edges")
class ShapeAspect(PropertyGroup):
@@ -64,8 +64,11 @@ class GeoreferenceDecorator:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
self.calculate_angles(context)
props = context.scene.BIMGeoreferenceProperties
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
return
self.calculate_angles(context)
e, n, h = [round(float(o), 7) for o in props.model_origin.split(",")]
if (operation := GeoreferenceData.data["coordinate_operation"]) and (scale := operation.get("Scale", None)):
e *= scale
@@ -75,8 +78,7 @@ class GeoreferenceDecorator:
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
font_size = tool.Blender.scale_font_size(12 + (self.scale / 20))
blf.size(self.font_id, font_size)
blf.size(self.font_id, 12)
color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color)
blf.enable(self.font_id, blf.SHADOW)
@@ -175,6 +177,10 @@ class GeoreferenceDecorator:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
return
self.calculate_angles(context)
self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -308,7 +314,6 @@ class GeoreferenceDecorator:
self.draw_batch("LINES", verts, decorator_color_special, edges)
self.draw_dashed_line(location * 3, location * 6, decorator_color_error)
props = context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
location = GeoreferenceData.data["local_origin"]["location"]
if location.length < 1000:
@@ -206,7 +206,11 @@ class BIMGeoreferenceProperties(PropertyGroup):
update=update_should_visualise,
)
visualization_scale: FloatProperty(
name="Visualization Scale", description="Affects the georeference decorator size", default=1, min=0.1, max=50
name="Visualization Scale",
description="Affects the georeference decorator size",
default=1,
soft_min=0.1,
soft_max=50,
)
grid_north_angle: StringProperty(name="Grid North Angle", update=update_grid_north_angle)
x_axis_abscissa: StringProperty(name="X Axis Abscissa", update=update_grid_north_vector)
@@ -25,12 +25,21 @@ from bonsai.bim.module.georeference.data import GeoreferenceData
class BIM_PT_gis(Panel):
bl_label = "Georeferencing"
bl_idname = "BIM_PT_gis"
bl_options = {"DEFAULT_CLOSED"}
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_geometry"
def draw_header(self, context):
props = context.scene.BIMGeoreferenceProperties
row = self.layout.row(align=True)
icon = "HIDE_OFF" if props.should_visualise else "HIDE_ON"
row.label(text="") # empty text occupies the left of the row
if props.should_visualise:
row.prop(props, "visualization_scale", text="Size", slider=True)
row.prop(props, "should_visualise", icon=icon, text="")
def draw(self, context):
self.layout.use_property_split = True
self.layout.use_property_decorate = False
@@ -87,19 +96,14 @@ class BIM_PT_gis(Panel):
if not GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Not Georeferenced", icon="ERROR")
row = self.layout.row(align=True)
row.prop(props, "should_visualise", icon="HIDE_OFF")
row.prop(props, "visualization_scale", text="Size", slider=True)
if tool.Ifc.get_schema() != "IFC2X3":
row = self.layout.row(align=True)
row.prop(props, "coordinate_operation_class", text="")
row.operator("bim.add_georeferencing", icon="ADD", text="")
if GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
row = self.layout.row(align=True)
row.prop(props, "should_visualise", icon="HIDE_OFF")
row.prop(props, "visualization_scale", text="Size", slider=True)
if tool.Ifc.get_schema() != "IFC2X3":
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
row.operator("bim.remove_georeferencing", icon="X", text="")
@@ -37,6 +37,7 @@ classes = (
operator.Push,
operator.RefreshGit,
operator.SwitchRevision,
operator.InstallGit,
prop.IfcGitTag,
prop.IfcGitListItem,
prop.IfcGitProperties,
@@ -386,3 +386,25 @@ class ObjectLog(bpy.types.Operator):
step_id = context.active_object.BIMObjectProperties.ifc_definition_id
core.entity_log(tool.IfcGit, tool.Ifc, step_id, self)
return {"FINISHED"}
class InstallGit(bpy.types.Operator):
"""Install Git Version Control System from the
Windows Package Manager Community Repository,
requires restarting Blender after installation"""
bl_label = "Install Git"
bl_idname = "ifcgit.install_git"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
if IfcGitData.data["git_exe"]:
return False
return True
def execute(self, context):
core.install_git(tool.IfcGit, self)
refresh()
return {"FINISHED"}
@@ -1,6 +1,7 @@
import bpy
import time
import os
import platform
from bonsai.bim.module.ifcgit.data import IfcGitData
@@ -26,6 +27,12 @@ class IFCGIT_PT_panel(bpy.types.Panel):
if not IfcGitData.data["git_exe"]:
row = layout.row()
row.label(text="Git is not installed", icon="ERROR")
if platform.system() == "Windows":
row.operator(
"ifcgit.install_git",
text="Install Git",
icon="PACKAGE",
)
return
props = context.scene.IfcGitProperties
+3 -1
View File
@@ -49,6 +49,8 @@ class LayersData:
return []
results = dict()
shape = tool.Ifc.get().by_id(data.BIMMeshProperties.ifc_definition_id)
for inverse in shape.LayerAssignments:
for inverse in getattr(shape, "LayerAssignments", []):
results[inverse.id()] = inverse.Name or "Unnamed"
for inverse in getattr(shape, "LayerAssignment", []):
results[inverse.id()] = inverse.Name or "Unnamed"
return results
@@ -29,7 +29,7 @@ import bonsai.core.style
import bonsai.core.material as core
import bonsai.bim.module.model.profile as model_profile
from bonsai.bim.ifc import IfcStore
from typing import Any, Union
from typing import Any, Union, TYPE_CHECKING
class LoadMaterials(bpy.types.Operator):
@@ -155,7 +155,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_material"
bl_label = "Diplicate Material"
bl_label = "Duplicate Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material ID")
@@ -202,9 +202,17 @@ class RemoveMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_material_to_selected"
bl_label = "Assign Material To Selected"
bl_description = "Assign currently selected material in Materials UI to the selected objects"
bl_description = (
"Assign currently selected material in Materials UI to the selected objects.\n\n"
"ALT+CLICK to assign material as a usage."
)
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material IFC ID")
assign_as_usage: bpy.props.BoolProperty(
name="Assign Material As A Usage",
default=False,
options={"SKIP_SAVE"},
)
@classmethod
def poll(cls, context):
@@ -213,13 +221,25 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
return False
return True
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
material_class = tool.Ifc.get().by_id(self.material).is_a()
if material_class not in ("IfcMaterialProfileSet", "IfcMaterialLayerSet"):
self.report({"ERROR"}, f"{material_class} cannot be assigned as a usage.")
return {"CANCELLED"}
self.assign_as_usage = True
return self.execute(context)
def _execute(self, context):
material = tool.Ifc.get().by_id(self.material)
objects = tool.Blender.get_selected_objects()
material_type = material.is_a()
if self.assign_as_usage:
material_type += "Usage"
core.assign_material(
tool.Ifc,
tool.Material,
material_type=tool.Material.get_active_material_type(),
material_type=material_type,
objects=objects,
material=material,
)
@@ -866,16 +886,30 @@ class SelectMaterialInMaterialsUI(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
material_id: bpy.props.IntProperty()
if TYPE_CHECKING:
material_id: int
def execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
ifc_file = tool.Ifc.get()
material = ifc_file.by_id(self.material_id)
core.load_materials(tool.Material, material.is_a())
material_id = self.material_id
material = ifc_file.by_id(material_id)
material_class = material.is_a()
# Current we do not support editing usaged not on instance,
# so fallback to the material set it's referring to.
if material_class.endswith("Usage"):
material_class = material_class.removesuffix("Usage")
if material_class == "IfcMaterialProfileSet":
material = material.ForProfileSet
else: # IfcMaterialLayerSet
material = material.ForLayerSet
material_id = material.id()
core.load_materials(tool.Material, material_class)
def get_material_item() -> Union[tuple[int, bpy.types.PropertyGroup], None]:
return next(
((i, m) for i, m in enumerate(props.materials) if m.ifc_definition_id == self.material_id), None
)
return next(((i, m) for i, m in enumerate(props.materials) if m.ifc_definition_id == material_id), None)
material_item = get_material_item()
@@ -124,6 +124,11 @@ class BIMMaterialProperties(PropertyGroup):
styles: EnumProperty(items=get_styles, name="Styles")
contexts: EnumProperty(items=get_contexts, name="Contexts")
@property
def active_material(self):
if self.active_material_index < len(self.materials):
return self.materials[self.active_material_index]
class BIMObjectMaterialProperties(PropertyGroup):
material_type: EnumProperty(items=get_object_material_type, name="Material Type")
+13 -5
View File
@@ -77,21 +77,23 @@ classes = (
wall.SplitWall,
wall.UnjoinWalls,
opening.AddBoolean,
opening.AddFilledOpening,
opening.AddPotentialHalfSpaceSolid,
opening.AddPotentialOpening,
opening.EditOpenings,
opening.CloneOpening,
opening.EditOpenings,
opening.FlipFill,
opening.HideBooleans,
opening.HideAllOpenings,
opening.HideOpenings,
opening.PurgeUnusedOpenings,
opening.RecalculateFill,
opening.RemoveBooleans,
opening.ShowBooleans,
opening.RemoveBoolean,
opening.SelectBoolean,
opening.ShowOpenings,
opening.UpdateOpeningsFocus,
profile.ChangeCardinalPoint,
profile.ChangeProfileDepth,
profile.DisableEditingExtrusionAxis,
profile.DrawPolylineProfile,
profile.EditExtrusionAxis,
profile.EnableEditingExtrusionAxis,
profile.ExtendProfile,
@@ -124,6 +126,7 @@ classes = (
prop.SnapMousePoint,
prop.PolylinePoint,
prop.Polyline,
prop.ProductPreviewItem,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -133,6 +136,7 @@ classes = (
prop.BIMRailingProperties,
prop.BIMRoofProperties,
prop.BIMPolylineProperties,
prop.BIMProductPreviewProperties,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
@@ -170,6 +174,7 @@ classes = (
door.EnableEditingDoor,
door.RemoveDoor,
railing.BIM_OT_add_railing,
railing.CopyRailingParameters,
railing.AddRailing,
railing.CancelEditingRailing,
railing.FinishEditingRailing,
@@ -182,6 +187,7 @@ classes = (
roof.BIM_OT_add_roof,
roof.AddRoof,
roof.CancelEditingRoof,
roof.CopyRoofParameters,
roof.FinishEditingRoof,
roof.EnableEditingRoof,
roof.CancelEditingRoofPath,
@@ -213,6 +219,7 @@ def register():
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
bpy.types.Scene.BIMProductPreviewProperties = bpy.props.PointerProperty(type=prop.BIMProductPreviewProperties)
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
@@ -243,6 +250,7 @@ def unregister():
del bpy.types.Scene.BIMModelProperties
del bpy.types.Scene.BIMPolylineProperties
del bpy.types.Scene.BIMProductPreviewProperties
del bpy.types.Object.BIMArrayProperties
del bpy.types.Object.BIMStairProperties
del bpy.types.Object.BIMSverchokProperties
+75 -32
View File
@@ -57,20 +57,23 @@ class AuthoringData:
cls.data["ifc_element_type"] = cls.ifc_element_type
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["ifc_class_current"] = cls.ifc_class_current()
cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes()
cls.data["relating_type_id_current"] = cls.relating_type_id_current() # only after .ifc_classes()
cls.data["relating_type_name"] = cls.relating_type_name() # only after .relating_type_id()
cls.data["relating_type_description"] = cls.relating_type_description() # only after .relating_type_id()
cls.data["predefined_type"] = cls.predefined_type() # only after .relating_type_id()
# only after .ifc_classes()
# Make sure .ifc_classes() was run before next lines
cls.data["type_elements"] = cls.type_elements()
cls.data["type_elements_filtered"] = cls.type_elements_filtered()
cls.data["relating_type_id"] = cls.relating_type_id()
# Make sure .relating_type_id() was run before next lines
cls.data["relating_type_id_current"] = cls.relating_type_id_current()
cls.data["relating_type_name"] = cls.relating_type_name()
cls.data["relating_type_description"] = cls.relating_type_description()
cls.data["predefined_type"] = cls.predefined_type()
# Make sure .type_elements_filtered() was run before next lines
cls.data["total_types"] = cls.total_types()
cls.data["total_pages"] = cls.total_pages()
cls.data["total_pages"] = cls.total_pages() # Only after .total_types()
cls.data["next_page"] = cls.next_page()
cls.data["prev_page"] = cls.prev_page()
cls.data["paginated_relating_types"] = cls.paginated_relating_types()
cls.data["type_thumbnail"] = cls.type_thumbnail() # only after .relating_type_id()
cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id_current()
cls.data["is_voidable_element"] = cls.is_voidable_element()
cls.data["has_visible_openings"] = cls.has_visible_openings()
cls.data["has_visible_boundaries"] = cls.has_visible_boundaries()
@@ -81,6 +84,10 @@ class AuthoringData:
cls.data["boundary_class"] = cls.boundary_class()
cls.data["selected_material_usages"] = cls.selected_material_usages()
# Only after .active_material_usage() and .active_class()
cls.data["is_flippable_element"] = cls.is_flippable_element()
cls.data["is_regenable_element"] = cls.is_regenable_element()
@classmethod
def default_container(cls) -> str | None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
@@ -100,23 +107,15 @@ class AuthoringData:
@classmethod
def type_thumbnail(cls):
if not cls.data["relating_type_id"]:
return 0
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
if relating_type_id is None:
return 0
element = tool.Ifc.get().by_id(int(relating_type_id))
return cls.type_thumbnails.get(element.id(), None) or 0
return cls.type_thumbnails.get(int(cls.data["relating_type_id_current"] or 0), 0)
@classmethod
def total_types(cls):
ifc_class = cls.data["ifc_class_current"]
return len(tool.Ifc.get().by_type(ifc_class)) if ifc_class else 0
return len(cls.data["type_elements_filtered"])
@classmethod
def total_pages(cls):
ifc_class = cls.data["ifc_class_current"]
total_types = len(tool.Ifc.get().by_type(ifc_class)) if ifc_class else 0
total_types = cls.data["total_types"]
return math.ceil(total_types / cls.types_per_page)
@classmethod
@@ -130,12 +129,35 @@ class AuthoringData:
return cls.props.type_page - 1
@classmethod
def paginated_relating_types(cls):
def type_elements(cls):
ifc_class = cls.data["ifc_class_current"]
if not ifc_class:
return []
elements = list(tool.Ifc.get().by_type(ifc_class))
return natsorted(elements, key=lambda s: (s.Name or "Unnamed").lower())
@classmethod
def type_elements_filtered(cls):
search_query = cls.props.search_name.lower()
def filter_element(element):
if search_query in (element.Name or "Unnamed").lower():
return True
if search_query in (element.Description or "").lower():
return True
if search_query in (ifcopenshell.util.element.get_predefined_type(element) or "").lower():
return True
return False
elements = cls.data["type_elements"]
if cls.props.search_name:
return [e for e in elements if filter_element(e)]
return elements
@classmethod
def paginated_relating_types(cls):
results = []
elements = natsorted(tool.Ifc.get().by_type(ifc_class), key=lambda e: e.Name or "Unnamed")
elements = cls.data["type_elements_filtered"]
elements = elements[(cls.props.type_page - 1) * cls.types_per_page : cls.props.type_page * cls.types_per_page]
for element in elements:
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
@@ -159,6 +181,28 @@ class AuthoringData:
element = tool.Ifc.get_entity(active_object)
return element and element.is_a("IfcElement") and not element.is_a("IfcOpeningElement")
@classmethod
def is_flippable_element(cls):
return cls.data["active_material_usage"] in ("LAYER2", "PROFILE") or cls.data["active_class"] in (
"IfcWindow",
"IfcWindowStandardCase",
"IfcDoor",
"IfcDoorStandardCase",
)
@classmethod
def is_regenable_element(cls):
if cls.data["active_material_usage"] in ("LAYER2", "PROFILE") and cls.data["active_class"] not in (
"IfcCableCarrierSegment",
"IfcCableSegment",
"IfcDuctSegment",
"IfcPipeSegment",
):
return True
if cls.data["active_class"] in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
return True
return False
@classmethod
def has_visible_openings(cls):
if active_object := tool.Blender.get_active_object():
@@ -197,10 +241,9 @@ class AuthoringData:
@classmethod
def is_representation_item_active(cls) -> bool:
object = bpy.context.active_object
if not object:
if not (obj := tool.Blender.get_active_object()):
return False
return tool.Geometry.is_representation_item(object)
return tool.Geometry.is_representation_item(obj)
@classmethod
def active_representation_type(cls):
@@ -240,13 +283,8 @@ class AuthoringData:
@classmethod
def relating_type_id(cls):
results = []
ifc_class = cls.data["ifc_class_current"]
if ifc_class:
elements = natsorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: (s.Name or "Unnamed").lower())
results.extend(elements)
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in results]
return []
elements = cls.data["type_elements"]
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in elements]
@classmethod
def relating_type_id_current(cls):
@@ -543,6 +581,7 @@ class RoofData:
if not cls.data["pset_data"]:
return
cls.data["general_params"] = cls.general_params()
cls.data["path_data"] = cls.path_data()
@classmethod
def pset_data(cls):
@@ -563,6 +602,10 @@ class RoofData:
general_params[prop_readable_name] = prop_value
return general_params
@classmethod
def path_data(cls):
return cls.data["pset_data"]["data_dict"]["path_data"]
class ItemData:
data = {}
+16 -334
View File
@@ -25,15 +25,14 @@ import bmesh
import ifcopenshell
import bonsai.tool as tool
import math
from math import sin, cos, tan, radians
from math import sin, cos, radians
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from mathutils import Vector, Matrix, Quaternion
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_circle_2d
from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
from bonsai.bim.module.geometry.decorator import ItemDecorator
def transparent_color(color, alpha=0.1):
@@ -209,11 +208,11 @@ class ProfileDecorator:
if draw_faces:
self.draw_faces(bm, all_vertices)
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
self.draw_batch("LINES", all_vertices, background_elements_color, arc_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
self.draw_batch("LINES", all_vertices, unselected_elements_color, unselected_edges)
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
self.draw_batch("POINTS", error_vertices, error_elements_color)
@@ -580,7 +579,7 @@ class PolylineDecorator:
padding = 8
verts = []
edges = []
if snap_prop.snap_type in ["Edge", "Edge-Intersection", "Vertex"]:
if snap_prop.snap_type in ["Edge", "Edge Intersection", "Vertex"]:
p1 = (coords[0] - padding, coords[1] + padding)
p2 = (coords[0] + padding, coords[1] + padding)
p3 = (coords[0] + padding, coords[1] - padding)
@@ -588,7 +587,7 @@ class PolylineDecorator:
verts = [p1, p2, p3, p4]
if snap_prop.snap_type == "Edge":
edges = [[0, 1], [1, 3], [3, 2], [2, 0]]
elif snap_prop.snap_type == "Edge-Intersection":
elif snap_prop.snap_type == "Edge Intersection":
edges = [[0, 2], [1, 3]]
else:
edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
@@ -786,6 +785,10 @@ class ProductDecorator:
@classmethod
def uninstall(cls):
props = bpy.context.scene.BIMProductPreviewProperties # updated by model/polyline.py
props.verts.clear()
props.edges.clear()
props.tris.clear()
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
@@ -799,315 +802,12 @@ class ProductDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def get_wall_preview_data(cls, context, relating_type):
def create_bmesh_from_vertices(vertices):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in polyline_vertices]
if is_closed:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
new_edges.append(
bm.edges.new((new_verts[-1], new_verts[0]))
) # Add an edge between the last an first point to make it closed.
else:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
return bm
# Get properties from object type
layers = tool.Model.get_material_layer_parameters(relating_type)
if not layers["thickness"]:
return
thickness = layers["thickness"]
model_props = context.scene.BIMModelProperties
direction_sense = model_props.direction_sense
direction = 1
if direction_sense == "NEGATIVE":
direction = -1
offset_type = model_props.offset_type
offset = 0
if offset_type == "CENTER":
offset = -thickness / 2
elif offset_type == "INTERIOR":
offset = -thickness
unit_system = tool.Drawing.get_unit_system()
factor = 1
if unit_system == "IMPERIAL":
factor = 3.048
if unit_system == "METRIC":
unit_length = context.scene.unit_settings.length_unit
if unit_length == "MILLIMETERS":
factor = 1000
# For the model properties, the offset value should just be converted
# However, for the wall preview logic that follows, offset and thickness must change direction
model_props.offset = offset * factor
thickness *= direction
offset *= direction
height = float(model_props.extrusion_depth)
rl = float(model_props.rl1)
x_angle = float(model_props.x_angle)
angle_distortion = height * tan(x_angle)
wall_preview_data = {}
wall_preview_data["verts"] = []
# Verts
polyline_vertices = []
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
wall_preview_data = []
return
for point in polyline_points:
polyline_vertices.append(Vector((point.x, point.y, point.z)))
is_closed = False
if (
polyline_vertices[0].x == polyline_vertices[-1].x
and polyline_vertices[0].y == polyline_vertices[-1].y
and polyline_vertices[0].z == polyline_vertices[-1].z
):
is_closed = True
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
bm_base = create_bmesh_from_vertices(polyline_vertices)
base_vertices = tool.Cad.offset_edges(bm_base, offset)
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
top_vertices = tool.Cad.offset_edges(bm_base, angle_distortion + offset)
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distortion + thickness + offset)
if is_closed:
base_vertices.append(base_vertices[0])
offset_base_verts.append(offset_base_verts[0])
top_vertices.append(top_vertices[0])
offset_top_verts.append(offset_top_verts[0])
if offset_base_verts is not None:
for v in base_vertices:
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in offset_base_verts[::-1]:
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in top_vertices:
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
for v in offset_top_verts[::-1]:
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
bm_base.free()
# Edges and Tris
points = []
side_edges_1 = []
side_edges_2 = []
base_edges = []
for i in range(len(wall_preview_data["verts"])):
points.append(Vector(wall_preview_data["verts"][i]))
n = len(points) // 2
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
side_edges_1.extend(bottom_side_1)
side_edges_2.extend(bottom_side_2)
base_edges.extend(bottom_loop)
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
side_edges_1.extend(upper_side_1)
side_edges_2.extend(upper_side_2)
base_edges.extend(upper_loop)
loops = [side_edges_1, side_edges_2, base_edges]
wall_preview_data["edges"] = []
wall_preview_data["tris"] = []
for i, group in enumerate(loops):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
bm.verts.index_update()
bm.edges.index_update()
if i == 2:
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
bm.verts.index_update()
bm.edges.index_update()
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
wall_preview_data["edges"].extend(edges)
wall_preview_data["tris"].extend(tris)
wall_preview_data["edges"] = list(set(tuple(e) for e in wall_preview_data["edges"]))
wall_preview_data["tris"] = list(set(tuple(t) for t in wall_preview_data["tris"]))
return wall_preview_data
def get_product_preview_data(cls, context, relating_type):
model_props = context.scene.BIMModelProperties
if relating_type.is_a("IfcDoorType"):
rl = float(model_props.rl1)
elif relating_type.is_a("IfcWindowType"):
rl = float(model_props.rl2)
else:
rl = 0
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
snap_element = tool.Ifc.get_entity(snap_obj)
rot_mat = Quaternion()
if snap_element and snap_element.is_a("IfcWall"):
rot_mat = snap_obj.matrix_world.to_quaternion()
obj_type = tool.Ifc.get_object(relating_type)
if obj_type.data:
data = ItemDecorator.get_obj_data(obj_type)
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
return data
def get_profile_preview_data(self, context, relating_type):
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile = material.MaterialProfiles[0].Profile
except:
return {}
model_props = context.scene.BIMModelProperties
extrusion_depth = model_props.extrusion_depth
cardinal_point = model_props.cardinal_point
rot_mat = Quaternion()
if relating_type.is_a("IfcBeamType"):
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
rot_mat = y_rot @ z_rot
# Get profile data
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts
if not verts:
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create extrusion bmesh
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in grouped_verts]
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
# Create bounding box
verts = bm.verts
i = len(verts)
min_x = min(v.co.x for v in verts)
max_x = max(v.co.x for v in verts)
min_y = min(v.co.y for v in verts)
max_y = max(v.co.y for v in verts)
min_z = min(v.co.z for v in verts)
max_z = max(v.co.z for v in verts)
bbox_verts = [
(min_x, min_y, min_z),
(max_x, min_y, min_z),
(max_x, max_y, min_z),
(min_x, max_y, min_z),
(min_x, min_y, max_z),
(max_x, min_y, max_z),
(max_x, max_y, max_z),
(min_x, max_y, max_z),
]
bbox_edges = [
(0 + i, 3 + i),
(3 + i, 7 + i),
(7 + i, 4 + i),
(4 + i, 0 + i),
(0 + i, 1 + i),
(3 + i, 2 + i),
(7 + i, 6 + i),
(4 + i, 5 + i),
(1 + i, 2 + i),
(2 + i, 6 + i),
(6 + i, 5 + i),
(5 + i, 1 + i),
]
# Calculate rotation, mouse position, angle and cardinal point
# TODO Angle
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
def get_product_preview_data(self, context):
props = context.scene.BIMProductPreviewProperties
data = {}
verts = [tuple(v.co) for v in verts]
verts.extend(bbox_verts)
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
verts = [tuple(Vector(v) + mouse_point) for v in verts]
data["verts"] = verts
data["edges"] = bbox_edges
# data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
data["tris"] = tris
bm.free()
data["verts"] = [(*v.value_3d,) for v in props.verts]
data["edges"] = [(int(e.value_2d[0]), int(e.value_2d[1])) for e in props.edges]
data["tris"] = [(int(t.value_3d[0]), int(t.value_3d[1]), int(t.value_3d[2])) for t in props.tris]
return data
def draw_product_preview(self, context):
@@ -1134,25 +834,7 @@ class ProductDecorator:
else:
return
# Wall
if self.relating_type.is_a("IfcWallType"):
wall_preview_data = self.get_wall_preview_data(context, self.relating_type)
if wall_preview_data:
self.draw_batch("LINES", wall_preview_data["verts"], decorator_color, wall_preview_data["edges"])
self.draw_batch(
"TRIS", wall_preview_data["verts"], transparent_color(decorator_color), wall_preview_data["tris"]
)
# Mesh type products
product_preview_data = self.get_product_preview_data(context, self.relating_type)
if product_preview_data:
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
self.draw_batch(
"TRIS", product_preview_data["verts"], transparent_color(decorator_color), product_preview_data["tris"]
)
# Profile type products
product_preview_data = self.get_profile_preview_data(context, self.relating_type)
product_preview_data = self.get_product_preview_data(context)
if product_preview_data:
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
self.draw_batch(
+50 -46
View File
@@ -19,8 +19,6 @@
import bpy
import bmesh
from bmesh.types import BMVert, BMFace
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
@@ -42,8 +40,7 @@ import collections
V_ = tool.Blender.V_
def update_door_modifier_representation(context: bpy.types.Context) -> None:
obj = context.active_object
def update_door_modifier_representation(obj: bpy.types.Object) -> None:
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
@@ -170,22 +167,6 @@ def update_door_modifier_representation(context: bpy.types.Context) -> None:
tool.Model.update_simple_openings(element)
# TODO: move it out to tools
def bm_sort_out_geom(
geom_data: list[Union[bmesh.types.BMVert, bmesh.types.BMEdge, bmesh.types.BMFace]]
) -> dict[str, Any]:
geom_dict = {"verts": [], "edges": [], "faces": []}
for el in geom_data:
if isinstance(el, BMVert):
geom_dict["verts"].append(el)
elif isinstance(el, BMFace):
geom_dict["faces"].append(el)
else:
geom_dict["edges"].append(el)
return geom_dict
def bm_mirror(
bm: bmesh.types.BMesh,
verts: list[bmesh.types.BMVert],
@@ -209,7 +190,7 @@ def bm_mirror(
for v in verts:
faces.update(v.link_faces)
duplicated = bmesh.ops.duplicate(bm, geom=list(faces))
verts = bm_sort_out_geom(duplicated["geom"])["verts"]
verts = tool.Model.bm_sort_out_geom(duplicated["geom"])["verts"]
bmesh.ops.transform(bm, verts=verts, matrix=matrix, space=Matrix.Identity(4))
return verts
@@ -243,7 +224,7 @@ def create_bm_extruded_profile(
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
extrusion_vector = extrusion_vector * magnitude
extruded_verts = bm_sort_out_geom(extruded["geom"])["verts"]
extruded_verts = tool.Model.bm_sort_out_geom(extruded["geom"])["verts"]
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
bmesh.ops.translate(bm, vec=position, verts=new_verts + extruded_verts)
@@ -305,7 +286,7 @@ def create_bm_door_lining(
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
extrusion_vector = Vector((0, 1, 0)) * depth
translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)]
translate_verts = [v for v in extruded["geom"] if isinstance(v, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
bmesh.ops.translate(bm, vec=position, verts=new_verts + translate_verts)
@@ -546,11 +527,10 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_door"
bl_label = "Add Door"
bl_description = "Add Bonsai parametric door to the active IFC element"
bl_description = "Add a Parametric Door to the Selected IFC Door Elements"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
def add_door_on_object(self, obj):
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
@@ -571,18 +551,25 @@ 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)
update_door_modifier_representation(obj)
def _execute(self, context):
for obj in tool.Blender.get_selected_objects():
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
continue
self.add_door_on_object(obj)
return {"FINISHED"}
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_door"
bl_label = "Cancel Editing Door"
bl_options = {"REGISTER"}
bl_label = "Cancel Editing Door on Selected Objects"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
def cancel_editing_door_on_object(self, obj):
element = tool.Ifc.get_entity(obj)
if not tool.Blender.Modifier.is_door(element):
return
props = obj.BIMDoorProperties
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
data.update(data.pop("lining_properties"))
@@ -603,17 +590,22 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
)
props.is_editing = False
def _execute(self, context):
for obj in tool.Blender.get_selected_objects():
self.cancel_editing_door_on_object(obj)
return {"FINISHED"}
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_door"
bl_label = "Finish Editing Door"
bl_options = {"REGISTER"}
bl_label = "Finish Editing Door on Selected Objects"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
def finish_editing_door_on_object(self, obj):
element = tool.Ifc.get_entity(obj)
if not tool.Blender.Modifier.is_door(element):
return
props = obj.BIMDoorProperties
door_data = props.get_general_kwargs(convert_to_project_units=True)
@@ -625,23 +617,28 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_door_modifier_representation(context)
update_door_modifier_representation(obj)
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
def _execute(self, context):
for obj in tool.Blender.get_selected_objects():
self.finish_editing_door_on_object(obj)
return {"FINISHED"}
class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_door"
bl_label = "Enable Editing Door"
bl_options = {"REGISTER"}
bl_label = "Enable Editing Door on Selected Objects"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMDoorProperties
def edit_door_on_obj(self, obj):
element = tool.Ifc.get_entity(obj)
if not tool.Blender.Modifier.is_door(element):
return
props = obj.BIMDoorProperties
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
data.update(data.pop("lining_properties"))
data.update(data.pop("panel_properties"))
@@ -649,21 +646,28 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
# required since we could load pset from .ifc and BIMDoorProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
props.is_editing = True
def _execute(self, context):
for obj in tool.Blender.get_selected_objects():
self.edit_door_on_obj(obj)
return {"FINISHED"}
class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_door"
bl_label = "Remove Door"
bl_options = {"REGISTER"}
bl_label = "Remove Door on Selected Objects"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMDoorProperties
def remove_door_on_object(self, obj):
element = tool.Ifc.get_entity(obj)
if not tool.Blender.Modifier.is_door(element):
return
obj.BIMDoorProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
def _execute(self, context):
for obj in tool.Blender.get_selected_objects():
self.remove_door_on_object(obj)
return {"FINISHED"}
+392 -407
View File
@@ -25,6 +25,8 @@ import logging
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.geometry
import ifcopenshell.api.void
import ifcopenshell.geom
import ifcopenshell.util.shape
import ifcopenshell.util.element
@@ -35,6 +37,7 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.core.geometry
import bonsai.bim.import_ifc as import_ifc
from collections import defaultdict
from bonsai.bim.ifc import IfcStore
from math import pi, radians
from mathutils import Vector, Matrix
@@ -43,37 +46,27 @@ from bpy.types import SpaceView3D
from bpy.props import FloatProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from gpu_extras.batch import batch_for_shader
from typing import Union, Optional, Any, cast
class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_filled_opening"
bl_label = "Add Filled Opening"
bl_options = {"REGISTER", "UNDO"}
voided_obj: bpy.props.StringProperty()
filling_obj: bpy.props.StringProperty()
def _execute(self, context):
FilledOpeningGenerator().generate(bpy.data.objects.get(self.filling_obj), bpy.data.objects.get(self.voided_obj))
return {"FINISHED"}
from typing import Union, Optional, Any, cast, Sequence
class FilledOpeningGenerator:
def generate(
self,
filling_obj: Union[bpy.types.Object, None],
voided_obj: Union[bpy.types.Object, None],
filling_obj: bpy.types.Object,
voided_obj: bpy.types.Object,
target: Optional[Vector] = None,
) -> None:
) -> Union[None, str]:
"""
:param target: Target opening position. If ommited, cursor position is used.
:return: None if there was no errors, otherwise returns a string with error message.
"""
props = bpy.context.scene.BIMModelProperties
opening_thickness_si = 0.0
filling = tool.Ifc.get_entity(filling_obj)
element = tool.Ifc.get_entity(voided_obj)
if not voided_obj or not filling_obj:
return
assert filling and element
if filling.FillsVoids:
ifcopenshell.api.run(
"void.remove_opening", tool.Ifc.get(), opening=filling.FillsVoids[0].RelatingOpeningElement
@@ -92,7 +85,7 @@ class FilledOpeningGenerator:
target = filling_obj.matrix_world.translation.copy()
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.5)
if not raycast[0]:
return
return "TARGET is too far away from the voided object's mesh."
# In this prototype, we assume openings are only added to axis-based elements
layers = tool.Model.get_material_layer_parameters(element)
@@ -119,6 +112,8 @@ class FilledOpeningGenerator:
new_matrix.translation.xyz = voided_obj.matrix_world @ local_position_on_voided_obj
rotation_matrix = Matrix.Rotation(radians(-90), 4, "X")
new_matrix @= rotation_matrix
else:
assert False, f"Unexpected layer set direction: {layers['layer_set_direction']}"
filling_obj.matrix_world = new_matrix
bpy.context.view_layer.update()
@@ -147,6 +142,7 @@ class FilledOpeningGenerator:
representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
assert representation
representation = ifcopenshell.util.representation.resolve_representation(representation)
else:
representation = self.generate_opening_from_filling(
@@ -165,7 +161,7 @@ class FilledOpeningGenerator:
voided_objs = [voided_obj]
# Openings affect all subelements of an aggregate
for subelement in ifcopenshell.util.element.get_decomposition(element):
for subelement in tool.Aggregate.get_parts_recursively(element):
subobj = tool.Ifc.get_object(subelement)
if subobj:
voided_objs.append(subobj)
@@ -173,8 +169,10 @@ class FilledOpeningGenerator:
for voided_obj in voided_objs:
if voided_obj.data:
voided_element = tool.Ifc.get_entity(voided_obj)
assert voided_element
context = tool.Geometry.get_active_representation_context(voided_obj)
representation = tool.Geometry.get_representation_by_context(voided_element, context)
assert representation
bonsai.core.geometry.switch_representation(
tool.Ifc,
@@ -223,7 +221,7 @@ class FilledOpeningGenerator:
opening_obj = tool.Ifc.get_object(opening)
if opening_obj:
tool.Ifc.unlink(element=opening)
bpy.data.objects.remove(opening_obj)
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
filling_obj = tool.Ifc.get_object(filling)
representation = self.generate_opening_from_filling(filling, filling_obj)
@@ -271,11 +269,12 @@ class FilledOpeningGenerator:
)
filling_obj = tool.Ifc.get_object(filling_type)
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
assert context
if profile:
profile = ifcopenshell.util.representation.resolve_representation(profile)
def get_curve_2d_from_3d(profile):
def get_curve_2d_from_3d(profile: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
if len(profile.Items) == 1:
curve_3d = profile.Items[0]
if tool.Ifc.get_schema() == "IFC2X3":
@@ -357,7 +356,7 @@ class FilledOpeningGenerator:
return shape_builder.get_representation(context, [extrusion])
def has_visible_openings(self, element):
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
for opening in [r.RelatedOpeningElement for r in tool.Geometry.get_openings(element)]:
if tool.Ifc.get_object(opening):
return True
return False
@@ -446,17 +445,9 @@ class AddPotentialOpening(Operator, AddObjectHelper):
bl_label = "Add Opening"
bl_description = "Add an Opening object which can be applied on an Element"
bl_options = {"REGISTER", "UNDO"}
x: FloatProperty(name="X", default=0.5)
y: FloatProperty(name="Y", default=0.5)
z: FloatProperty(name="Z", default=0.5)
def draw_settings(context, layout, tool):
row = self.layout.row()
row.prop(data=self, property="x", label="Size X")
row = self.layout.row()
row.prop(data=self, property="y")
row = self.layout.row()
row.prop(data=self, property="z")
x: FloatProperty(name="Size X", default=0.5, subtype="DISTANCE")
y: FloatProperty(name="Y", default=0.5, subtype="DISTANCE")
z: FloatProperty(name="Z", default=0.5, subtype="DISTANCE")
def execute(self, context):
props = context.scene.BIMModelProperties
@@ -496,10 +487,8 @@ class AddPotentialOpening(Operator, AddObjectHelper):
if new_matrix:
obj.matrix_world = new_matrix
tool.Model.clear_scene_openings()
new = props.openings.add()
new.obj = obj
tool.Model.purge_scene_openings()
tool.Root.add_tracked_opening(obj, "OPENING")
DecorationsHandler.install(context)
return {"FINISHED"}
@@ -511,7 +500,6 @@ class AddPotentialHalfSpaceSolid(Operator, AddObjectHelper):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMModelProperties
bm = bmesh.new()
bmesh.ops.create_grid(bm, size=0.5)
bm.verts.ensure_lookup_table()
@@ -523,10 +511,8 @@ class AddPotentialHalfSpaceSolid(Operator, AddObjectHelper):
obj = object_data_add(context, mesh, operator=self)
obj.name = "HalfSpaceSolid"
tool.Model.clear_scene_openings()
new = props.openings.add()
new.obj = obj
tool.Model.purge_scene_openings()
tool.Root.add_tracked_opening(obj, "BOOLEAN")
DecorationsHandler.install(context)
return {"FINISHED"}
@@ -540,280 +526,36 @@ class AddBoolean(Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
if not len(context.selected_objects) == 2:
cls.poll_message_set("Exactly 2 objects need to be selected.")
if not len(context.selected_objects) >= 2:
cls.poll_message_set("At least 2 objects need to be selected.")
return False
return True
def _execute(self, context):
props = context.scene.BIMModelProperties
obj1, obj2 = context.selected_objects
element1 = tool.Ifc.get_entity(obj1)
element2 = tool.Ifc.get_entity(obj2)
if not (bool(element1) ^ bool(element2)):
self.report({"INFO"}, "One of the selected objects should be blender object and another IFC object.")
return {"FINISHED"}
first_obj = tool.Blender.get_active_object()
if not first_obj or not tool.Geometry.is_boolean_operand(first_obj):
self.report({"INFO"}, "At least two valid objects must be selected to add a boolean.")
return {"CANCELLED"}
second_objs = [
o for o in tool.Blender.get_selected_objects() if o != first_obj and tool.Geometry.is_boolean_operand(o)
]
if not second_objs:
self.report({"INFO"}, "At least two representation items must be selected to add a boolean.")
return {"CANCELLED"}
# element1 - IFC object, element2 - blender object.
if element2 and not element1:
obj1, obj2 = obj2, obj1
element1, element2 = element2, element1
props = context.scene.BIMBooleanProperties
representation = tool.Geometry.get_active_representation(obj1)
if not representation:
self.report({"INFO"}, "No representation found for the selected IFC object.")
return {"FINISHED"}
first_item = tool.Ifc.get().by_id(first_obj.data.BIMMeshProperties.ifc_definition_id)
second_items = [tool.Ifc.get().by_id(o.data.BIMMeshProperties.ifc_definition_id) for o in second_objs]
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
if not obj2.data or len(obj2.data.polygons) <= 4: # It takes 4 faces to create a closed solid
mesh_data = {"type": "IfcHalfSpaceSolid", "matrix": obj1.matrix_world.inverted() @ obj2.matrix_world}
else:
mesh_data = {"type": "Mesh", "blender_obj": obj1, "blender_void": obj2}
booleans = ifcopenshell.api.run(
"geometry.add_boolean", tool.Ifc.get(), representation=representation, operator="DIFFERENCE", **mesh_data
)
tool.Model.mark_manual_booleans(element1, booleans)
tool.Model.clear_scene_openings()
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj1,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.data.objects.remove(obj2)
return {"FINISHED"}
class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
bl_idname = "bim.show_booleans"
bl_label = "Show Booleans"
bl_description = "Show booleans for the selected objects.\nCan be used to reset booleans positions"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
obj = context.active_object
return (
obj is not None
and obj.data
and hasattr(obj.data, "BIMMeshProperties")
and obj.data.BIMMeshProperties.ifc_definition_id
)
def _execute(self, context):
selected_objects = tool.Blender.get_selected_objects()
boolean_objs = []
for obj in selected_objects:
if not isinstance(mesh := obj.data, bpy.types.Mesh) or not mesh.BIMMeshProperties.ifc_definition_id:
continue
boolean_objs.extend(self.show_booleans(obj))
if boolean_objs:
DecorationsHandler.install(context)
tool.Blender.set_objects_selection(context, boolean_objs[0], boolean_objs)
return {"FINISHED"}
def show_booleans(self, obj: bpy.types.Object) -> list[bpy.types.Object]:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
booleans: list[ifcopenshell.entity_instance] = []
for item in representation.Items:
booleans.extend(self.get_booleans(item))
props = bpy.context.scene.BIMModelProperties
tool.Model.clear_scene_openings()
existing_booleans = {
boolean_obj.data.BIMMeshProperties.ifc_boolean_id: boolean_obj
for opening in props.openings
if (boolean_obj := opening.obj).data.BIMMeshProperties.obj == obj
}
booleans_objs: list[bpy.types.Object] = []
for boolean in booleans:
boolean_obj = None
if boolean.is_a() == "IfcHalfSpaceSolid":
surface = boolean.BaseSurface
if surface.is_a("IfcPlane"):
boolean_obj = self.create_half_space_solid()
position = surface.Position
position = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist())
position.translation *= unit_scale
boolean_obj.matrix_world = obj.matrix_world @ position
else:
self.report(
{"INFO"},
f"Showing boolean using non-IfcPlane IfcSurface ({surface.is_a()}) is not supported.",
)
else:
settings = ifcopenshell.geom.settings()
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
shape = ifcopenshell.geom.create_shape(settings, boolean)
if shape:
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = tool.Ifc.get()
mesh = ifc_importer.create_mesh(boolean, shape)
else:
mesh = None
boolean_obj = object_data_add(bpy.context, mesh, operator=self)
boolean_obj.name = "BooleanMesh"
boolean_obj.matrix_world = obj.matrix_world
if boolean_obj:
boolean_id = boolean.id()
# Remove existing boolean as it will be reloaded.
if boolean_id in existing_booleans:
bpy.data.objects.remove(existing_booleans[boolean_id])
boolean_obj.data.BIMMeshProperties.ifc_boolean_id = boolean_id
boolean_obj.data.BIMMeshProperties.obj = obj
new = props.openings.add()
new.obj = boolean_obj
booleans_objs.append(boolean_obj)
return booleans_objs
def get_booleans(self, item: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
results = []
if item.is_a("IfcBooleanResult"):
results.extend(self.get_booleans(item.FirstOperand))
results.append(item.SecondOperand)
return results
def create_half_space_solid(self) -> bpy.types.Object:
bm = bmesh.new()
bmesh.ops.create_grid(bm, size=0.5)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
mesh = bpy.data.meshes.new(name="Dumb Opening")
bm.to_mesh(mesh)
bm.free()
obj = object_data_add(bpy.context, mesh, operator=self)
obj.name = "HalfSpaceSolid"
return obj
class HideBooleans(Operator, tool.Ifc.Operator):
bl_idname = "bim.hide_booleans"
bl_label = "Hide Booleans"
bl_description = (
"Hide boolean objects and apply their changed transforms.\n"
"If boolean object is selected, hide it, otherwise hide all boolean objects"
)
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
set_active_obj, set_selected_objs = None, None
boolean_objs: list[bpy.types.Object]
selected_objects = tool.Blender.get_selected_objects()
selected_booleans_objs = [obj for obj in selected_objects if tool.Model.is_boolean_obj(obj)]
if selected_booleans_objs:
boolean_objs = selected_booleans_objs
if (active_object := context.active_object) in selected_booleans_objs:
active_obj_source = active_object
else:
active_obj_source = selected_booleans_objs[0]
set_active_obj = active_obj_source.data.BIMMeshProperties.obj
set_selected_objs = [tool.Model.get_booleaned_obj(obj) for obj in boolean_objs]
else:
props = bpy.context.scene.BIMModelProperties
boolean_objs = [obj for o in props.openings if (obj := o.obj)]
for obj in boolean_objs:
ifc_boolean_id = obj.data.BIMMeshProperties.ifc_boolean_id
boolean = tool.Ifc.get_entity_by_id(ifc_boolean_id)
# Update boolean transform.
if boolean:
main_obj = cast(bpy.types.Object, obj.data.BIMMeshProperties.obj)
if boolean.is_a("IfcHalfSpaceSolid"):
surface = boolean.BaseSurface
# Only IfcPlane is supported currently.
position = surface.Position
m = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist())
m.translation *= unit_scale
# Only update if transform was changed.
if not np.allclose(m, obj.matrix_world):
new_m = main_obj.matrix_world.inverted() @ obj.matrix_world
new_m.normalize()
new_m.translation /= unit_scale
new_m = np.array(new_m)
ifcopenshell.util.element.remove_deep2(ifc_file, position)
surface.Position = builder.create_axis2_placement_3d_from_matrix(new_m)
tool.Geometry.reload_representation(main_obj)
bpy.data.objects.remove(obj)
tool.Model.clear_scene_openings()
if set_active_obj and set_selected_objs is not None:
tool.Blender.set_objects_selection(context, set_active_obj, set_selected_objs)
return {"FINISHED"}
class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
bl_idname = "bim.remove_booleans"
bl_label = "Remove Booleans"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
upstream_obj = None
bbim_boolean_updates = {}
for obj in context.selected_objects:
if (
not obj.data
or not hasattr(obj.data, "BIMMeshProperties")
or not obj.data.BIMMeshProperties.ifc_boolean_id
):
continue
try:
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id)
except:
continue
boolean_id = None
for inverse in tool.Ifc.get().get_inverse(item):
if inverse.is_a("IfcBooleanResult"):
boolean_id = inverse.id()
break
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=item)
if obj.data.BIMMeshProperties.obj:
upstream_obj = obj.data.BIMMeshProperties.obj
element = tool.Ifc.get_entity(upstream_obj)
bbim_boolean_updates.setdefault(element, []).append(boolean_id)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=upstream_obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.data.objects.remove(obj)
for element, boolean_ids in bbim_boolean_updates.items():
tool.Model.unmark_manual_booleans(element, boolean_ids)
tool.Blender.set_active_object(upstream_obj)
return {"FINISHED"}
rep_obj = bpy.context.scene.BIMGeometryProperties.representation_obj
rep_element = tool.Ifc.get_entity(rep_obj)
tool.Model.mark_manual_booleans(rep_element, booleans)
tool.Geometry.reload_representation(rep_obj)
if props.is_editing:
bpy.ops.bim.enable_editing_booleans()
tool.Root.reload_item_decorator()
class ShowOpenings(Operator, tool.Ifc.Operator):
@@ -823,35 +565,115 @@ class ShowOpenings(Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
objs = context.selected_objects
# add the obj's aggregate, as well, to objs
for obj in objs:
objs = list(context.selected_objects)
# If several parts of an aggregation are selected, the aggregate openings may be queried more than once
objects_element_map = set()
while objs:
obj = objs.pop(0)
element = tool.Ifc.get_entity(obj)
if element and getattr(element, "Decomposes", None):
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
aggregate_obj = tool.Ifc.get_object(aggregate)
objs.append(aggregate_obj)
if element:
if getattr(element, "Decomposes", None):
# Select aggregate recursively
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
aggregate_obj = tool.Ifc.get_object(aggregate)
objs.append(aggregate_obj)
objects_element_map.add((obj, element))
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element or not getattr(element, "HasOpenings", None):
continue
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
openings = tool.Model.load_openings(
[
r.RelatedOpeningElement
for r in element.HasOpenings
if not tool.Ifc.get_object(r.RelatedOpeningElement)
],
)
for opening in openings:
new = props.openings.add()
new.obj = opening
opening.display_type = "WIRE"
for obj, element in objects_element_map:
self.show_object_openings(obj, element)
DecorationsHandler.install(bpy.context)
bpy.ops.bim.update_openings_focus()
return {"FINISHED"}
def show_object_openings(self, obj, element):
openings_elements = [rel.RelatedOpeningElement for rel in tool.Geometry.get_openings(element)]
if not openings_elements:
return
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
openings_elements_to_load = [o for o in openings_elements if not tool.Ifc.get_object(o)]
openings_objects = tool.Model.load_openings(openings_elements_to_load)
for opening in openings_objects:
self.on_new_opening_obj(opening)
def on_new_opening_obj(self, opening_obj: bpy.types.Object) -> None:
tool.Root.add_tracked_opening(opening_obj, "OPENING")
opening_obj.display_type = "WIRE"
class UpdateOpeningsFocus(Operator):
bl_idname = "bim.update_openings_focus"
bl_label = "Update Openings Focus"
bl_description = "Show objects that are not part of the object or its openings as transparent"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
preferences = tool.Blender.get_addon_preferences()
if preferences.opening_focus_opacity == 100:
return {"FINISHED"}
openings = set()
building_objects = set()
for opening in context.scene.BIMModelProperties.openings:
if opening.obj:
openings.add(opening.obj)
opening_element = tool.Ifc.get_entity(opening.obj)
if opening_element is None: # Most likely an opening created with bim.add_potential_opening
continue
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
building_objects.add(tool.Ifc.get_object(building_element))
for obj in context.scene.objects:
obj.color = [
obj.color[0],
obj.color[1],
obj.color[2],
(
1
if not context.scene.BIMModelProperties.openings
or not building_objects
or obj in openings
or obj in building_objects
or obj in context.selected_objects
else preferences.opening_focus_opacity / 100
),
]
return {"FINISHED"}
def hide_openings(context: bpy.types.Context, objects: Sequence[bpy.types.Object]) -> None:
objects_to_remove = set()
for opening_prop in context.scene.BIMModelProperties.openings:
opening_obj = opening_prop.obj
if not opening_obj:
continue
opening_element = tool.Ifc.get_entity(opening_obj)
if opening_element:
if not opening_element.is_a("IfcOpeningElement"):
# This opening has been assigned to another ifc class. Remove it from the openings pool. See #3854
opening_prop.obj = None
continue
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
if building_element:
building_obj = tool.Ifc.get_object(building_element)
if building_obj in objects:
tool.Ifc.unlink(element=opening_element)
objects_to_remove.add(opening_obj)
if opening_obj in objects:
objects_to_remove.add(opening_obj)
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
tool.Model.purge_scene_openings()
bpy.ops.bim.update_openings_focus()
class HideAllOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.hide_all_openings"
bl_label = "Hide All Openings"
bl_description = "Hide every single opening"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
hide_openings(context, context.scene.objects[:])
return {"FINISHED"}
@@ -862,23 +684,7 @@ class HideOpenings(Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
to_delete = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcOpeningElement"):
element = element.VoidsElements[0].RelatingBuildingElement
obj = tool.Ifc.get_object(element)
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
for opening in openings:
opening_obj = tool.Ifc.get_object(opening)
if opening_obj:
to_delete.add((opening, opening_obj))
for opening, opening_obj in to_delete:
tool.Ifc.unlink(element=opening)
bpy.data.objects.remove(opening_obj)
tool.Model.clear_scene_openings()
hide_openings(context, context.selected_objects[:])
return {"FINISHED"}
@@ -887,51 +693,90 @@ class EditOpenings(Operator, tool.Ifc.Operator):
bl_label = "Edit"
bl_description = "Edit Openings"
bl_options = {"REGISTER", "UNDO"}
apply_all: bpy.props.BoolProperty(default=False)
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
building_objs = set()
current_openings = bpy.data.collections.get("IfcOpeningElement").objects
for obj in current_openings:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcOpeningElement"):
element = element.VoidsElements[0].RelatingBuildingElement
obj = tool.Ifc.get_object(element)
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
for opening in openings:
opening_obj = tool.Ifc.get_object(opening)
building_objs.add(obj)
similar_openings = bonsai.core.geometry.get_similar_openings(tool.Ifc, opening)
similar_openings_building_objs = bonsai.core.geometry.get_similar_openings_building_objs(
tool.Ifc, similar_openings
)
building_objs.update(similar_openings_building_objs)
if opening_obj:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
bonsai.core.geometry.edit_similar_opening_placement(tool.Geometry, opening, similar_openings)
elif tool.Ifc.is_moved(opening_obj):
bonsai.core.geometry.edit_object_placement(
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
)
bonsai.core.geometry.edit_similar_opening_placement(tool.Geometry, opening, similar_openings)
building_objs.update(
self.get_all_building_objects_of_similar_openings(opening)
) # NB this has nothing to do with clone similar_opening
tool.Ifc.unlink(element=opening)
if bpy.context.scene.BIMGeometryProperties.representation_obj == opening_obj:
bpy.context.scene.BIMGeometryProperties.representation_obj = None
bpy.data.objects.remove(opening_obj)
building_objs, opening_elements = self.get_buildings_and_openings(context)
self.edit_openings(building_objs, opening_elements)
tool.Model.purge_scene_openings()
tool.Model.reload_body_representation(building_objs)
bpy.ops.bim.update_openings_focus()
return {"FINISHED"}
def get_buildings_and_openings(
self, context: bpy.types.Context
) -> tuple[set[bpy.types.Object], set[ifcopenshell.entity_instance]]:
props = context.scene.BIMModelProperties
building_objs: set[bpy.types.Object] = set()
opening_elements: set[ifcopenshell.entity_instance] = set()
objects_to_remove = set()
if self.apply_all:
for opening_prop in props.openings:
opening_obj = opening_prop.obj
if opening_obj is None:
continue
opening_element = tool.Ifc.get_entity(opening_obj)
if opening_element is None:
objects_to_remove.add(opening_obj)
continue
opening_elements.add(opening_element)
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
building_objs.add(tool.Ifc.get_object(building_element))
else:
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element.is_a("IfcOpeningElement"):
opening_element = element
opening_elements.add(opening_element)
if opening_element.VoidsElements:
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
building_obj = tool.Ifc.get_object(building_element)
if building_obj:
building_objs.add(building_obj)
else:
for relation in tool.Geometry.get_openings(element):
opening_element = relation.RelatedOpeningElement
if tool.Ifc.get_object(opening_element):
opening_elements.add(opening_element)
building_objs.add(obj)
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
return building_objs, opening_elements
def edit_openings(
self, building_objs: set[bpy.types.Object], opening_elements: set[ifcopenshell.entity_instance]
) -> None:
objects_to_remove: set[bpy.types.Object] = set()
for opening_element in opening_elements:
opening_obj = tool.Ifc.get_object(opening_element)
similar_openings = bonsai.core.geometry.get_similar_openings(tool.Ifc, opening_element)
similar_openings_building_objs = bonsai.core.geometry.get_similar_openings_building_objs(
tool.Ifc, similar_openings
)
building_objs.update(similar_openings_building_objs)
if opening_obj:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
bonsai.core.geometry.edit_similar_opening_placement(
tool.Geometry, opening_element, similar_openings
)
elif tool.Ifc.is_moved(opening_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj)
bonsai.core.geometry.edit_similar_opening_placement(
tool.Geometry, opening_element, similar_openings
)
building_objs.update(
self.get_all_building_objects_of_similar_openings(opening_element)
) # NB this has nothing to do with clone similar_opening
tool.Ifc.unlink(element=opening_element)
if bpy.context.scene.BIMGeometryProperties.representation_obj == opening_obj:
bpy.context.scene.BIMGeometryProperties.representation_obj = None
objects_to_remove.add(opening_obj)
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
def get_all_building_objects_of_similar_openings(self, opening):
if not opening.is_a("IfcOpeningElement") or not opening.HasFillings:
return []
@@ -952,20 +797,28 @@ class EditOpenings(Operator, tool.Ifc.Operator):
class CloneOpening(Operator, tool.Ifc.Operator):
bl_idname = "bim.clone_opening"
bl_label = "Clone Opening"
bl_description = "Clone the selected Opening and assign to the selected Element"
bl_description = "Clone the active Opening object and assign to the selected Element"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
objects = bpy.context.selected_objects
@classmethod
def poll(cls, context):
if len(context.selected_objects) != 2:
cls.poll_message_set("Exactly 2 objects must be selected.")
return False
return True
for obj in objects:
entity = tool.Ifc.get_entity(obj)
if entity.is_a() == "IfcWall":
wall = entity
continue
if entity.is_a() == "IfcOpeningElement":
opening = entity
continue
def _execute(self, context):
# NOTE: Operator displayed in UI only with IfcOpeningElement being active.
ifc_file = tool.Ifc.get()
objects = bpy.context.selected_objects
opening_obj = context.active_object
assert opening_obj
opening = tool.Ifc.get_entity(opening_obj)
assert opening and opening.is_a("IfcOpeningElement")
voided_obj = next(o for o in objects if o != opening_obj)
voided_element = tool.Ifc.get_entity(voided_obj)
assert voided_element
opening_placement = opening.ObjectPlacement
opening_representation = opening.Representation
@@ -973,8 +826,127 @@ class CloneOpening(Operator, tool.Ifc.Operator):
new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement")
new_opening.Representation = opening_representation
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=new_opening, element=wall)
ifcopenshell.api.void.add_opening(ifc_file, opening=new_opening, element=voided_element)
new_opening.ObjectPlacement = opening_placement
# Update affected representations.
elements_to_update = tool.Aggregate.get_parts_recursively(voided_element)
for element in elements_to_update:
obj = tool.Ifc.get_object(element)
if not isinstance(obj, bpy.types.Object) or not isinstance(obj.data, bpy.types.Mesh):
continue
representation = tool.Geometry.get_active_representation(obj)
assert representation
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
class PurgeUnusedOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.purge_unused_openings"
bl_label = "Purge Unused Openings"
bl_description = "Purge Openings that do not intersect with their related building element"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return any(
[tool.Geometry.has_openings(element)]
for element in [tool.Ifc.get_entity(obj) for obj in context.selected_objects]
if element
)
def _execute(self, context):
bpy.ops.bim.show_openings()
objects = context.selected_objects[:]
[o.select_set(False) for o in objects]
active_object = context.active_object
purged = 0
for obj in objects:
element = tool.Ifc.get_entity(obj)
if not element or not tool.Geometry.has_openings(element):
continue
obj_bvh_tree = tool.Geometry.get_bvh_tree(obj)
for opening_rel in tool.Geometry.get_openings(element):
opening_elt = opening_rel.RelatedOpeningElement
opening_obj = tool.Ifc.get_object(opening_elt)
opening_bvh_tree = tool.Geometry.get_bvh_tree(opening_obj)
if not opening_bvh_tree.overlap(obj_bvh_tree):
opening_obj.select_set(True)
purged += 1
if context.selected_objects:
bpy.ops.bim.override_object_delete(is_batch=False)
bpy.ops.bim.edit_openings(apply_all=True)
[o.select_set(True) for o in objects]
context.view_layer.objects.active = active_object
self.report({"INFO"}, f"{purged} unused openings were purged.")
return {"FINISHED"}
class RemoveBoolean(Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_boolean"
bl_label = "Remove Boolean"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Removes the actively selected boolean"
@classmethod
def poll(cls, context):
props = context.scene.BIMBooleanProperties
return props.active_boolean
def _execute(self, context):
props = context.scene.BIMBooleanProperties
ifcopenshell.api.geometry.remove_boolean(
tool.Ifc.get(), tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)
)
bpy.ops.bim.enable_editing_booleans()
rep_obj = bpy.context.scene.BIMGeometryProperties.representation_obj
tool.Geometry.reload_representation(rep_obj)
tool.Root.reload_item_decorator()
class SelectBoolean(Operator):
bl_idname = "bim.select_boolean"
bl_label = "Remove Boolean"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Selects operands of the active boolean\nSHIFT-CLICK to select all operands recursively"
is_recursive: bpy.props.BoolProperty(name="Is Recursive", default=False, options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
props = context.scene.BIMBooleanProperties
return props.active_boolean
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.shift:
self.is_recursive = True
return self.execute(context)
def execute(self, context):
props = context.scene.BIMBooleanProperties
queue = [tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)]
items = {i.ifc_definition_id: i.obj for i in context.scene.BIMGeometryProperties.item_objs}
while queue:
item = queue.pop()
if item.is_a("IfcBooleanResult"):
if self.is_recursive:
queue.append(item.FirstOperand)
queue.append(item.SecondOperand)
else:
if obj := items.get(item.FirstOperand.id()):
tool.Blender.select_object(obj)
if obj := items.get(item.SecondOperand.id()):
tool.Blender.select_object(obj)
elif obj := items.get(item.id()):
tool.Blender.select_object(obj)
return {"FINISHED"}
@@ -1004,6 +976,8 @@ class DecorationsHandler:
batch.draw(shader)
def __call__(self, context):
if not context.scene.BIMModelProperties.openings:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
@@ -1019,6 +993,12 @@ class DecorationsHandler:
for opening in context.scene.BIMModelProperties.openings:
obj = opening.obj
if context.scene.BIMGeometryProperties.representation_obj == obj:
# We are editing the representation of the opening :
for item in context.scene.BIMGeometryProperties.item_objs:
if item.obj.mode == "EDIT":
obj = item.obj
break
if not obj:
continue
@@ -1069,9 +1049,14 @@ class DecorationsHandler:
bm.from_mesh(obj.data)
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
edges = [tuple([v.index for v in e.verts]) for e in bm.edges]
if ios_edges_attribute := obj.data.attributes.get("ios_edges"):
edges = [e for i, e in enumerate(bm.edges) if ios_edges_attribute.data[i].value]
else:
edges = bm.edges
edges_indices = [tuple([v.index for v in e.verts]) for e in edges]
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges)
self.draw_batch("LINES", verts, color, edges_indices)
obj.data.calc_loop_triangles()
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
+456 -2
View File
@@ -35,14 +35,440 @@ import bonsai.core.geometry
import bonsai.core.model as core
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from math import pi, sin, cos, degrees
from mathutils import Vector, Matrix
from math import pi, sin, cos, degrees, tan
from mathutils import Vector, Matrix, Quaternion
from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.geometry.decorator import ItemDecorator
from typing import Optional, Union, Literal
from lark import Lark, Transformer
def get_wall_preview_data(context, relating_type):
def create_bmesh_from_vertices(vertices):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in polyline_vertices]
if is_closed:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
new_edges.append(
bm.edges.new((new_verts[-1], new_verts[0]))
) # Add an edge between the last an first point to make it closed.
else:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
return bm
# Get properties from object type
layers = tool.Model.get_material_layer_parameters(relating_type)
if not layers["thickness"]:
return
thickness = layers["thickness"]
model_props = context.scene.BIMModelProperties
direction_sense = model_props.direction_sense
direction = 1
if direction_sense == "NEGATIVE":
direction = -1
offset_type = model_props.offset_type
offset = 0
if offset_type == "CENTER":
offset = -thickness / 2
elif offset_type == "INTERIOR":
offset = -thickness
unit_system = tool.Drawing.get_unit_system()
factor = 1
if unit_system == "IMPERIAL":
factor = 3.28084
if unit_system == "METRIC":
unit_length = context.scene.unit_settings.length_unit
if unit_length == "MILLIMETERS":
factor = 1000
# For the model properties, the offset value should just be converted
# However, for the wall preview logic that follows, offset and thickness must change direction
model_props.offset = offset * factor
thickness *= direction
offset *= direction
height = float(model_props.extrusion_depth)
rl = float(model_props.rl1)
x_angle = float(model_props.x_angle)
angle_distortion = height * tan(x_angle)
data = {}
data["verts"] = []
# Verts
polyline_vertices = []
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
data = []
return
for point in polyline_points:
polyline_vertices.append(Vector((point.x, point.y, point.z)))
is_closed = False
if (
polyline_vertices[0].x == polyline_vertices[-1].x
and polyline_vertices[0].y == polyline_vertices[-1].y
and polyline_vertices[0].z == polyline_vertices[-1].z
):
is_closed = True
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
bm_base = create_bmesh_from_vertices(polyline_vertices)
base_vertices = tool.Cad.offset_edges(bm_base, offset)
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
top_vertices = tool.Cad.offset_edges(bm_base, angle_distortion + offset)
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distortion + thickness + offset)
if is_closed:
base_vertices.append(base_vertices[0])
offset_base_verts.append(offset_base_verts[0])
top_vertices.append(top_vertices[0])
offset_top_verts.append(offset_top_verts[0])
if offset_base_verts is not None:
for v in base_vertices:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in offset_base_verts[::-1]:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in top_vertices:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
for v in offset_top_verts[::-1]:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
bm_base.free()
# Edges and Tris
points = []
side_edges_1 = []
side_edges_2 = []
base_edges = []
for i in range(len(data["verts"])):
points.append(Vector(data["verts"][i]))
n = len(points) // 2
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
side_edges_1.extend(bottom_side_1)
side_edges_2.extend(bottom_side_2)
base_edges.extend(bottom_loop)
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
side_edges_1.extend(upper_side_1)
side_edges_2.extend(upper_side_2)
base_edges.extend(upper_loop)
loops = [side_edges_1, side_edges_2, base_edges]
data["edges"] = []
data["tris"] = []
for i, group in enumerate(loops):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
bm.verts.index_update()
bm.edges.index_update()
if i == 2:
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
bm.verts.index_update()
bm.edges.index_update()
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
data["edges"].extend(edges)
data["tris"].extend(tris)
data["edges"] = list(set(tuple(e) for e in data["edges"]))
data["tris"] = list(set(tuple(t) for t in data["tris"]))
return data
def get_vertical_profile_preview_data(context, relating_type):
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile = material.MaterialProfiles[0].Profile
except:
return {}
model_props = context.scene.BIMModelProperties
extrusion_depth = model_props.extrusion_depth
cardinal_point = model_props.cardinal_point
rot_mat = Quaternion()
if relating_type.is_a("IfcBeamType"):
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
rot_mat = y_rot @ z_rot
# Get profile data
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts
if not verts:
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create extrusion bmesh
bm = bmesh.new()
grouped_verts.append(grouped_verts[0]) # Close profile
new_verts = [bm.verts.new(v) for v in grouped_verts]
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
# Calculate rotation, mouse position, angle and cardinal point
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
data = {}
verts = [tuple(v.co) for v in bm.verts]
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
verts = [tuple(Vector(v) + mouse_point) for v in verts]
min_z = min(v.co.z for v in bm.verts)
max_z = max(v.co.z for v in bm.verts)
# Add axis verts
verts.append(tuple(mouse_point))
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
# Add only profile edges
edges = []
for edge in bm.edges:
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
):
edges.append(edge)
# Add axis edge
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
edges.append((len(verts) - 1, len(verts) - 2))
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
bm.free()
return data
def get_horizontal_profile_preview_data(context, relating_type):
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile_curve = material.MaterialProfiles[0].Profile
except:
return {}
model_props = context.scene.BIMModelProperties
cardinal_point = model_props.cardinal_point
polyline_verts = []
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
return
for point in polyline_points:
polyline_verts.append(Vector((point.x, point.y, point.z)))
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
# Get profile shape
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
verts = shape.verts
if not verts:
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create profile curve
scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0))
grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts]
profile_curve = bpy.data.curves.new("Profile", type="CURVE")
profile_curve.dimensions = "2D"
profile_curve.splines.new("POLY")
profile_curve.splines[0].points.add(len(grouped_verts))
for i, point in enumerate(profile_curve.splines[0].points):
if i == len(grouped_verts): # Close curve
point.co = Vector((*grouped_verts[0], 0))
continue
point.co = Vector((*grouped_verts[i], 0))
profile_obj = bpy.data.objects.new("Profile", profile_curve)
# Create path curve with profile object as bevel
path_curve = bpy.data.curves.new("Polyline", type="CURVE")
path_curve.dimensions = "2D"
path_curve.splines.new("POLY")
path_curve.splines[0].points.add(len(polyline_verts) - 1)
for i, point in enumerate(path_curve.splines[0].points):
point.co = Vector((*polyline_verts[i], 0))
path_curve.splines[0].use_smooth = False
path_curve.bevel_mode = "OBJECT"
path_curve.bevel_object = profile_obj
# Convert path curve to mesh
# This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here
path_obj = bpy.data.objects.new("Preview", path_curve)
context.scene.collection.objects.link(path_obj)
bpy.context.view_layer.objects.active = path_obj
dg = context.evaluated_depsgraph_get()
path_obj = path_obj.evaluated_get(dg)
me = path_obj.to_mesh()
# Create bmesh from path mesh
bm = bmesh.new()
new_verts = [bm.verts.new(v.co) for v in me.vertices]
index = [[v for v in edge.vertices] for edge in me.edges]
new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index]
for face in me.polygons:
verts = [new_verts[i] for i in face.vertices]
bm.faces.new(verts)
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True)
bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True)
try:
bpy.data.curves.remove(profile_obj.data, do_unlink=True)
except:
pass
try:
bpy.data.curves.remove(path_obj.data, do_unlink=True)
except:
pass
data = {}
data["verts"] = [tuple(v.co) for v in bm.verts]
data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
data["tris"] = tris
bm.free()
return data
def get_product_preview_data(context, relating_type):
model_props = context.scene.BIMModelProperties
if relating_type.is_a("IfcDoorType"):
rl = float(model_props.rl1)
elif relating_type.is_a("IfcWindowType"):
rl = float(model_props.rl2)
else:
rl = 0
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
snap_element = tool.Ifc.get_entity(snap_obj)
rot_mat = Quaternion()
if snap_element and snap_element.is_a("IfcWall"):
rot_mat = snap_obj.matrix_world.to_quaternion()
obj_type = tool.Ifc.get_object(relating_type)
if obj_type.data:
data = ItemDecorator.get_obj_data(obj_type)
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
return data
class PolylineOperator:
# TODO Fill doc strings
""" """
@@ -387,6 +813,34 @@ class PolylineOperator:
tool.Polyline.remove_last_polyline_point()
tool.Blender.update_viewport()
def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_isntance):
if tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() not in {"IfcColumnType"}:
data = get_horizontal_profile_preview_data(context, relating_type)
elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() in {"IfcColumnType"}:
data = get_vertical_profile_preview_data(context, relating_type)
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
data = get_wall_preview_data(context, relating_type)
else:
data = get_product_preview_data(context, relating_type)
# Update properties so it can be used by the decorator
if not data:
return
props = context.scene.BIMProductPreviewProperties
props.verts.clear()
props.edges.clear()
props.tris.clear()
for vert in data["verts"]:
v = props.verts.add()
v.value_3d = vert
for edge in data["edges"]:
e = props.edges.add()
e.value_2d = edge
for tri in data["tris"]:
t = props.tris.add()
t.value_3d = tri
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> Union[set[str], None]:
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
tool.Blender.update_viewport()
+12 -4
View File
@@ -191,6 +191,8 @@ class AddOccurrence(bpy.types.Operator, PolylineOperator):
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
self.create_occurrence(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event)
if cancel is not None:
ProductDecorator.uninstall()
@@ -417,7 +419,7 @@ class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator):
):
if instance_class in ["IfcWindow", "IfcDoor"]:
# TODO For now we are hardcoding windows and doors as a prototype
bpy.ops.bim.add_filled_opening(voided_obj=building_obj.name, filling_obj=obj.name)
tool.Model.add_filled_opening(building_obj, obj)
else:
if self.container_obj:
if props.rl_mode == "BOTTOM":
@@ -489,7 +491,6 @@ class SetActiveType(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMModelProperties
props.relating_type_id = str(self.relating_type)
context.window.screen = context.window.screen # Closes the type manager popup
class AlignProduct(bpy.types.Operator):
@@ -564,10 +565,11 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
return
props = bpy.context.scene.BIMModelProperties
processing = set()
# Only process at most one paginated class at a time.
# Large projects have hundreds of types which can lead to unnecessary lag.
queue = sorted(tool.Ifc.get().by_type(self.ifc_class), key=lambda e: e.Name or "Unnamed")
if not AuthoringData.is_loaded:
AuthoringData.load()
queue = AuthoringData.data["type_elements_filtered"]
if self.limit:
queue = queue[self.offset : self.offset + self.limit]
else:
@@ -576,6 +578,12 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
offset = 0
queue = queue[offset : offset + 9]
# The active type may be in another page than the active one :
if relating_type_id_current := AuthoringData.data["relating_type_id_current"]:
active_element = tool.Ifc.get_entity_by_id(int(relating_type_id_current))
if active_element and active_element not in queue:
queue.append(active_element)
while queue:
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
element = queue.pop()
+170 -14
View File
@@ -32,11 +32,13 @@ import bonsai.core.type
import bonsai.core.geometry
import bonsai.core.material
import bonsai.core.root
from math import pi, degrees, inf
from math import pi, degrees, inf, atan2
from mathutils import Vector, Matrix, Quaternion
from bonsai.bim.module.geometry.helper import Helper
from bonsai.bim.module.model.wall import DumbWallRecalculator
from bonsai.bim.module.model.decorator import ProfileDecorator
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union, Any, Optional
class DumbProfileGenerator:
@@ -44,7 +46,7 @@ class DumbProfileGenerator:
self.relating_type = relating_type
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self):
def generate(self, insertion_type="CURSOR"):
self.file = tool.Ifc.get()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
material = ifcopenshell.util.element.get_material(self.relating_type)
@@ -67,7 +69,28 @@ class DumbProfileGenerator:
self.rotation = 0
self.location = Vector((0, 0, 0))
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
return self.derive_from_cursor()
if insertion_type == "POLYLINE":
return self.derive_from_polyline()
elif insertion_type == "CURSOR":
return self.derive_from_cursor()
def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]:
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
is_polyline_closed = False
if len(polyline_points) > 3:
first_vec = Vector((polyline_points[0].x, polyline_points[0].y, polyline_points[0].z))
last_vec = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z))
if first_vec == last_vec:
is_polyline_closed = True
profiles = []
for i in range(len(polyline_points) - 1):
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
coords = (vec1, vec2)
profiles.append(self.create_profile_from_2_points(coords))
return profiles, is_polyline_closed
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
@@ -82,10 +105,13 @@ class DumbProfileGenerator:
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
matrix_world = Matrix()
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"] or self.relating_type.is_a(
if self.relating_type.is_a() in ["IfcBeamType", "IfcCoveringType", "IfcMemberType"] or self.relating_type.is_a(
"IfcFlowSegmentType"
):
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
matrix_world.translation = self.location
if self.container_obj:
matrix_world.translation.z = self.container_obj.location.z
@@ -146,6 +172,25 @@ class DumbProfileGenerator:
return obj
def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]:
direction = coords[1] - coords[0]
length = direction.length
if round(length, 4) < 0.1:
return
data = {"coords": coords}
self.depth = length
self.rotation = atan2(direction[1], direction[0])
if should_round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_profile()
return data
class DumbProfileRegenerator:
def regenerate_from_profile_def(self, profile):
@@ -272,7 +317,7 @@ class DumbProfileJoiner:
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
def unjoin(self, profile1):
def unjoin(self, profile1: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
@@ -285,7 +330,7 @@ class DumbProfileJoiner:
body = copy.deepcopy(axis1)
self.recreate_profile(element1, profile1, axis, body)
def join_E(self, profile1, target, connection=None):
def join_E(self, profile1: bpy.types.Object, target: Vector, connection: Optional[str] = None) -> None:
"""`connection` = `ATEND` / `ATSTART` to explicitly define the reference point for the join.
For example if profile 1m long and `target` is at (0, 0, 0.1) and `connection` = `None`
@@ -309,7 +354,7 @@ class DumbProfileJoiner:
body[1 if connection == "ATEND" else 0] = intersect
self.recreate_profile(element1, profile1, axis, body)
def set_depth(self, profile1, si_length):
def set_depth(self, profile1: bpy.types.Object, si_length: float) -> None:
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
@@ -324,7 +369,7 @@ class DumbProfileJoiner:
body[1] = end
self.recreate_profile(element1, profile1, axis, body)
def join_T(self, profile1, profile2):
def join_T(self, profile1: bpy.types.Object, profile2: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
@@ -348,7 +393,7 @@ class DumbProfileJoiner:
self.recreate_profile(element1, profile1, axis1, axis1)
def join_V(self, profile1, profile2):
def join_V(self, profile1: bpy.types.Object, profile2: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
@@ -374,7 +419,7 @@ class DumbProfileJoiner:
self.recreate_profile(element1, profile1, axis1, axis1)
self.recreate_profile(element2, profile2, axis2, axis2)
def join_L(self, profile1, profile2):
def join_L(self, profile1: bpy.types.Object, profile2: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
@@ -400,7 +445,7 @@ class DumbProfileJoiner:
self.recreate_profile(element1, profile1, axis1, axis1)
self.recreate_profile(element2, profile2, axis2, axis2)
def recreate_profile(self, element, obj, axis=None, body=None):
def recreate_profile(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None):
if axis is None or body is None:
axis = body = self.get_profile_axis(obj)
self.axis = copy.deepcopy(axis)
@@ -791,10 +836,10 @@ class DumbProfileJoiner:
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
return self.create_matrix(p, x_axis, y_axis, z_axis)
def create_matrix(self, p, x, y, z):
def create_matrix(self, p: Vector, x: Vector, y: Vector, z: Vector) -> Matrix:
return Matrix([x, y, z, p]).to_4x4().transposed()
def get_profile_axis(self, obj):
def get_profile_axis(self, obj: bpy.types.Object) -> list[Vector]:
z_values = [v[2] for v in obj.bound_box]
return [
(obj.matrix_world @ Vector((0.0, 0.0, min(z_values)))),
@@ -1064,3 +1109,114 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
joiner = DumbProfileJoiner()
joiner.set_depth(obj, depth)
return {"FINISHED"}
class DrawPolylineProfile(bpy.types.Operator, PolylineOperator):
bl_idname = "bim.draw_polyline_profile"
bl_label = "Draw Polyline Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.space_data.type == "VIEW_3D"
def __init__(self):
super().__init__()
self.relating_type = None
props = bpy.context.scene.BIMModelProperties
relating_type_id = props.relating_type_id
if relating_type_id:
self.relating_type = tool.Ifc.get().by_id(int(relating_type_id))
def create_profiles_from_polyline(self, context: bpy.types.Context) -> Union[set[str], None]:
if not self.relating_type:
return {"FINISHED"}
model_props = context.scene.BIMModelProperties
direction_sense = model_props.direction_sense
offset = model_props.offset
profiles, is_polyline_closed = DumbProfileGenerator(self.relating_type).generate("POLYLINE")
if profiles:
if is_polyline_closed:
for profile1, profile2 in zip(profiles, profiles[1:] + [profiles[0]]):
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
else:
for profile1, profile2 in zip(profiles[:-1], profiles[1:]):
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
def modal(self, context, event):
if not self.relating_type:
self.report({"WARNING"}, "You need to select a profile type.")
PolylineDecorator.uninstall()
tool.Blender.update_viewport()
return {"FINISHED"}
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
tool.Blender.update_viewport()
self.handle_lock_axis(context, event) # Must come before "PASS_TRHOUGH"
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
self.handle_mouse_move(context, event)
return {"PASS_THROUGH"}
# Wall axis settings
if event.value == "RELEASE" and event.type == "F":
direction_sense = context.scene.BIMModelProperties.direction_sense
context.scene.BIMModelProperties.direction_sense = (
"NEGATIVE" if direction_sense == "POSITIVE" else "POSITIVE"
)
if event.value == "RELEASE" and event.type == "O":
offset_type = context.scene.BIMModelProperties.offset_type
items = ["EXTERIOR", "CENTER", "INTERIOR"]
index = items.index(offset_type)
size = len(items)
context.scene.BIMModelProperties.offset_type = items[((index + 1) % size)]
props = bpy.context.scene.BIMModelProperties
wall_config = f"""Direction: {props.direction_sense}
Offset Type: {props.offset_type}
Offset Value: {props.offset}
"""
self.handle_instructions(context, wall_config)
self.handle_mouse_move(context, event, should_round=True)
self.choose_axis(event)
self.handle_snap_selection(context, event)
if (
not self.tool_state.is_input_on
and event.value == "RELEASE"
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
):
self.create_profiles_from_polyline(context)
context.workspace.status_text_set(text=None)
ProductDecorator.uninstall()
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
tool.Blender.update_viewport()
return {"FINISHED"}
self.handle_keyboard_input(context, event)
self.handle_inserting_polyline(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event)
if cancel is not None:
ProductDecorator.uninstall()
return cancel
return {"RUNNING_MODAL"}
def invoke(self, context, event):
super().invoke(context, event)
ProductDecorator.install(context)
self.tool_state.use_default_container = True
self.tool_state.plane_method = "XY"
return {"RUNNING_MODAL"}
+108 -5
View File
@@ -25,6 +25,7 @@ from bonsai.bim.prop import ObjProperty
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import PropertyGroup, NodeTree
from math import pi, radians
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
def get_ifc_class(self, context):
@@ -42,6 +43,9 @@ def get_boundary_class(self, context):
def get_relating_type_id(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
else:
AuthoringData.data["type_elements"] = AuthoringData.type_elements()
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
return AuthoringData.data["relating_type_id"]
@@ -64,10 +68,14 @@ def update_relating_type_id(self, context):
AuthoringData.data["relating_type_name"] = AuthoringData.relating_type_name()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
AuthoringData.data["predefined_type"] = AuthoringData.predefined_type()
self.type_page = [e[0] for e in AuthoringData.data["relating_type_id"]].index(self.relating_type_id) // 9 + 1
def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class, offset=9 * (self.type_page - 1), limit=9)
self["type_page"] = min(self["type_page"], AuthoringData.data["total_pages"])
self["type_page"] = max(self["type_page"], 1)
def update_relating_array_from_object(self, context):
@@ -82,11 +90,46 @@ def is_object_array_applicable(self, obj):
return ifcopenshell.util.element.get_pset(element, "BBIM_Array")
def update_wall_axis_decorator(self, context):
if self.show_wall_axis:
WallAxisDecorator.install(bpy.context)
else:
WallAxisDecorator.uninstall()
def update_slab_direction_decorator(self, context):
if self.show_slab_direction:
SlabDirectionDecorator.install(bpy.context)
else:
SlabDirectionDecorator.uninstall()
def update_search_name(self, context):
AuthoringData.load()
# Total number of pages may decrease when using the search bar :
if self.type_page > AuthoringData.data["total_pages"]:
self.type_page = max(1, AuthoringData.data["total_pages"])
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
def update_x_angle(self, context):
angle_deg = math.degrees(self.x_angle)
if tool.Cad.is_x(angle_deg, -90, 0.5) or tool.Cad.is_x(angle_deg, 90, 0.5):
self.x_angle = 0
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
relating_type_id: bpy.props.EnumProperty(
items=get_relating_type_id, name="Relating Type", update=update_relating_type_id
)
search_name: bpy.props.StringProperty(
name="Search Name",
default="",
description="Use this property to filter the list of available types",
update=update_search_name,
options={"SKIP_SAVE", "TEXTEDIT_UPDATE"},
)
menu_relating_type_id: bpy.props.IntProperty()
icon_id: bpy.props.IntProperty()
updating: bpy.props.BoolProperty(default=False)
@@ -146,8 +189,10 @@ class BIMModelProperties(PropertyGroup):
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
# Used for plan calculation points such as in room generation
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=-pi / 180 * 89, max=pi / 180 * 89)
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
x_angle: bpy.props.FloatProperty(
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
direction_sense: bpy.props.EnumProperty(
@@ -162,6 +207,16 @@ class BIMModelProperties(PropertyGroup):
description="It's a convention that affects the offset to reference line",
)
offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line")
show_wall_axis: bpy.props.BoolProperty(
name="Show Wall Axis",
default=False,
update=update_wall_axis_decorator,
)
show_slab_direction: bpy.props.BoolProperty(
name="Show Slab Direction",
default=False,
update=update_slab_direction_decorator,
)
class BIMArrayProperties(PropertyGroup):
@@ -195,6 +250,24 @@ class BIMArrayProperties(PropertyGroup):
)
def update_total_length_target(self, context):
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
def update_tread_run(self, context):
if self.total_length_lock:
self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1)
else:
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
def update_number_of_treads(self, context):
if self.total_length_lock:
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
else:
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
class BIMStairProperties(PropertyGroup):
def validate_nosing_value(self, context):
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
@@ -210,9 +283,26 @@ class BIMStairProperties(PropertyGroup):
is_editing: bpy.props.BoolProperty(default=False)
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01, subtype="DISTANCE")
number_of_treads: bpy.props.IntProperty(name="Number of Treads", default=6, soft_min=1)
number_of_treads: bpy.props.IntProperty(
name="Number of Treads", default=6, soft_min=1, update=update_number_of_treads
)
total_length_target: bpy.props.FloatProperty(
name="Total Length Target",
default=3.0,
soft_min=0.01,
subtype="DISTANCE",
update=update_total_length_target,
description="Total Length Target, might not be exactly respected depending on the parameters",
)
total_length_lock: bpy.props.BoolProperty(
default=False,
name="Lock Total Length",
description="Lock Total Length when changing number of treads or tread run",
)
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01, subtype="DISTANCE")
tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE")
tread_run: bpy.props.FloatProperty(
name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE", update=update_tread_run
)
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
@@ -739,7 +829,9 @@ class BIMRoofProperties(PropertyGroup):
soft_max=to_percentage(radians(60.0)),
)
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE")
rafter_edge_angle: bpy.props.FloatProperty(name="Rafter Edge Angle", min=0, max=pi, default=pi / 2, subtype="ANGLE")
rafter_edge_angle: bpy.props.FloatProperty(
name="Rafter Edge Angle", min=0, max=pi / 2, default=pi / 2, subtype="ANGLE"
)
def get_general_kwargs(self, generation_method=None, convert_to_project_units=False):
if generation_method is None:
@@ -795,3 +887,14 @@ class BIMPolylineProperties(PropertyGroup):
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
insertion_polyline: bpy.props.CollectionProperty(type=Polyline)
measurement_polyline: bpy.props.CollectionProperty(type=Polyline)
class ProductPreviewItem(PropertyGroup):
value_3d: bpy.props.FloatVectorProperty()
value_2d: bpy.props.FloatVectorProperty(size=2)
class BIMProductPreviewProperties(PropertyGroup):
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
+42 -5
View File
@@ -26,7 +26,6 @@ import ifcopenshell.util.unit
import bonsai.core.root
import bonsai.core.geometry
import bonsai.tool as tool
from bonsai.bim.module.model.door import bm_sort_out_geom
from bonsai.bim.module.model.data import RailingData, refresh
from bonsai.bim.module.model.decorator import ProfileDecorator
@@ -47,7 +46,7 @@ def bm_split_edge_at_offset(edge: bmesh.types.BMEdge, offset: float) -> dict[str
split_output_0 = bmesh.utils.edge_split(edge, v0, offset / edge_len)
split_output_1 = bmesh.utils.edge_split(edge, v1, offset / (edge_len - offset))
new_geometry = bm_sort_out_geom(split_output_0 + split_output_1)
new_geometry = tool.Model.bm_sort_out_geom(split_output_0 + split_output_1)
return new_geometry
@@ -175,11 +174,11 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None:
ortho_vector = edge_dir.cross(V_(0, 0, 1))
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
# dissolve middle edge
@@ -187,7 +186,7 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None:
# height
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
extrusion_vector = Vector((0, 0, 1)) * height
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
@@ -354,6 +353,44 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
tool.Model.add_body_representation(obj)
class CopyRailingParameters(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.copy_railing_parameters"
bl_label = "Copy Railing Parameters from Active to Selected"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not context.active_object or len(context.selected_objects) < 1:
cls.poll_message_set("At least 2 objects must be selected.")
return False
return True
def _execute(self, context):
source_obj = context.active_object
source_props = source_obj.BIMRailingProperties
railing_data = source_props.get_general_kwargs(convert_to_project_units=True)
for target_obj in context.selected_objects:
if target_obj == source_obj:
continue
context.view_layer.objects.active = target_obj
RailingData.load()
if not "path_data" in RailingData.data:
continue
railing_data["path_data"] = RailingData.data["path_data"]
target_element = tool.Ifc.get_entity(target_obj)
target_props = target_obj.BIMRailingProperties
target_props.set_props_kwargs_from_ifc_data(railing_data)
update_bbim_railing_pset(target_element, railing_data)
refresh()
update_railing_modifier_bmesh(context)
update_railing_modifier_ifc_data(context)
context.view_layer.objects.active = source_obj
return {"FINISHED"}
class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_railing"
bl_label = "Enable Editing Railing"
+241 -211
View File
@@ -25,18 +25,15 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.core.root
import bonsai.tool as tool
from bonsai.bim.helper import convert_property_group_from_si
from bonsai.bim.module.model.door import bm_sort_out_geom
from bonsai.bim.module.model.data import RoofData, refresh
from bonsai.bim.module.model.decorator import ProfileDecorator
import json
from math import tan, pi, radians
from mathutils import Vector, Matrix
from math import cos, tan, pi, radians
from mathutils import Vector, Matrix, Quaternion
import mathutils.geometry
from bpypolyskel import bpypolyskel
import shapely
from pprint import pprint
from typing import Literal, Union, Any
# reference:
@@ -90,7 +87,7 @@ class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator):
self.report(op_status, error_message)
return {"CANCELLED"}
generate_hiped_roof_bmesh(bm, self.mode, self.height, self.angle)
generate_hipped_roof_bmesh(bm, self.mode, self.height, self.angle)
tool.Blender.apply_bmesh(obj.data, bm)
return {"FINISHED"}
@@ -108,7 +105,7 @@ def is_valid_roof_footprint(bm: bmesh.types.BMesh) -> tuple[set[str], str]:
return ({"FINISHED"}, "")
def generate_hiped_roof_bmesh(
def generate_hipped_roof_bmesh(
bm: bmesh.types.BMesh,
mode: Literal["ANGLE", "HEIGHT"] = "ANGLE",
height: float = 1.0,
@@ -126,29 +123,23 @@ def generate_hiped_roof_bmesh(
If roof bmesh needed only to supply into decorator then there is no reason to mutate it.
"""
if not mutate_current_bmesh:
bm = bm.copy()
# CLEAN UP
bm_mesh_clean_up(bm)
boundary_lines = []
original_geometry_data = dict()
rafter_edge_angle = pi / 2 - rafter_edge_angle # It's easier to work with this angle.
angled_edges = []
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
if angle_layer:
original_geometry_data["edges"] = [
(set(bm_get_indices(e.verts)), e[angle_layer], e[separate_verts_layer]) for e in bm.edges
angled_edges = [
(set([v.co.copy().freeze() for v in e.verts]), e[angle_layer]) for e in bm.edges if e[angle_layer]
]
else:
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None, None) for e in bm.edges]
original_geometry_data["verts"] = {v.index: v.co.copy() for v in bm.verts}
footprint_z = bm.verts[:][0].co.z
def calculate_hiped_roof():
def calculate_hipped_roof():
boundary_lines = []
for edge in bm.edges:
boundary_lines.append(shapely.LineString([v.co for v in edge.verts]))
@@ -188,202 +179,224 @@ def generate_hiped_roof_bmesh(
nonlocal height, angle
if mode == "HEIGHT":
height = height
angle = 0.0
tan_angle = 0.0
else:
angle = tan(angle)
tan_angle = tan(angle)
height = 0.0
# I think bpypolyskel upstream has a bug which can cause sloped ridges
# where it is not necessary to have sloped ridges specifically for
# dormers. This seems to work, but I'm not confident because apparently
# you need a PhD to understand this. See bug #5319.
if not hasattr(bpypolyskel._SLAV, "unpatched_handle_dormer_event"):
def patched_handle_dormer_event(self, event):
result = self.unpatched_handle_dormer_event(event)
if result[0]:
result[0][1] = bpypolyskel.Subtree(result[0][1].source, result[0][0].height, result[0][1].sinks)
return result
bpypolyskel._SLAV.unpatched_handle_dormer_event = bpypolyskel._SLAV.handle_dormer_event
bpypolyskel._SLAV.handle_dormer_event = patched_handle_dormer_event
faces = bpypolyskel.polygonize(
verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors
verts, start_exterior_index, total_exterior_verts, inner_loops, height, tan_angle, faces, unit_vectors
)
edges = []
return verts, edges, faces
verts, edges, faces = calculate_hiped_roof()
verts, edges, faces = calculate_hipped_roof()
bm.clear()
new_verts = [bm.verts.new(v) for v in verts]
new_edges = [bm.edges.new([new_verts[vi] for vi in edge]) for edge in edges]
new_faces = [bm.faces.new([new_verts[vi] for vi in face]) for face in faces]
def find_identical_new_vert(co: Vector) -> Union[bmesh.types.BMVert, None]:
for v in bm.verts:
if tool.Cad.is_x((co - v.co).length, 0):
return v
if mode == "HEIGHT": # Calculate the angle we ended up with.
new_faces[0].normal_update()
angle = new_faces[0].normal.angle(Vector((0, 0, 1)))
def find_other_polygon_verts(edge: bmesh.types.BMEdge) -> list[bmesh.types.BMVert]:
polygon = edge.link_faces[0]
return [v for v in polygon.verts if v not in edge.verts]
# bpypolyskel performs a straight skeleton with equal weights. Ideally, we
# need support for weighted straight skeletons (CGAL has this function) to
# calculate skeletons of varying angles. Doing anything else, such as
# creating new vertices, splitting edges, or sliding vertices is extremely
# error prone and only works in simple scenarios.
def change_angle(projected_vert_co: Vector, edge_verts: list[bmesh.types.BMVert], new_angle: float) -> Vector:
A, B = [v.co for v in edge_verts]
P = projected_vert_co
AP = P - A
AB = B - A
AB_dir = AB.normalized()
proj_length = AP.dot(AB_dir)
C = A + AB_dir * proj_length
Pp = P * Vector([1, 1, 0]) + Vector([0, 0, C.z])
Pp = P * Vector([1, 1, 0]) + Vector([0, 0, C.z])
angle_tan = tan(new_angle)
dist = (P.z - Pp.z) / angle_tan
PPnew = C + (Pp - C).normalized() * dist
Pnew = PPnew * Vector([1, 1, 0]) + Vector([0, 0, P.z])
return Pnew
footprint_edges = []
footprint_verts = set()
verts_to_change = {}
verts_to_rip = []
bottom_chords_to_remove = []
def is_footprint_vert(v: bmesh.types.BMVert) -> bool:
return tool.Cad.is_x(v.co.z - footprint_z, 0)
# In the meantime, we can support the simplest, but luckily also most
# common scenario of a gable roof, or hipped roof where the varied angle
# only occurs on a triangle face.
def is_footprint_edge(edge: bmesh.types.BMEdge) -> bool:
return all(is_footprint_vert(v) for v in edge.verts)
return all(tool.Cad.is_x(v.co.z - footprint_z, 0) for v in edge.verts)
# find footprint edges
footprint_edges = set()
gable_edges = set()
edges_to_delete = set()
face_angles = {}
# Post process skeleton to handle gables and custom angles.
for edge in bm.edges:
if is_footprint_edge(edge):
footprint_edges.append(edge)
footprint_verts.update(edge.verts)
old_verts_remap = {}
for old_vert in original_geometry_data["verts"]:
old_vert_co = original_geometry_data["verts"][old_vert]
old_verts_remap[old_vert] = find_identical_new_vert(old_vert_co)
# iterate over edges from original geometry
# if their angle was redefined by user - apply the changes to the related vertices
# to match the requested angle
process_later = []
for old_edge_verts, defined_angle, separate_verts in original_geometry_data["edges"]:
if not defined_angle:
if not is_footprint_edge(edge):
continue
edge_verts_remaped = set(old_verts_remap[old_vert] for old_vert in old_edge_verts)
identical_edge = None
for edge in footprint_edges:
if set(edge.verts) == edge_verts_remaped:
identical_edge = edge
break
assert identical_edge
if separate_verts:
verts_to_move = find_other_polygon_verts(identical_edge)
for v in verts_to_move:
vert_co = v.co
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_rip.append([v, new_vert_co, identical_edge])
else:
process_later.append([identical_edge, edge_verts_remaped, defined_angle])
if defined_angle >= pi / 2:
bottom_chords_to_remove.append(identical_edge)
def separate_vert(
bm: bmesh.types.BMesh, vert: bmesh.types.BMVert, edge: bmesh.types.BMEdge, new_co: Vector
) -> bmesh.types.BMVert:
face = next(f for f in vert.link_faces if edge in f.edges)
new_v = bmesh.utils.face_vert_separate(face, vert)
new_v.co = new_co
new_edge = bm.edges.new((vert, new_v))
for cur_edge in face.edges:
if cur_edge == edge:
continue
bmesh.ops.contextual_create(bm, geom=[new_edge, cur_edge])
return new_v
# correct angle for verts that require verts separation
# store separated verts for angle correction later
related_verts = {}
for v, new_co, edge in verts_to_rip:
new_v = separate_vert(bm, v, edge, new_co)
related_verts.setdefault(v, []).append(new_v)
# verts angle correction.
# we're taking into account new verts created after verts separation
# required for asymmetrical gable roof
for identical_edge, edge_verts_remaped, defined_angle in process_later:
verts_to_move = find_other_polygon_verts(identical_edge)
for v in verts_to_move[:]:
verts_to_move.extend(related_verts.get(v, []))
for v in verts_to_move:
vert_co = verts_to_change.get(v, v.co)
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
verts_to_change[v] = new_vert_co
# apply all changes once at the end
for v in verts_to_change:
v.co = verts_to_change[v]
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
# add roof thickness
extrusion_geom = bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"])
extruded_edges = extrusion_geom["edges"]
extruded_verts = extrusion_geom["verts"]
rafter_edge_angle = pi / 2 - rafter_edge_angle
default_offset_dir = Vector([0, 0, 1]) * roof_thickness
footprint_verts = set()
if not tool.Cad.is_x(rafter_edge_angle, 0):
footprint_edges = []
for edge in extruded_edges:
if is_footprint_edge(edge):
footprint_edges.append(edge)
footprint_verts.update(edge.verts)
def get_top_vert(v):
non_footprint_verts = []
for edge in v.link_edges:
v1 = edge.other_vert(v)
if not is_footprint_vert(v1):
non_footprint_verts.append(v1)
if len(non_footprint_verts) == 1:
return non_footprint_verts[0]
return min(non_footprint_verts, key=lambda v1: (v1.co - v.co).length)
top_verts = dict()
for v in footprint_verts:
top_verts[v] = get_top_vert(v)
# TODO: rafter_edge_angle might differ
# if footprint edges are not connected by 90 degrees
verts_offsets = dict()
for edge in footprint_edges:
v0, v1 = edge.verts
edge_dir = (v0.co - v1.co).normalized()
offset_dir = Matrix.Rotation(rafter_edge_angle, 4, edge_dir) @ default_offset_dir
for v in edge.verts:
previous_offset = verts_offsets.get(v, None)
if previous_offset:
previous_offset.xy += offset_dir.xy
footprint_edges.add(edge)
if len(edge.link_faces) != 1 or len(edge.link_faces[0].verts) != 3:
continue # Too complicated for us to figure out without a weighted skeleton
face_verts = edge.link_faces[0].verts
verts = set([v.co.copy().freeze() for v in edge.verts])
for angled_edge, edge_angle in angled_edges:
if angled_edge == verts:
face_angles[edge.link_faces[0]] = edge_angle
ridge_vert = [v for v in face_verts if v.co.copy().freeze() not in verts][0]
if len(ridge_vert.link_edges) == 3:
ridge_edge = [e for e in ridge_vert.link_edges if e not in edge.link_faces[0].edges][0]
other_ridge_vert = ridge_edge.other_vert(ridge_vert)
else:
verts_offsets[v] = offset_dir
# We cannot actually get this correct without a weighted
# skeleton, but for now let's assume we duplicate the ridge
# vert and slide the vertex towards the midpoint of the
# footprint edge.
edge_midpoint = edge.verts[0].co.lerp(edge.verts[1].co, 0.5)
edge_midpoint.z = ridge_vert.co.z
poke = bmesh.ops.poke(bm, faces=[edge.link_faces[0]])
other_ridge_vert = poke["verts"][0]
other_ridge_vert.co = ridge_vert.co + (edge_midpoint - ridge_vert.co).normalized() * 0.01
ridge_vert, other_ridge_vert = other_ridge_vert, ridge_vert
for v in verts_offsets:
vert_offset = verts_offsets[v]
top = top_verts[v].co
bot = v.co + default_offset_dir
base = v.co
offsetted = v.co + vert_offset
# all this math is needed to maintain the same roof thickness
# for different rafter edge angles
v.co = mathutils.geometry.intersect_line_line(bot, top, base, offsetted)[0]
face_center = (face_verts[0].co + face_verts[1].co + face_verts[2].co) / 3
x_axis = (edge.verts[1].co - edge.verts[0].co).normalized()
z_axis = Vector((0, 0, -1))
y_axis = z_axis.cross(x_axis)
positive = edge.verts[0].co + (y_axis * 0.01)
negative = edge.verts[0].co - (y_axis * 0.01)
if (face_center - positive).length < (face_center - negative).length:
y_axis = -y_axis
x_axis = y_axis.cross(z_axis) # Recalculate X axis to make rotation sign consistent
rotation_quaternion = Quaternion(x_axis, edge_angle)
plane_no = rotation_quaternion @ z_axis
if intersect := tool.Cad.intersect_edge_plane(
other_ridge_vert.co, ridge_vert.co, edge.verts[0].co, plane_no
):
edge_percent = tool.Cad.edge_percent(intersect, (other_ridge_vert.co, ridge_vert.co))
if edge_percent <= 0:
ridge_vert.co = other_ridge_vert.co
else:
ridge_vert.co = intersect
if tool.Cad.is_x(edge_angle, pi / 2, tolerance=radians(1)):
footprint_edges.remove(edge)
edges_to_delete.add(edge)
gable_edges.update(
[e for e in ridge_vert.link_edges if e.other_vert(ridge_vert) != other_ridge_vert]
)
break
bmesh.ops.delete(bm, geom=list(edges_to_delete), context="EDGES")
# Determine cutting planes for rafter_edge_angle
# The skeleton is always extruded in the -Z. A cutting plane is applied at
# the footprint or gable edge if a rafter_edge_angle is specified.
cutting_planes = {}
bm.faces.ensure_lookup_table()
if not tool.Cad.is_x(rafter_edge_angle, 0, tolerance=radians(1)):
for e in footprint_edges:
face = e.link_faces[0]
planes = []
verts = [v.co for v in face.verts]
face_center = sum(verts[1:], start=verts[0].copy()) / len(verts)
v1, v2 = [v.co.copy() for v in e.verts]
if not tool.Cad.is_x(v1.z, footprint_z):
v1, v2 = v2, v1
x_axis = (v2 - v1).normalized()
z_axis = Vector((0, 0, -1))
y_axis = z_axis.cross(x_axis)
positive = v1 + (y_axis * 0.01)
negative = v1 - (y_axis * 0.01)
plane_no = y_axis
if (face_center - positive).length < (face_center - negative).length:
plane_no = -y_axis
x_axis = plane_no.cross(z_axis) # Recalculate X axis to make rotation sign consistent
rotation_quaternion = Quaternion(x_axis, rafter_edge_angle)
plane_no = rotation_quaternion @ plane_no
planes.append((v1, plane_no))
cutting_planes[face] = planes
# Gable edges are special - they are not cut at the rafter_edge_angle.
# We copy the neighbouring cutting planes to make sure they mitre
# correctly.
for e in gable_edges:
face = e.link_faces[0]
planes = []
neighbouring_faces = set()
neighbouring_faces.update(e.verts[0].link_faces)
neighbouring_faces.update(e.verts[1].link_faces)
for f in neighbouring_faces:
planes.extend(cutting_planes.get(f, []))
cutting_planes[face] = planes
# Extrude skeleton using roof thickness
for top_face in [f for f in bm.faces]:
face = top_face.copy()
face.tag = True # Tags keep track of which geometry is part of our current skeleton fragment
if face.normal.z > 0:
face.normal_flip()
# This means that different fragments of the roof may not exactly join together.
# It's technically correct, but may look weird if the architect doesn't know what they're doing.
face_angle = face_angles.get(top_face, angle)
# Alternatively maybe we can offer a "distort" mode which doesn't preserve thickness at custom angles:
# face_angle = angle
extrusion_height = roof_thickness / cos(face_angle)
extrusion_vector = Vector((0, 0, -1)) * extrusion_height
extrusion = tool.Model.bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=[face])["geom"])
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extrusion["verts"])
for plane_co, plane_no in cutting_planes.get(top_face, []):
geom = set()
for f in bm.faces:
if f.tag:
geom.add(f)
for e in f.edges:
geom.add(e)
for v in f.verts:
geom.add(v)
result = bmesh.ops.bisect_plane(
bm, geom=list(geom), plane_co=plane_co, plane_no=plane_no, clear_outer=True, clear_inner=False
)
for g in result["geom"]:
if isinstance(g, bmesh.types.BMFace):
g.tag = True
bm.faces.ensure_lookup_table()
result = bmesh.ops.triangle_fill(
bm, use_dissolve=True, edges=[g for g in result["geom_cut"] if isinstance(g, bmesh.types.BMEdge)]
)
for g in result["geom"]:
if isinstance(g, bmesh.types.BMFace):
g.tag = True
verts = set()
[verts.update(f.verts) for f in bm.faces if f.tag]
bmesh.ops.remove_doubles(bm, verts=list(verts), dist=1e-4)
for f in bm.faces:
f.tag = False
verts = [v for v in extruded_verts if v not in footprint_verts]
bmesh.ops.translate(bm, vec=default_offset_dir, verts=verts)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
# Merge fragments and remove internal faces.
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4)
faces_to_delete = set()
for face in [f for f in bm.faces]:
is_internal = True
for e in face.edges:
if len(e.link_faces) < 3:
is_internal = False
if is_internal:
faces_to_delete.add(face)
bmesh.ops.delete(bm, geom=list(faces_to_delete), context="FACES")
return bm
@@ -414,9 +427,6 @@ def update_roof_modifier_ifc_data(context: bpy.types.Context) -> None:
if props.roof_type == "HIP/GABLE ROOF":
element.PredefinedType = "GABLE_ROOF" if roof_is_gabled() else "HIP_ROOF"
# occurrences attributes
# occurrences = tool.Ifc.get_all_element_occurrences(element)
tool.Model.add_body_representation(obj)
@@ -438,32 +448,27 @@ def update_roof_modifier_bmesh(obj: bpy.types.Object) -> None:
# NOTE: using Data since bmesh update will hapen very often
if not RoofData.is_loaded:
RoofData.load()
path_data = RoofData.data["pset_data"]["data_dict"]["path_data"]
path_data = RoofData.data["path_data"]
angle_layer_data = path_data.get("gable_roof_angles", None)
separate_verts_data = path_data.get("gable_roof_separate_verts", None)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# need to make sure we support edit mode
# since users will probably be in edit mode when they'll be changing roof path
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
angle_layer = bm.edges.layers.float.new("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.new("BBIM_gable_roof_separate_verts")
# generating roof path
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
new_edges = []
for i in range(len(path_data["edges"])):
e = path_data["edges"][i]
edge = bm.edges.new((new_verts[e[0]], new_verts[e[1]]))
edge[angle_layer] = angle_layer_data[i] if angle_layer_data else 0
edge[separate_verts_layer] = separate_verts_data[i] if separate_verts_data else 0
new_edges.append(edge)
if props.is_editing_path:
tool.Blender.apply_bmesh(obj.data, bm)
return
generate_hiped_roof_bmesh(
generate_hipped_roof_bmesh(
bm,
props.generation_method,
props.height,
@@ -488,15 +493,12 @@ def get_path_data(obj: bpy.types.Object) -> Union[dict[str, Any], None]:
bm_mesh_clean_up(bm)
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
path_data = dict()
path_data["edges"] = [bm_get_indices(e.verts) for e in bm.edges]
path_data["verts"] = [v.co / si_conversion for v in bm.verts]
if angle_layer:
path_data["gable_roof_angles"] = [e[angle_layer] for e in bm.edges]
if separate_verts_layer:
path_data["gable_roof_separate_verts"] = [e[separate_verts_layer] for e in bm.edges]
if not path_data["edges"] or not path_data["verts"]:
return None
@@ -693,7 +695,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
main_bm.edges.layers.int.new("BBIM_preview")
second_bm = generate_hiped_roof_bmesh(
second_bm = generate_hipped_roof_bmesh(
bm,
props.generation_method,
props.height,
@@ -702,7 +704,6 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
props.rafter_edge_angle,
mutate_current_bmesh=False,
)
bmesh.ops.translate(second_bm, verts=second_bm.verts, vec=Vector((0, 0, 1)))
tool.Blender.bmesh_join(main_bm, second_bm, callback=mark_preview_edges)
return main_bm
@@ -739,6 +740,41 @@ class CancelEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
return cancel_editing_roof_path(context)
class CopyRoofParameters(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.copy_roof_parameters"
bl_label = "Copy Roof Parameters from Active to Selected"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.active_object and len(context.selected_objects) > 1
def _execute(self, context):
source_obj = context.active_object
source_props = source_obj.BIMRoofProperties
data = source_props.get_general_kwargs(convert_to_project_units=True)
for target_obj in context.selected_objects:
if target_obj == source_obj:
continue
context.view_layer.objects.active = target_obj
RoofData.load()
if not "path_data" in RoofData.data:
continue
data["path_data"] = RoofData.data["path_data"]
target_element = tool.Ifc.get_entity(target_obj)
target_props = target_obj.BIMRoofProperties
target_props.set_props_kwargs_from_ifc_data(data)
update_bbim_roof_pset(target_element, data)
refresh()
update_roof_modifier_bmesh(target_obj)
update_roof_modifier_ifc_data(context)
context.view_layer.objects.active = source_obj
return {"FINISHED"}
class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_roof_path"
bl_label = "Finish Editing Roof Path"
@@ -792,7 +828,6 @@ class SetGableRoofEdgeAngle(bpy.types.Operator):
bl_label = "Set Gable Roof Edge Angle"
bl_options = {"REGISTER", "UNDO"}
angle: bpy.props.FloatProperty(name="Angle", default=90)
separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
@classmethod
def poll(cls, context):
@@ -815,17 +850,12 @@ class SetGableRoofEdgeAngle(bpy.types.Operator):
if "BBIM_gable_roof_angles" not in me.attributes:
me.attributes.new("BBIM_gable_roof_angles", type="FLOAT", domain="EDGE")
if "BBIM_gable_roof_separate_verts" not in me.attributes:
me.attributes.new("BBIM_gable_roof_separate_verts", type="INT", domain="EDGE")
angles_layer = bm.edges.layers.float["BBIM_gable_roof_angles"]
separate_verts_layer = bm.edges.layers.int["BBIM_gable_roof_separate_verts"]
for e in bm.edges:
if not e.select:
continue
e[angles_layer] = self.angle
e[separate_verts_layer] = self.separate_verts
tool.Blender.apply_bmesh(me, bm)
return {"FINISHED"}
@@ -188,6 +188,8 @@ class DumbSlabGenerator:
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer3"})
material = ifcopenshell.util.element.get_material(element)
material.LayerSetDirection = "AXIS3"
obj.select_set(True)
return obj
+53 -30
View File
@@ -84,10 +84,9 @@ class LaunchTypeManager(bpy.types.Operator):
bl_idname = "bim.launch_type_manager"
bl_label = "Launch Type Manager"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Display all available Construction Types to add new instances"
bl_description = "Browse, Edit and Manage Types"
def execute(self, context):
bpy.ops.bim.hotkey("INVOKE_DEFAULT", hotkey="S_A")
return {"FINISHED"}
def invoke(self, context, event):
@@ -101,40 +100,47 @@ class LaunchTypeManager(bpy.types.Operator):
# will be None if project has no types
if ifc_class is not None:
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
return context.window_manager.invoke_props_dialog(
self, width=550, title="Type Manager", confirm_text="Add Type"
)
return context.window_manager.invoke_props_dialog(self, width=550, title="Type Manager", confirm_text="Close")
def draw(self, context):
props = context.scene.BIMModelProperties
row = self.layout.row(align=True)
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type'] or 'Types'}"
if AuthoringData.data["total_types"] > 1:
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}s"
else:
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}"
text += "s"
row.label(text=text, icon="FILE_VOLUME")
# prop_with_search(row, props, "ifc_class", text="", should_click_ok_to_validate=True)
row.menu("BIM_MT_type_manager_menu", text="", icon="PREFERENCES")
row = self.layout.row(align=True)
row.operator("bim.launch_add_element", text=f"Create New {AuthoringData.data['ifc_element_type']}", icon="ADD")
op = row.operator(
"bim.add_element", text=f"Create New {AuthoringData.data['ifc_element_type'] or 'Type'}", icon="ADD"
)
op.is_specific_tool = bool(AuthoringData.data["ifc_element_type"])
op.ifc_product = "IfcElementType"
op.ifc_class = AuthoringData.data["ifc_element_type"] or props.ifc_class or ""
if AuthoringData.data["total_types"]:
row = self.layout.row(align=True)
row.prop(props, "search_name", icon="FILTER", text="")
columns = self.layout.column_flow(columns=3)
row = columns.row(align=True)
row.alignment = "CENTER"
### In case you want something here in the future
if AuthoringData.data["total_pages"] > 0:
row = columns.row(align=True)
row.alignment = "RIGHT"
row2 = row.row(align=True)
row2.label(text="Page")
row2.prop(props, "type_page", text="", emboss=False)
row2.label(text=f"/{AuthoringData.data['total_pages']} ")
###
prev_page_op = row.row(align=True)
op = prev_page_op.operator("bim.change_type_page", icon="TRIA_LEFT", text="")
op.page = AuthoringData.data["prev_page"] or 0
prev_page_op.enabled = op.page > 0
row = columns.row(align=True)
row.alignment = "RIGHT"
if AuthoringData.data["total_pages"] > 1:
row.label(text=f"Page {props.type_page}/{AuthoringData.data['total_pages']} ")
if AuthoringData.data["prev_page"]:
op = row.operator("bim.change_type_page", icon="TRIA_LEFT", text="")
op.page = AuthoringData.data["prev_page"]
if AuthoringData.data["next_page"]:
op = row.operator("bim.change_type_page", icon="TRIA_RIGHT", text="")
op.page = AuthoringData.data["next_page"]
next_page_op = row.row(align=True)
op = next_page_op.operator("bim.change_type_page", icon="TRIA_RIGHT", text="")
op.page = AuthoringData.data["next_page"] or 0
next_page_op.enabled = op.page > 0
flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
@@ -142,11 +148,11 @@ class LaunchTypeManager(bpy.types.Operator):
outer_col = flow.column()
box = outer_col.box()
row = box.row(align=True)
row = box.row()
op = row.operator("bim.set_active_type", text=relating_type["name"], icon="BLANK1", emboss=False)
op.relating_type = relating_type["id"]
op = row.operator("bim.launch_type_menu", icon="DOWNARROW_HLT", text="", emboss=False)
op = row.operator("bim.launch_type_menu", icon="PREFERENCES", text="", emboss=True)
op.relating_type_id = relating_type["id"]
row = box.row()
@@ -163,7 +169,15 @@ class LaunchTypeManager(bpy.types.Operator):
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
is_current_relating_type = str(relating_type["id"]) == str(
AuthoringData.data["relating_type_id_current"]
)
if is_current_relating_type:
active_row = row2.row()
active_row.alignment = "CENTER"
active_row.label(text="Active", icon="CHECKMARK")
else:
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
else:
row = box.row()
op = box.operator("bim.load_type_thumbnails", text="", icon="FILE_REFRESH", emboss=False)
@@ -302,6 +316,15 @@ class BIM_PT_stair(bpy.types.Panel):
self.layout.prop(props, prop_name, text="")
else:
self.layout.prop(props, prop_name)
if prop_name == "height": # Weak but we just want to insert this inside props drawing
row_length = self.layout.row(align=True)
row_length.prop(props, "total_length_target")
row_length.prop(
props,
"total_length_lock",
text="",
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
)
regenerate_stair_mesh(obj)
else:
calculated_params = StairData.data["calculated_params"]
@@ -548,7 +571,7 @@ class BIM_PT_door(bpy.types.Panel):
else:
row = self.layout.row()
row.label(text="No Door Found")
row.operator("bim.add_door", icon="ADD", text="").obj = ""
row.operator("bim.add_door", icon="ADD", text="")
class BIM_PT_railing(bpy.types.Panel):
@@ -596,6 +619,7 @@ class BIM_PT_railing(bpy.types.Panel):
else:
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
row.operator("bim.copy_railing_parameters", icon="COPYDOWN", text="")
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
# TODO: good for preview but probably should move to .is_editing == True
# since it's writing to ifc
@@ -648,13 +672,12 @@ class BIM_PT_roof(bpy.types.Panel):
self.layout.prop(props, prop)
update_roof_modifier_bmesh(obj)
elif props.is_editing_path:
row.operator("bim.finish_editing_roof_path", icon="CHECKMARK", text="")
row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="")
else:
row.operator("bim.enable_editing_roof", icon="GREASEPENCIL", text="")
row.operator("bim.copy_roof_parameters", icon="COPYDOWN", text="")
row.operator("bim.enable_editing_roof_path", icon="ANIM", text="")
row.operator("bim.remove_roof", icon="X", text="")
@@ -421,6 +421,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator):
self.handle_inserting_polyline(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event)
if cancel is not None:
ProductDecorator.uninstall()
@@ -616,6 +618,8 @@ class DumbWallGenerator:
polyline_points = extrusion.SweptArea.OuterCurve.Points.CoordList
polyline_points = [[(v * self.unit_scale) for v in p] for p in polyline_points]
polyline_points = [slab_obj.matrix_world @ Vector((p[0], p[1], elevation)) for p in polyline_points]
if not tool.Cad.is_counter_clockwise_order(polyline_points[0], polyline_points[1], polyline_points[2]):
polyline_points = polyline_points[::-1]
is_polyline_closed = True
walls = []
for i in range(len(polyline_points) - 1):
@@ -803,6 +807,8 @@ class DumbWallGenerator:
)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
material = ifcopenshell.util.element.get_material(element)
material.LayerSetDirection = "AXIS2"
obj.select_set(True)
return obj
+176 -177
View File
@@ -32,24 +32,10 @@ from bonsai.bim.module.model.prop import get_ifc_class
from typing import Optional, Union
# TODO duplicate code in cad/workspace and model/workspace
def check_display_mode():
global display_mode
try:
theme = bpy.context.preferences.themes["Default"]
text_color = theme.user_interface.wcol_menu_item.text
if sum(text_color) < 2.6:
display_mode = "lm"
else:
display_mode = "dm"
except:
display_mode = "dm"
def load_custom_icons():
global custom_icon_previews
global custom_icon_previews, display_mode
if display_mode is None:
check_display_mode()
display_mode = tool.Blender.detect_icon_color_mode("user_interface.wcol_tool.text")
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
custom_icon_previews = bpy.utils.previews.new()
@@ -118,10 +104,6 @@ class BimTool(WorkSpaceTool):
EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
else:
CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
# Show some UI for spatial elements that are unselectable by default.
if active_ifc_object:
EditObjectUI.layout = layout # Prevent .draw_modes from using old layout and crash.
EditObjectUI.draw_modes(context)
class WallTool(BimTool):
@@ -235,8 +217,18 @@ class CableTool(BimTool):
def add_layout_hotkey_operator(
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = ""
layout: bpy.types.UILayout,
text: str,
hotkey: str,
description: Union[str, None],
ui_context: str = "",
*,
operator: str = "bim.hotkey",
) -> bpy.types.OperatorProperties:
"""
:param operator: Operator to display in UI. Displaying the specific operator in UI can be useful
to provide poll error messages.
"""
parts = hotkey.split("_") if hotkey else []
modifier, key = (parts + ["", ""])[:2]
@@ -245,7 +237,7 @@ def add_layout_hotkey_operator(
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
row = layout.row(align=True)
op = row.operator("bim.hotkey", text=op_text, icon_value=custom_icon)
op = row.operator(operator, text=op_text, icon_value=custom_icon)
if ui_context != "TOOL_HEADER":
row.label(text="", icon=modifier_icon)
@@ -254,11 +246,12 @@ def add_layout_hotkey_operator(
hotkey_description = f"Hotkey: {modifier_str} {key}".strip()
description = "\n\n".join(filter(None, [description, hotkey_description]))
op.hotkey = hotkey
if ui_context == "TOOL_HEADER":
op.description = text + "\n" + description
else:
op.description = description
if operator == "bim.hotkey":
op.hotkey = hotkey
if ui_context == "TOOL_HEADER":
op.description = text + "\n" + description
else:
op.description = description
return op
@@ -306,30 +299,41 @@ class BIM_MT_add_representation_item(Menu):
ItemData.load()
if ItemData.data["representation_type"] in ("Tessellation", "Brep", "AdvancedBrep"):
self.layout.operator("bim.add_meshlike_item", icon="MESH_PLANE", text="Mesh Plane").shape = "PLANE"
self.layout.operator("bim.add_meshlike_item", icon="MESH_CUBE", text="Mesh Cube").shape = "CUBE"
self.layout.operator("bim.add_meshlike_item", icon="MESH_CIRCLE", text="Mesh Circle").shape = "CIRCLE"
self.layout.operator("bim.add_meshlike_item", icon="MESH_UVSPHERE", text="Mesh UV Sphere").shape = (
"UVSPHERE"
)
self.layout.operator("bim.add_meshlike_item", icon="MESH_ICOSPHERE", text="Mesh Icosphere").shape = (
"ICOSPHERE"
)
self.layout.operator("bim.add_meshlike_item", icon="MESH_CYLINDER", text="Mesh Cylinder").shape = "CYLINDER"
if ItemData.data["representation_type"] in ("SolidModel", "SweptSolid"):
self.layout.operator(
"bim.add_swept_area_solid_item", icon="MESH_CUBE", text="Extruded Area Solid Cube"
).shape = "CUBE"
self.layout.operator(
"bim.add_swept_area_solid_item", icon="MESH_CYLINDER", text="Extruded Area Solid Cylinder"
).shape = "CYLINDER"
if ItemData.data["representation_type"] in ("Annotation2D"):
self.draw_meshlike()
self.layout.separator()
self.layout.operator("bim.add_half_space_solid_item", icon="ORIENTATION_NORMAL", text="Half Space Solid")
elif ItemData.data["representation_type"] in ("SolidModel", "SweptSolid"):
self.draw_swept_area()
self.layout.separator()
self.layout.operator("bim.add_half_space_solid_item", icon="ORIENTATION_NORMAL", text="Half Space Solid")
elif ItemData.data["representation_type"] in ("CSG"):
# Surprisingly, once something becomes a CSG, you can combine lots of things due to IfcBooleanOperand
self.draw_swept_area()
self.layout.separator()
self.draw_meshlike()
self.layout.separator()
self.layout.operator("bim.add_half_space_solid_item", icon="ORIENTATION_NORMAL", text="Half Space Solid")
elif ItemData.data["representation_type"] in ("Annotation2D"):
self.layout.operator("bim.add_curvelike_item", icon="IPO_CONSTANT", text="Polycurve").shape = "LINE"
self.layout.operator("bim.add_curvelike_item", icon="MESH_CIRCLE", text="Circle").shape = "CIRCLE"
self.layout.operator("bim.add_curvelike_item", icon="MESH_CIRCLE", text="Ellipse").shape = "ELLIPSE"
def draw_meshlike(self):
self.layout.operator("bim.add_meshlike_item", icon="MESH_PLANE", text="Mesh Plane").shape = "PLANE"
self.layout.operator("bim.add_meshlike_item", icon="MESH_CUBE", text="Mesh Cube").shape = "CUBE"
self.layout.operator("bim.add_meshlike_item", icon="MESH_CIRCLE", text="Mesh Circle").shape = "CIRCLE"
self.layout.operator("bim.add_meshlike_item", icon="MESH_UVSPHERE", text="Mesh UV Sphere").shape = "UVSPHERE"
self.layout.operator("bim.add_meshlike_item", icon="MESH_ICOSPHERE", text="Mesh Icosphere").shape = "ICOSPHERE"
self.layout.operator("bim.add_meshlike_item", icon="MESH_CYLINDER", text="Mesh Cylinder").shape = "CYLINDER"
def draw_swept_area(self):
self.layout.operator(
"bim.add_swept_area_solid_item", icon="MESH_CUBE", text="Extruded Area Solid Cube"
).shape = "CUBE"
self.layout.operator(
"bim.add_swept_area_solid_item", icon="MESH_CYLINDER", text="Extruded Area Solid Cylinder"
).shape = "CYLINDER"
class CreateObjectUI:
layout: bpy.types.UILayout
@@ -392,7 +396,7 @@ class CreateObjectUI:
row1.operator(
"bim.launch_type_manager",
icon="ERROR",
text=f"No {AuthoringData.data['ifc_element_type']}s Found",
text=f"No {AuthoringData.data['ifc_element_type'] or 'Type'}s Found",
emboss=False,
)
row1.operator(
@@ -410,10 +414,12 @@ class CreateObjectUI:
row = box.row(align=True)
row.alignment = "CENTER"
row.operator(
"bim.launch_add_element",
text=f"Create New {AuthoringData.data['ifc_element_type']}",
op = row.operator(
"bim.add_element", text=f"Create New {AuthoringData.data['ifc_element_type'] or 'Type'}", icon="ADD"
)
op.is_specific_tool = bool(AuthoringData.data["ifc_element_type"])
op.ifc_product = "IfcElementType"
op.ifc_class = AuthoringData.data["ifc_element_type"] or props.ifc_class or ""
row = box.row(align=True)
@@ -440,8 +446,9 @@ class CreateObjectUI:
row = cls.layout.row(align=True)
if AuthoringData.data["relating_type_id"]:
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
op = row.operator("bim.hotkey", text="Add", icon_value=custom_icon_previews["ADD"].icon_id)
op.hotkey = "S_A"
if context.space_data.type == "VIEW_3D": # Wall polyline tool works only in 3D Space
op = row.operator("bim.hotkey", text="Add", icon_value=custom_icon_previews["ADD"].icon_id)
op.hotkey = "S_A"
else:
row.label(text="No Construction Type", icon="FILE_3D")
@@ -504,62 +511,58 @@ class CreateObjectUI:
row = cls.layout.row(align=True)
if not AuthoringData.data["ifc_element_type"]:
prop_with_search(row, cls.props, "ifc_class", text="Type Class" if ui_context != "TOOL_HEADER" else "")
if AuthoringData.data["ifc_classes"]:
ifc_class = AuthoringData.data["ifc_class_current"]
if ifc_class:
box = cls.layout.box()
row = box.row(align=True)
if AuthoringData.data["type_thumbnail"] and ui_context == "TOOL_HEADER":
row.template_icon(icon_value=AuthoringData.data["type_thumbnail"])
row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False)
else:
row.operator(
"bim.launch_type_manager",
icon="BLANK1",
text=AuthoringData.data["relating_type_name"],
emboss=False,
)
if not AuthoringData.data["ifc_classes"]:
return
if not (ifc_class := AuthoringData.data["ifc_class_current"]):
return
row.operator(
"bim.launch_type_manager",
icon=tool.Blender.TYPE_MANAGER_ICON,
text="",
emboss=False,
)
box = cls.layout.box()
if ui_context != "TOOL_HEADER":
row = box.row(align=True)
row.alignment = "CENTER"
row.operator(
"bim.launch_type_manager",
text=AuthoringData.data["relating_type_description"],
emboss=False,
)
row = box.row(align=True)
thumbnail: int = AuthoringData.data["type_thumbnail"]
row.template_icon(icon_value=thumbnail)
row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False)
row.operator(
"bim.launch_type_manager",
icon=tool.Blender.TYPE_MANAGER_ICON,
text="",
emboss=False,
)
if AuthoringData.data["type_thumbnail"]:
row1 = box.row()
row1.ui_units_y = 0.01
row1.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=4)
row2 = box.column(align=True)
row2.ui_units_y = 4
for _ in range(4):
row2.operator("bim.launch_type_manager", text="", emboss=False)
else:
op = box.operator(
"bim.load_type_thumbnails",
text="",
icon="FILE_REFRESH",
emboss=False,
)
op.ifc_class = ifc_class
if ui_context == "TOOL_HEADER":
return
row = box.row(align=True)
row.alignment = "CENTER"
row.operator(
"bim.launch_type_manager",
text=AuthoringData.data["relating_type_description"],
emboss=False,
)
row = box.row(align=True)
row.alignment = "CENTER"
row.operator(
"bim.launch_type_manager",
text=AuthoringData.data["predefined_type"],
emboss=False,
)
if thumbnail != 0:
row1 = box.row()
row1.ui_units_y = 0.01
row1.template_icon(icon_value=thumbnail, scale=4)
row2 = box.column(align=True)
row2.ui_units_y = 4
for _ in range(4):
row2.operator("bim.launch_type_manager", text="", emboss=False)
else:
op = box.operator(
"bim.load_type_thumbnails",
text="",
icon="FILE_REFRESH",
emboss=False,
)
op.ifc_class = ifc_class
row = box.row(align=True)
row.alignment = "CENTER"
row.operator(
"bim.launch_type_manager",
text=AuthoringData.data["predefined_type"],
emboss=False,
)
class EditObjectUI:
@@ -589,6 +592,17 @@ class EditObjectUI:
AuthoringData.load(ifc_element_type)
if context.region.type == "TOOL_HEADER":
if context.scene.BIMAggregateProperties.in_aggregate_mode:
layout.label(text=f"Aggregate Mode", icon="EMPTY_AXIS")
row = cls.layout.row(align=True)
op = row.operator("bim.disable_aggregate_mode", text="", icon="X")
op = row.operator("bim.toggle_aggregate_mode_local_view", text="", icon="ZOOM_SELECTED")
if context.scene.BIMNestProperties.in_nest_mode:
layout.label(text=f"Nest Mode", icon="EMPTY_AXIS")
row = cls.layout.row(align=True)
op = row.operator("bim.disable_nest_mode", text="", icon="X")
op = row.operator("bim.toggle_nest_mode_local_view", text="", icon="ZOOM_SELECTED")
text = format_ifc_camel_case(AuthoringData.data["active_class"])
layout.label(text=f"{text} Edit Tools:", icon="RESTRICT_SELECT_OFF")
cls.draw_parameter_adjustments(context)
@@ -698,10 +712,6 @@ class EditObjectUI:
)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Rotate 90", "S_R", "Rotate the selected Element by 90 degrees", ui_context)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(
row, "Flip", "S_F", "Flip Element about its local axes, keep the position", ui_context
)
elif AuthoringData.data["active_material_usage"] == "LAYER3":
if "LAYER2" in AuthoringData.data["selected_material_usages"]:
@@ -711,8 +721,6 @@ class EditObjectUI:
elif AuthoringData.data["active_material_usage"] == "PROFILE":
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Extend", "S_E", "", ui_context)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
if AuthoringData.data["active_class"] in (
@@ -758,6 +766,9 @@ class EditObjectUI:
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
if AuthoringData.data["is_flippable_element"]:
cls.draw_flip(ui_context, row)
if PortData.data["total_ports"] > 0:
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
row.operator(
@@ -774,24 +785,11 @@ class EditObjectUI:
def draw_regen_operations(cls, row):
custom_icon = custom_icon_previews.get("REGEN", custom_icon_previews["IFC"]).icon_id
if AuthoringData.data["active_material_usage"] == "LAYER2":
if AuthoringData.data["is_regenable_element"]:
op = row.operator("bim.hotkey", text="", icon_value=custom_icon)
description = f"{bpy.ops.bim.recalculate_wall.__doc__}\n\nHotkey: S G"
description = "Recalculate Element Geometry\nHotkey: S G"
op.hotkey = "S_G"
op.description = description.strip()
elif AuthoringData.data["active_material_usage"] == "PROFILE":
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
"IfcCableSegment",
"IfcDuctSegment",
"IfcPipeSegment",
):
pass
else:
op = row.operator("bim.hotkey", text="", icon_value=custom_icon)
description = f"{bpy.ops.bim.recalculate_profile.__doc__}\n\nHotkey: S G"
op.hotkey = "S_G"
op.description = description.strip()
if PortData.data["total_ports"] > 0:
op = row.operator("bim.hotkey", text="", icon_value=custom_icon)
@@ -809,8 +807,9 @@ class EditObjectUI:
op_icon = custom_icon_previews["APPLY_VOID"].icon_id
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
row.operator("bim.add_opening", text=op_text, icon_value=op_icon)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if ui_context != "TOOL_HEADER":
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
else:
op_text = "Add Void" if ui_context != "TOOL_HEADER" else ""
op_icon = custom_icon_previews["ADD_VOID"].icon_id
@@ -832,12 +831,8 @@ class EditObjectUI:
op_text = "" if IS_TOOL_HEADER else "Edit Openings"
row.operator("bim.edit_openings", icon="CHECKMARK", text=op_text)
row.operator("bim.hide_openings", icon="CANCEL", text="")
if len(context.selected_objects) == 2:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_L")
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
row.operator("bim.clone_opening", text="Clone Opening")
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Clone Opening", "S_L", "", ui_context, operator="bim.clone_opening")
@classmethod
def draw_align(cls, context):
@@ -905,6 +900,11 @@ class EditObjectUI:
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Decomposition", "A_D", "Select decomposition", ui_context)
@classmethod
def draw_flip(cls, ui_context, layout) -> None:
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else layout
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.hotkey"
@@ -960,47 +960,43 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "z")
def hotkey_S_A(self):
props = bpy.context.scene.BIMModelProperties
relating_type_id = AuthoringData.data["relating_type_id_current"]
if relating_type_id is None:
self.report({"ERROR"}, "No relating type selected")
return
props.relating_type_id = relating_type_id
if bpy.context.scene.BIMGeometryProperties.mode == "ITEM":
bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item")
else:
# Slab from walls
walls = False
for obj in bpy.context.selected_objects:
walls = tool.Ifc.get_entity(obj).is_a("IfcWall")
if (
walls
and relating_type_id
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
):
bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT")
return {"FINISHED"}
# Walls from slab
slab = tool.Ifc.get_entity(bpy.context.active_object)
if (
slab
and slab.is_a("IfcSlab")
and relating_type_id
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER2"
):
bpy.ops.bim.draw_walls_from_slab("INVOKE_DEFAULT")
return {"FINISHED"}
return
for obj in tool.Blender.get_selected_objects():
obj.select_set(False)
if relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER2":
bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
elif (
relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
):
bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
else:
bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")
props = bpy.context.scene.BIMModelProperties
relating_type_class = AuthoringData.data["ifc_class_current"]
if not (relating_type_id := tool.Blender.get_enum_safe(props, "relating_type_id")):
self.report({"ERROR"}, "No relating type selected")
return
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
has_only_walls_selected = tool.Blender.get_selected_objects(include_active=False) and all(
(e := tool.Ifc.get_entity(o)) and e.is_a("IfcWall")
for o in tool.Blender.get_selected_objects(include_active=False)
)
if tool.Model.get_usage_type(relating_type) == "LAYER3" and has_only_walls_selected:
return bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT")
elif (
(active_obj := tool.Blender.get_active_object(is_selected=True))
and (active_element := tool.Ifc.get_entity(active_obj))
and active_element.is_a("IfcSlab")
and tool.Model.get_usage_type(relating_type) == "LAYER2"
):
return bpy.ops.bim.draw_walls_from_slab("INVOKE_DEFAULT")
for obj in tool.Blender.get_selected_objects():
obj.select_set(False)
if tool.Model.get_usage_type(relating_type) == "LAYER2":
return bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
elif tool.Model.get_usage_type(relating_type) == "LAYER3":
return bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type_class != "IfcColumnType":
return bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT")
return bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")
def hotkey_S_Q(self):
if not bpy.context.selected_objects:
@@ -1028,7 +1024,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.align_product(align_type="CENTERLINE")
def hotkey_S_E(self):
if not bpy.context.selected_objects:
if not bpy.context.selected_objects or not (active_object := bpy.context.active_object):
return
# NOTE: placing it before the other operations because railing can also be SweptSolid
@@ -1048,7 +1044,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
elif tool.Model.is_parametric_window_active() or tool.Model.is_parametric_door_active():
return
selected_usages = {}
selected_usages: dict[str, list[bpy.types.Object]] = {}
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -1066,6 +1062,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
selected_usages.setdefault(usage, []).append(obj)
if len(bpy.context.selected_objects) == 1:
# Active object was probably unselected because it doesn't have a usage.
if bpy.context.selected_objects[0] != active_object:
return
if self.active_material_usage == "LAYER3":
# Edit LAYER3 profile
if bpy.context.active_object and bpy.context.active_object.mode == "OBJECT":
@@ -1258,9 +1258,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.enable_editing_extrusion_axis()
def hotkey_A_O(self):
opening_collection = bpy.data.collections.get("IfcOpeningElement")
if opening_collection and opening_collection.objects:
bpy.ops.bim.edit_openings()
if bpy.context.scene.BIMModelProperties.openings:
bpy.ops.bim.edit_openings(apply_all=True)
else:
bpy.ops.bim.show_openings()
@@ -26,14 +26,20 @@ classes = (
operator.BIM_OT_select_components,
operator.BIM_OT_select_nest,
operator.BIM_OT_nest_unassign_object,
operator.BIM_OT_disable_nest_mode,
operator.BIM_OT_toggle_nest_mode_local_view,
prop.BIMObjectNestProperties,
prop.Objects,
prop.BIMNestProperties,
ui.BIM_PT_nest,
)
def register():
bpy.types.Object.BIMObjectNestProperties = bpy.props.PointerProperty(type=prop.BIMObjectNestProperties)
bpy.types.Scene.BIMNestProperties = bpy.props.PointerProperty(type=prop.BIMNestProperties)
def unregister():
del bpy.types.Object.BIMObjectNestProperties
del bpy.types.Scene.BIMNestProperties
@@ -0,0 +1,336 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2025 Bruno Perdigão <contact@brunopo.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 blf
import bpy
import gpu
import ifcopenshell
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
from bonsai.bim.module.geometry.decorator import ItemDecorator
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
def create_bounding_box(objs):
# Initialize the bounding box coordinates
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
max_x, max_y, max_z = float("-inf"), float("-inf"), float("-inf")
# Iterate over the selected objects
for obj in objs:
if not obj.data:
continue
# Get the object's bounding box coordinates
bbox_world = [obj.matrix_world @ Vector(b) for b in obj.bound_box]
obj_min = []
obj_min.append(min([b[0] for b in bbox_world]))
obj_min.append(min([b[1] for b in bbox_world]))
obj_min.append(min([b[2] for b in bbox_world]))
obj_max = []
obj_max.append(max([b[0] for b in bbox_world]))
obj_max.append(max([b[1] for b in bbox_world]))
obj_max.append(max([b[2] for b in bbox_world]))
# Update the overall bounding box coordinates
min_x = min(min_x, min(obj_min[0], obj_max[0]))
min_y = min(min_y, min(obj_min[1], obj_max[1]))
min_z = min(min_z, min(obj_min[2], obj_max[2]))
max_x = max(max_x, max(obj_min[0], obj_max[0]))
max_y = max(max_y, max(obj_min[1], obj_max[1]))
max_z = max(max_z, max(obj_min[2], obj_max[2]))
indices = [
(min_x, min_y, min_z),
(max_x, min_y, min_z),
(max_x, max_y, min_z),
(min_x, max_y, min_z),
(min_x, min_y, max_z),
(max_x, min_y, max_z),
(max_x, max_y, max_z),
(min_x, max_y, max_z),
]
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)]
return indices, edges
class NestDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "u_ViewProjectionMatrix")
shader_info.push_constant("FLOAT", "u_Scale")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "FLOAT", "arcLength")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.push_constant("VEC4", "color")
shader_info.vertex_source(
"void main()"
"{"
" v_ArcLength = arcLength;"
" gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f);"
"}"
)
shader_info.fragment_source(
"void main()" "{" " if (step(sin(v_ArcLength * u_Scale), 0.4) == 1) discard;" " FragColor = color;" "}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
def draw_custom_batch(self, coords, color):
shader = self.dotted_line_shader()
arc_lengths = [0]
for a, b in zip(coords[:-1], coords[1:]):
arc_lengths.append(arc_lengths[-1] + (a - b).length)
batch = batch_for_shader(
shader,
"LINE_STRIP",
{"position": coords, "arcLength": arc_lengths},
)
matrix = bpy.context.region_data.perspective_matrix
shader.uniform_float("color", color)
shader.uniform_float("u_ViewProjectionMatrix", matrix)
shader.uniform_float("u_Scale", 25)
batch.draw(shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_nest(self, context):
if context.scene.BIMNestProperties.in_nest_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
decorator_color_error = self.addon_prefs.decorator_color_error
decorator_color_unselected = self.addon_prefs.decorator_color_unselected
decorator_color_background = self.addon_prefs.decorator_color_background
theme = context.preferences.themes.items()[0][1]
selected_object_color = (*theme.view_3d.object_active, 1)
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
nests = []
if not (selected_objects := context.selected_objects):
return
for obj in selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
return
components = ifcopenshell.util.element.get_components(element)
if components:
nests.append(obj)
continue
nest = ifcopenshell.util.element.get_nest(element)
if nest:
nests.append(tool.Ifc.get_object(nest))
nests = set(nests)
for nest in nests:
self.line_shader.uniform_float("lineWidth", 1.0)
color = decorator_color_unselected
if nest in selected_objects:
color = selected_object_color
size = nest.empty_display_size
location = nest.location
if nest.type == "EMPTY":
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
else:
self.draw_batch("POINTS", [location], color)
# if context.scene.BIMNestProperties.in_aggregate_mode:
# return
components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(nest))
components_objs = [tool.Ifc.get_object(p) for p in components]
components_objs.append(nest)
indices, edges = create_bounding_box(components_objs)
self.line_shader.uniform_float("lineWidth", 0.5)
self.draw_batch("LINES", indices, color, edges)
line = (Vector(indices[0]), location)
self.draw_custom_batch(line, decorator_color_unselected)
class NestModeDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest_name, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest_empty, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_nest_name(self, context):
if context.mode == "EDIT_MESH":
return
region = context.region
rv3d = region.data
props = context.scene.BIMNestProperties
aggregate_obj = props.editing_nest
if not aggregate_obj:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
font_size = tool.Blender.scale_font_size(12)
blf.size(self.font_id, font_size)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorator_color_selected
if aggregate_obj in context.selected_objects:
color = self.addon_prefs.decorator_color_selected
blf.color(self.font_id, *color)
text = aggregate_obj.name
text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, aggregate_obj.location)
text_length = blf.dimensions(self.font_id, text)
text_coords[0] -= text_length[0] / 2
text_coords[1] -= 20
blf.position(self.font_id, text_coords[0], text_coords[1], 0)
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
blf.draw(self.font_id, text)
def draw_nest_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = context.scene.BIMNestProperties
nest_obj = props.editing_nest
if not nest_obj:
return
location = nest_obj.location
self.addon_prefs = tool.Blender.get_addon_preferences()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
theme = context.preferences.themes.items()[0][1]
selected_object_color = (*theme.view_3d.object_active, 1)
color = self.addon_prefs.decorator_color_selected
if nest_obj in context.selected_objects:
color = selected_object_color
if nest_obj.type == "EMPTY":
size = nest_obj.empty_display_size
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
else:
self.draw_batch("POINTS", [location], color)
components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(nest_obj))
if components:
for component in components:
component_obj = tool.Ifc.get_object(component)
if component.is_a("IfcElementAssembly"):
self.line_shader.uniform_float("lineWidth", 1.0)
size = component_obj.empty_display_size
location = component_obj.location
color = self.addon_prefs.decorator_color_unselected
if component_obj in context.selected_objects:
color = selected_object_color
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
components = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(nest_obj))
color = self.addon_prefs.decorator_color_selected
components_objs = [tool.Ifc.get_object(p) for p in components]
components_objs.append(nest_obj)
indices, edges = create_bounding_box(components_objs)
self.line_shader.uniform_float("lineWidth", 0.5)
self.draw_batch("LINES", indices, color, edges)
@@ -140,3 +140,33 @@ class BIM_OT_select_nest(bpy.types.Operator):
nest_obj.select_set(True)
bpy.context.view_layer.objects.active = nest_obj
return {"FINISHED"}
class BIM_OT_disable_nest_mode(bpy.types.Operator):
bl_idname = "bim.disable_nest_mode"
bl_label = "Disable Nest Mode"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
core.disable_nest_mode(tool.Nest)
return {"FINISHED"}
class BIM_OT_toggle_nest_mode_local_view(bpy.types.Operator):
bl_idname = "bim.toggle_nest_mode_local_view"
bl_label = "Toggle Nest Mode Local View"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMNestProperties
objs = [o.obj for o in props.editing_objects]
if props.in_nest_mode:
if context.space_data.local_view:
bpy.ops.view3d.localview()
else:
for obj in objs:
obj.select_set(True)
bpy.ops.view3d.localview()
return {"FINISHED"}
+33
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.module.spatial.data import SpatialData
from bpy.types import PropertyGroup
@@ -30,6 +31,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from bonsai.bim.module.nest.decorator import NestDecorator, NestModeDecorator
def update_relating_object(self, context):
@@ -46,3 +48,34 @@ def update_relating_object(self, context):
class BIMObjectNestProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_object: PointerProperty(name="Nest Host", type=bpy.types.Object, update=update_relating_object)
def update_nest_decorator(self, context):
if self.nest_decorator:
NestDecorator.install(bpy.context)
else:
NestDecorator.uninstall()
def update_nest_mode_decorator(self, context):
if self.in_nest_mode:
NestModeDecorator.install(bpy.context)
else:
NestModeDecorator.uninstall()
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
class BIMNestProperties(PropertyGroup):
in_nest_mode: BoolProperty(name="In Edit Mode", update=update_nest_mode_decorator)
editing_nest: PointerProperty(name="Editing nest", type=bpy.types.Object)
editing_objects: CollectionProperty(type=Objects)
not_editing_objects: CollectionProperty(type=Objects)
nest_decorator: BoolProperty(
name="Display Nest",
default=False,
update=update_nest_decorator,
)
@@ -93,17 +93,11 @@ class ExecuteIfcPatch(bpy.types.Operator):
args = ifcpatch.ArgumentsDict(
recipe=props.ifc_patch_recipes,
arguments=arguments,
log=os.path.join(context.scene.BIMProperties.data_dir, "process.log"),
log=tool.Blender.get_data_dir_path("process.log").__str__(),
)
if props.should_load_from_memory and tool.Ifc.get():
args["file"] = tool.Ifc.get()
if ifcpatch.get_patch_input_argument_use(recipe_name) == "REQUIRED":
self.report(
{"ERROR"},
f"The recipe '{recipe_name}' is not currently supported if file is loaded from memory.",
)
return {"CANCELLED"}
else:
args["input"] = cast(str, props.ifc_patch_input)
args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input))
+2 -8
View File
@@ -69,11 +69,8 @@ class ProjectData:
if not ifc_file:
return []
current_schema = tool.Ifc.get().schema_identifier
files = (Path(bpy.context.scene.BIMProperties.data_dir) / "libraries").iterdir()
results = [("0", "Custom Library", "")]
for f in files:
if not f.suffix.lower().startswith(".ifc"):
continue
for f in tool.Blender.get_data_dir_paths("libraries", "*.ifc*"):
if cls.get_file_schema(f) != current_schema:
continue
results.append((f.name, f.stem, "Library"))
@@ -85,10 +82,7 @@ class ProjectData:
for export_schema in cls.data["export_schema"]:
template_files[export_schema[0]].append(("0", "Blank Project", ""))
files = (Path(bpy.context.scene.BIMProperties.data_dir) / "templates" / "projects").iterdir()
for f in files:
if not f.suffix.lower().startswith(".ifc"):
continue
for f in tool.Blender.get_data_dir_paths(Path("templates") / "projects", "*.ifc*"):
current_schema = cls.get_file_schema(f)
if current_schema not in template_files:
continue
@@ -856,8 +856,8 @@ class LoadProjectElements(bpy.types.Operator):
bonsai.bim.schema.reload(self.file.schema_identifier)
start = time.time()
logger = logging.getLogger("ImportIFC")
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
path_log = tool.Blender.get_data_dir_path("process.log")
if not os.access(path_log.parent, os.W_OK):
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
logging.basicConfig(
filename=path_log,
@@ -889,6 +889,7 @@ class LoadProjectElements(bpy.types.Operator):
tool.Project.set_default_context()
tool.Project.set_default_modeling_dimensions()
tool.Root.reload_grid_decorator()
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
def get_decomposition_elements(self):
@@ -1373,8 +1374,8 @@ class ExportIFC(bpy.types.Operator):
def _execute(self, context):
start = time.time()
logger = logging.getLogger("ExportIFC")
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
path_log = tool.Blender.get_data_dir_path("process.log")
if not os.access(path_log.parent, os.W_OK):
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
logging.basicConfig(
filename=path_log,
@@ -1419,9 +1420,6 @@ class ExportIFC(bpy.types.Operator):
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
)
if bpy.data.is_saved:
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
@classmethod
def description(cls, context, properties):
if properties.should_save_as:
+3 -3
View File
@@ -20,6 +20,7 @@ import os
import bpy
import ifcopenshell.util.element
import ifcopenshell.util.placement
import bonsai.tool as tool
from bonsai.bim.module.project.data import ProjectData
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, ObjProperty
@@ -60,9 +61,8 @@ def get_library_file(self: "BIMProjectProperties", context: bpy.types.Context) -
def update_library_file(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
if self.library_file != "0":
bpy.ops.bim.select_library_file(
filepath=os.path.join(bpy.context.scene.BIMProperties.data_dir, "libraries", self.library_file)
)
filepath = next(p for p in tool.Blender.get_data_dir_paths("libraries", "*.ifc") if p.name == self.library_file)
bpy.ops.bim.select_library_file(filepath=filepath)
def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
@@ -537,6 +537,7 @@ class BIM_PT_purge(Panel):
layout = self.layout
layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE"
layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE"
layout.operator("bim.purge_unused_openings", text="Purge Unused Openings in Selected Objects")
row = layout.row(align=True)
row.label(text="Materials: ")
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = "MATERIAL"
@@ -47,7 +47,6 @@ classes = (
ui.BIM_PT_object_psets,
ui.BIM_PT_object_qtos,
ui.BIM_PT_material_psets,
ui.BIM_PT_material_set_psets,
ui.BIM_PT_material_set_item_psets,
ui.BIM_PT_task_qtos,
ui.BIM_PT_resource_qtos,
@@ -66,7 +65,6 @@ classes = (
def register():
bpy.types.Object.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.MaterialPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
@@ -82,7 +80,6 @@ def register():
def unregister():
del bpy.types.Object.PsetProperties
del bpy.types.Scene.MaterialPsetProperties
del bpy.types.Object.MaterialSetPsetProperties
del bpy.types.Object.MaterialSetItemPsetProperties
del bpy.types.Scene.TaskPsetProperties
del bpy.types.Scene.ResourcePsetProperties
+1 -18
View File
@@ -33,7 +33,6 @@ def refresh():
ObjectPsetsData.is_loaded = False
ObjectQtosData.is_loaded = False
MaterialPsetsData.is_loaded = False
MaterialSetPsetsData.is_loaded = False
MaterialSetItemPsetsData.is_loaded = False
TaskQtosData.is_loaded = False
ResourceQtosData.is_loaded = False
@@ -185,7 +184,7 @@ class MaterialPsetsData(Data):
if material.ifc_definition_id:
material = tool.Ifc.get().by_id(material.ifc_definition_id)
category = getattr(material, "Category", None) or None
psets = bonsai.bim.schema.ifc.psetqto.get_applicable("IfcMaterial", category, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(props.material_type, category, pset_only=True)
psetnames = cls.format_pset_enum(psets)
assigned_names = ifcopenshell.util.element.get_psets(
material, psets_only=True, should_inherit=False
@@ -194,22 +193,6 @@ class MaterialPsetsData(Data):
return []
class MaterialSetPsetsData(Data):
data = {}
is_loaded = False
@classmethod
def load(cls):
psets = {}
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if material and "Set" in material.is_a():
psets = cls.psetqtos(material)
cls.data = {"psets": psets}
cls.is_loaded = True
class MaterialSetItemPsetsData(Data):
data = {}
is_loaded = False
+7 -48
View File
@@ -20,12 +20,11 @@ import bpy
import bonsai.tool as tool
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import prop_with_search
from bonsai.bim.helper import prop_with_search, get_display_value
from bonsai.bim.module.pset.data import (
ObjectPsetsData,
ObjectQtosData,
MaterialPsetsData,
MaterialSetPsetsData,
MaterialSetItemPsetsData,
TaskQtosData,
ResourceQtosData,
@@ -88,7 +87,7 @@ def draw_enumerated_property(
def get_active_pset_obj_name(context: bpy.types.Context, obj_type: tool.Ifc.OBJECT_TYPE) -> str:
if obj_type in ("Object", "MaterialSet", "MaterialSetItem"):
if obj_type in ("Object", "MaterialSetItem"):
return context.active_object.name
return ""
@@ -197,7 +196,9 @@ def draw_psetqto_ui(
row = box.row(align=True)
row.scale_y = 0.8
row.label(text=prop["Name"])
op = row.operator("bim.select_similar", text=nominal_value, icon="NONE", emboss=False)
op = row.operator(
"bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False
)
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
# calculate sum of all selected objects
if active_operator:
@@ -400,10 +401,8 @@ class BIM_PT_material_psets(Panel):
if not ifc_file or ifc_file.schema == "IFC2X3":
return False # We don't support material psets in IFC2X3 because they suck
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
return True
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
return True
return False
def draw(self, context):
@@ -430,46 +429,6 @@ class BIM_PT_material_psets(Panel):
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Material")
class BIM_PT_material_set_psets(Panel):
bl_label = "Material Set Property Sets"
bl_idname = "BIM_PT_material_set_psets"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_parent_id = "BIM_PT_object_material"
@classmethod
def poll(cls, context):
if not context.active_object:
return False
if not tool.Ifc.get() or tool.Ifc.get().schema == "IFC2X3":
return False # We don't support material psets in IFC2X3 because they suck
if not tool.Ifc.get_entity(context.active_object):
return False
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
ifc_class = ObjectMaterialData.data["material_class"]
return bool(ifc_class and "Set" in ifc_class)
def draw(self, context):
if not MaterialSetPsetsData.is_loaded:
MaterialSetPsetsData.load()
props = context.active_object.MaterialSetPsetProperties
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj = context.active_object.name
op.obj_type = "MaterialSet"
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
draw_psetqto_ui(context, 0, {}, props, self.layout, "MaterialSet")
for pset in MaterialSetPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "MaterialSet")
class BIM_PT_material_set_item_psets(Panel):
bl_label = "Material Set Item Property Sets"
bl_idname = "BIM_PT_material_set_item_psets"
@@ -109,19 +109,15 @@ class PsetTemplatesData:
return [(i, i, "") for i in enum_items]
@classmethod
def pset_template_files(cls):
results = []
pset_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "pset")
files = os.listdir(pset_dir)
for f in files:
results.append(
(os.path.join(pset_dir, f), os.path.splitext(os.path.basename(f))[0], "Global Pset Template")
)
def pset_template_files(cls) -> list[tuple[str, str, str]]:
results: list[tuple[str, str, str]] = []
for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"):
results.append((f.__str__(), f.stem, "Global Pset Template"))
pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)
if os.path.isdir(pset_dir):
for path in pathlib.Path(pset_dir).glob("*.ifc"):
results.append((str(path), os.path.splitext(os.path.basename(str(path)))[0], "Project Pset Template"))
results.append((str(path), path.stem, "Project Pset Template"))
return sorted(results, key=lambda x: x[1])
@@ -42,7 +42,9 @@ class AddPsetTemplateFile(bpy.types.Operator):
def execute(self, context):
template = ifcopenshell.file()
filepath = os.path.join(context.scene.BIMProperties.data_dir, "pset", self.new_template_filename + ".ifc")
filepath = next(
p for p in tool.Blender.get_data_dir_paths("pset", "*.ifc") if p.stem == self.new_template_filename
).__str__()
pset_template = ifcopenshell.api.run("pset_template.add_pset_template", template)
ifcopenshell.api.run("pset_template.add_prop_template", template, pset_template=pset_template)
+30 -13
View File
@@ -220,7 +220,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
if tool.Geometry.mesh_has_loose_geometry(obj.data):
self.report(
{"WARNING"},
f"Mesh '{obj.data.name}' has loose geometry, loose geometry was be ignored to save mesh to IFC as a tessellation.",
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
)
element = core.assign_class(
@@ -341,22 +341,30 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Add Element"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add an IFC physical product, construction type, and more"
is_specific_tool: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
ifc_product: bpy.props.StringProperty(options={"SKIP_SAVE"})
ifc_class: bpy.props.StringProperty(options={"SKIP_SAVE"})
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
def _invoke(self, context, event):
props = context.scene.BIMRootProperties
# For convenience, preselect OBJ representation template if applicable
if (obj := context.active_object) and obj.type == "MESH":
props = context.scene.BIMRootProperties
props.representation_template = "OBJ"
props.representation_obj = obj
# For convenience, preselect IFC class
if self.ifc_product:
props.ifc_product = self.ifc_product
if self.ifc_class:
props.ifc_class = self.ifc_class
return context.window_manager.invoke_props_dialog(self)
def _execute(self, context):
props = context.scene.BIMRootProperties
predefined_type = (
props.userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type
props.ifc_userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type
)
representation_template = props.representation_template
ifc_file = tool.Ifc.get()
@@ -380,6 +388,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new(props.ifc_class[3:], mesh)
obj.name = props.name or "Unnamed"
obj.location = bpy.context.scene.cursor.location
element = core.assign_class(
tool.Ifc,
@@ -390,6 +399,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
predefined_type=predefined_type,
should_add_representation=False,
)
element.Description = props.description or None
if representation_template == "EMTPY" or not ifc_context:
pass
@@ -532,34 +542,41 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
"material.assign_profile", tool.Ifc.get(), material_profile=material_profile, profile=profile
)
elif representation_template == "WINDOW":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_window()
elif representation_template == "DOOR":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_door()
elif representation_template == "STAIR":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_stair()
elif representation_template == "RAILING":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_railing()
elif representation_template == "ROOF":
with context.temp_override(active_object=obj):
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_roof()
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
tool.Blender.set_active_object(obj)
def draw(self, context):
props = context.scene.BIMRootProperties
self.layout.use_property_split = True
self.layout.use_property_decorate = False
prop_with_search(self.layout, props, "ifc_product", text="Definition")
prop_with_search(self.layout, props, "ifc_class", should_click_ok_to_validate=True)
row = self.layout.row()
row.prop(props, "name")
row = self.layout.row()
row.prop(props, "description")
if not self.is_specific_tool:
prop_with_search(self.layout, props, "ifc_product", text="Definition", should_click_ok=True)
prop_with_search(self.layout, props, "ifc_class", should_click_ok=True)
ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context)
if ifc_predefined_types:
prop_with_search(self.layout, props, "ifc_predefined_type")
prop_with_search(self.layout, props, "ifc_predefined_type", should_click_ok=True)
if props.ifc_predefined_type == "USERDEFINED":
row = self.layout.row()
row.prop(props, "ifc_userdefined_type")
prop_with_search(self.layout, props, "representation_template", text="Representation")
prop_with_search(self.layout, props, "representation_template", text="Representation", should_click_ok=True)
if props.representation_template == "OBJ":
row = self.layout.row()
row.prop(props, "representation_obj", text="Object")
@@ -567,7 +584,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
row = self.layout.row()
row.prop(props, "profile", text="Profile")
if props.representation_template != "EMPTY":
prop_with_search(self.layout, props, "contexts")
prop_with_search(self.layout, props, "contexts", should_click_ok=True)
class LaunchAddElement(bpy.types.Operator, tool.Ifc.Operator):
@@ -127,6 +127,8 @@ def poll_representation_obj(self, obj):
class BIMRootProperties(PropertyGroup):
contexts: EnumProperty(items=get_contexts, name="Contexts", options=set())
name: StringProperty(name="Name")
description: StringProperty(name="Description")
ifc_product: EnumProperty(items=get_ifc_products, name="Products", update=refresh_classes)
ifc_class: EnumProperty(items=get_ifc_classes, name="Class", update=refresh_predefined_types)
ifc_predefined_type: EnumProperty(items=get_ifc_predefined_types, name="Predefined Type", default=None)
@@ -38,8 +38,6 @@ classes = (
operator.AssignSuccessor,
operator.AssignWorkSchedule,
operator.Bonsai_DatePicker,
operator.Bonsai_DatePickerSetDate,
operator.Bonsai_RedrawDatePicker,
operator.CalculateTaskDuration,
operator.ClearPreviousAnimation,
operator.ContractAllTasks,
@@ -46,7 +46,7 @@ class SequenceData:
"has_work_calendars": cls.has_work_calendars(),
"schedule_predefined_types_enum": cls.schedule_predefined_types_enum(),
"task_columns_enum": cls.task_columns_enum(),
"tasktimecolumns_enum": cls.tasktimecolumns_enum(),
"task_time_columns_enum": cls.task_time_columns_enum(),
}
cls.load_work_plans()
cls.load_work_schedules()
@@ -278,7 +278,7 @@ class SequenceData:
return taskcolumns_enum
@classmethod
def tasktimecolumns_enum(cls) -> list[tuple[str, str, str]]:
def task_time_columns_enum(cls) -> list[tuple[str, str, str]]:
schema = tool.Ifc.schema()
tasktimecolumns_enum = []
for a in schema.declaration_by_name("IfcTaskTime").all_attributes():
@@ -38,6 +38,7 @@ def parse_duration(value):
def canonicalise_time(time: Union[datetime, None]) -> str:
"""Actualy canonicalises datetime as just a date, time is not included."""
if not time:
return "-"
return time.strftime("%d/%m/%y")
@@ -1178,44 +1178,72 @@ class Bonsai_DatePicker(bpy.types.Operator):
bl_label = "Date Picker"
bl_idname = "bim.datepicker"
bl_options = {"REGISTER", "UNDO"}
display_date: bpy.props.StringProperty(name="Display Date")
target_prop: bpy.props.StringProperty(name="Target date prop to set")
# TODO: base it on property type.
include_time: bpy.props.BoolProperty(name="Include Time", default=True)
if TYPE_CHECKING:
target_prop: str
include_time: bool
def execute(self, context):
selected_date = context.scene.DatePickerProperties.selected_date
try:
value = parser.parse(context.scene.DatePickerProperties.selected_date, dayfirst=True, fuzzy=True)
self.set_scene_prop(self.target_prop, helper.canonicalise_time(value))
except:
pass
return {"FINISHED"}
# Just to make sure the date is valid.
tool.Sequence.parse_isodate_datetime(selected_date, self.include_time)
self.set_scene_prop(self.target_prop, selected_date)
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Provided date is invalid: '{selected_date}'. Exception: {str(e)}.")
return {"CANCELLED"}
def draw(self, context):
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
current_month = (current_date.year, current_date.month)
props = context.scene.DatePickerProperties
display_date = tool.Sequence.parse_isodate_datetime(props.display_date, False)
current_month = (display_date.year, display_date.month)
lines = calendar.monthcalendar(*current_month)
month_title, week_titles = calendar.month(*current_month).splitlines()[:2]
layout = self.layout
row = layout.row()
row.prop(context.scene.DatePickerProperties, "selected_date")
row.prop(props, "selected_date", text="Date")
# Time.
if self.include_time:
row = layout.row()
row.label(text="Time:")
row.prop(props, "selected_hour", text="H")
row.prop(props, "selected_min", text="M")
row.prop(props, "selected_sec", text="S")
# Month.
month_delta = relativedelta.relativedelta(months=1)
split = layout.split()
col = split.row()
op = col.operator("bim.redraw_datepicker", icon="TRIA_LEFT", text="")
op.action = "previous"
op = col.operator("wm.context_set_string", icon="TRIA_LEFT", text="")
op.data_path = "scene.DatePickerProperties.display_date"
op.value = tool.Sequence.isodate_datetime(display_date - month_delta, False)
col = split.row()
col.label(text=month_title.strip())
col = split.row()
col.alignment = "RIGHT"
op = col.operator("bim.redraw_datepicker", icon="TRIA_RIGHT", text="")
op.action = "next"
op = col.operator("wm.context_set_string", icon="TRIA_RIGHT", text="")
op.data_path = "scene.DatePickerProperties.display_date"
op.value = tool.Sequence.isodate_datetime(display_date + month_delta, False)
# Day of week.
row = layout.row(align=True)
for title in week_titles.split():
col = row.column(align=True)
col.alignment = "CENTER"
col.label(text=title.strip())
# Days calendar.
current_selected_date = tool.Sequence.parse_isodate_datetime(props.selected_date, self.include_time)
current_selected_date = current_selected_date.replace(hour=0, minute=0, second=0)
for line in lines:
row = layout.row(align=True)
for i in line:
@@ -1223,15 +1251,33 @@ class Bonsai_DatePicker(bpy.types.Operator):
if i == 0:
col.label(text=" ")
else:
op = col.operator("bim.datepicker_setdate", text="{:2d}".format(i))
selected_date = "{}/{}/{}".format(i, current_date.month, current_date.year)
selected_date = parser.parse(selected_date, dayfirst=True, fuzzy=True)
op.selected_date = helper.canonicalise_time(selected_date)
selected_date = datetime(year=display_date.year, month=display_date.month, day=i)
is_current_date = current_selected_date == selected_date
op = col.operator("wm.context_set_string", text="{:2d}".format(i), depress=is_current_date)
if self.include_time:
selected_date = selected_date.replace(
hour=props.selected_hour, minute=props.selected_min, second=props.selected_sec
)
op.data_path = "scene.DatePickerProperties.selected_date"
op.value = tool.Sequence.isodate_datetime(selected_date, self.include_time)
def invoke(self, context, event):
self.display_date = self.get_scene_prop(self.target_prop) or helper.canonicalise_time(datetime.now())
context.scene.DatePickerProperties.display_date = self.display_date
context.scene.DatePickerProperties.selected_date = self.display_date
props = context.scene.DatePickerProperties
current_date_str = self.get_scene_prop(self.target_prop)
if current_date_str:
current_date = tool.Sequence.parse_isodate_datetime(current_date_str, self.include_time)
else:
current_date = datetime.now()
# Seconds of the moment when datepicker opened will probably only annoy users.
current_date = current_date.replace(second=0)
if self.include_time:
props["selected_hour"] = current_date.hour
props["selected_min"] = current_date.minute
props["selected_sec"] = current_date.second
props.display_date = tool.Sequence.isodate_datetime(current_date.replace(day=1), False)
props.selected_date = tool.Sequence.isodate_datetime(current_date, self.include_time)
return context.window_manager.invoke_props_dialog(self)
def get_scene_prop(self, prop_path: str) -> str:
@@ -1243,36 +1289,6 @@ class Bonsai_DatePicker(bpy.types.Operator):
tool.Blender.set_prop_from_path(scene, prop_path, value)
class Bonsai_DatePickerSetDate(bpy.types.Operator):
bl_label = "Set Date"
bl_idname = "bim.datepicker_setdate"
bl_options = {"REGISTER", "UNDO"}
selected_date: bpy.props.StringProperty()
def invoke(self, context, event):
context.scene.DatePickerProperties.selected_date = self.selected_date
return {"FINISHED"}
class Bonsai_RedrawDatePicker(bpy.types.Operator):
bl_label = "Redraw Datepicker Window"
bl_idname = "bim.redraw_datepicker"
bl_options = {"REGISTER", "UNDO"}
action: bpy.props.StringProperty()
def invoke(self, context, event):
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
if self.action == "previous":
date_to_set = current_date - relativedelta.relativedelta(months=1)
elif self.action == "next":
date_to_set = current_date + relativedelta.relativedelta(months=1)
context.scene.DatePickerProperties.display_date = helper.canonicalise_time(date_to_set)
return {"FINISHED"}
class RecalculateSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.recalculate_schedule"
bl_label = "Recalculate Schedule"
+17 -3
View File
@@ -45,13 +45,13 @@ from bpy.props import (
def getTaskColumns(self, context):
if not SequenceData.is_loaded:
SequenceData.load()
return SequenceData.data["taskcolumns_enum"]
return SequenceData.data["task_columns_enum"]
def getTaskTimeColumns(self, context):
if not SequenceData.is_loaded:
SequenceData.load()
return SequenceData.data["tasktimecolumns_enum"]
return SequenceData.data["task_time_columns_enum"]
def getWorkSchedules(self, context):
@@ -520,9 +520,23 @@ class BIMWorkCalendarProperties(PropertyGroup):
end_time: StringProperty(name="End Time")
def update_selected_date(self: "DatePickerProperties", context: bpy.types.Context) -> None:
# `include_time` is `True`, otherwise time props are not displayed in UI.
include_time = True
selected_date = tool.Sequence.parse_isodate_datetime(self.selected_date, include_time)
selected_date = selected_date.replace(hour=self.selected_hour, minute=self.selected_min, second=self.selected_sec)
self.selected_date = tool.Sequence.isodate_datetime(selected_date, include_time)
class DatePickerProperties(PropertyGroup):
display_date: StringProperty(name="Display Date")
display_date: StringProperty(
name="Display Date",
description="Needed to keep track of what month is currently opened in date picker without affecting the currently selected date.",
)
selected_date: StringProperty(name="Selected Date")
selected_hour: IntProperty(min=0, max=23, update=update_selected_date)
selected_min: IntProperty(min=0, max=59, update=update_selected_date)
selected_sec: IntProperty(min=0, max=59, update=update_selected_date)
class BIMDateTextProperties(PropertyGroup):
+74 -26
View File
@@ -16,7 +16,10 @@
# 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 ifcopenshell
import isodate
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
@@ -28,7 +31,7 @@ from bonsai.bim.module.sequence.data import (
TaskICOMData,
AnimationColorSchemeData,
)
from typing import Any
from typing import Any, Optional
class BIM_PT_status(Panel):
@@ -320,6 +323,7 @@ class BIM_PT_work_schedules(Panel):
row.operator("bim.contract_all_tasks", text="Contract All")
row = self.layout.row(align=True)
self.draw_task_operators()
BIM_UL_tasks.draw_header(self.layout)
self.layout.template_list(
"BIM_UL_tasks",
"",
@@ -814,10 +818,21 @@ class BIM_UL_product_output_tasks(UIList):
class BIM_UL_tasks(UIList):
@classmethod
def draw_header(cls, layout: bpy.types.UILayout):
props = bpy.context.scene.BIMWorkScheduleProperties
row = layout.row(align=True)
split1 = row.split(factor=0.1)
split1.label(text="ID", icon="BLANK1")
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(props.columns)))
split2.label(text="Name")
cls.draw_custom_columns(props, split2, header=True)
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
self.props = context.scene.BIMWorkScheduleProperties
task = IfcStore.get_file().by_id(item.ifc_definition_id)
task = SequenceData.data["tasks"][item.ifc_definition_id]
row = layout.row(align=True)
self.draw_hierarchy(row, item)
@@ -827,7 +842,7 @@ class BIM_UL_tasks(UIList):
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
split2.prop(item, "name", emboss=False, text="")
self.draw_custom_columns(split2, item, task)
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task)
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
row.prop(
@@ -860,7 +875,7 @@ class BIM_UL_tasks(UIList):
op = row.operator("bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False)
op.task = item.ifc_definition_id
def draw_order_operator(self, row, ifc_definition_id):
def draw_order_operator(self, row: bpy.types.UILayout, ifc_definition_id: int) -> None:
task = SequenceData.data["tasks"][ifc_definition_id]
if task["NestingIndex"] is not None:
if task["NestingIndex"] == 0:
@@ -872,7 +887,7 @@ class BIM_UL_tasks(UIList):
op.task = ifc_definition_id
op.new_index = task["NestingIndex"] - 1
def draw_hierarchy(self, row, item):
def draw_hierarchy(self, row: bpy.types.UILayout, item: bpy.types.PropertyGroup) -> None:
for i in range(0, item.level_index):
row.label(text="", icon="BLANK1")
if item.has_children:
@@ -887,37 +902,70 @@ class BIM_UL_tasks(UIList):
else:
row.label(text="", icon="DOT")
def draw_custom_columns(self, row, item, task):
for column in self.props.columns:
@classmethod
def draw_custom_columns(
cls,
props: bpy.types.PropertyGroup,
row: bpy.types.UILayout,
item: Optional[bpy.types.PropertyGroup] = None,
task: Optional[dict[str, Any]] = None,
*,
header: bool = False,
) -> None:
if not header:
assert item and task
for column in props.columns:
if column.name == "IfcTaskTime.ScheduleStart":
if item.derived_start:
row.label(text=item.derived_start + "*")
if header:
row.label(text="Start")
else:
row.prop(item, "start", emboss=False, text="")
if item.derived_start:
row.label(text=item.derived_start + "*")
else:
row.prop(item, "start", emboss=False, text="")
elif column.name == "IfcTaskTime.ScheduleFinish":
if item.derived_finish:
row.label(text=item.derived_finish + "*")
if header:
row.label(text="Finish")
else:
row.prop(item, "finish", emboss=False, text="")
if item.derived_finish:
row.label(text=item.derived_finish + "*")
else:
row.prop(item, "finish", emboss=False, text="")
elif column.name == "IfcTaskTime.ScheduleDuration":
if item.derived_duration:
row.label(text=item.derived_duration + "*")
if header:
row.label(text="Duration")
else:
row.prop(item, "duration", emboss=False, text="")
if item.derived_duration:
row.label(text=item.derived_duration + "*")
else:
row.prop(item, "duration", emboss=False, text="")
elif column.name == "Controls.Calendar":
if item.derived_calendar:
row.label(text=item.derived_calendar + "*")
if header:
row.label(text="Calendar")
else:
row.label(text=item.calendar or "-")
if item.derived_calendar:
row.label(text=item.derived_calendar + "*")
else:
row.label(text=item.calendar or "-")
else:
ifc_class, name = column.name.split(".")
if ifc_class == "IfcTask":
value = getattr(task, name)
elif ifc_class == "IfcTaskTime":
value = getattr(task.TaskTime, name) if task.TaskTime else None
if value is None:
value = "-"
row.label(text=str(value))
if header:
row.label(text=name)
else:
if ifc_class == "IfcTask":
value = task[name]
elif ifc_class == "IfcTaskTime":
if (task_time_id := task["TaskTime"]) is None:
value = None
else:
value = SequenceData.data["task_times"][task_time_id][name]
else:
assert False, f"Unexpected ifc_class '{ifc_class}'."
if value is None:
value = "-"
else:
row.label(text=str(value))
class BIM_PT_work_calendars(Panel):
@@ -76,9 +76,20 @@ class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_container"
bl_label = "Assign Container"
bl_description = "Assign the selected objects to the selected container"
bl_description = "\n".join(
(
"Assign the selected objects to the selected container.",
"This will move objects to the container collection in the outliner.",
"ALT + Click to ensure objects are only linked in the container collection",
)
)
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
remove_from_other_containers: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.remove_from_other_containers = event.alt
return self.execute(context)
def _execute(self, context):
props = context.active_object.BIMObjectSpatialProperties
@@ -87,6 +98,9 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
elif (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
pass
for element_obj in context.selected_objects:
if self.remove_from_other_containers:
for col in element_obj.users_collection[:]:
col.objects.unlink(element_obj)
core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj)
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator, workspace
classes = (
operator.ShowLoads,
operator.LoadStructuralAnalysisModels,
operator.DisableStructuralAnalysisModelEditingUI,
operator.AddStructuralAnalysisModel,
@@ -83,6 +84,7 @@ classes = (
ui.BIM_PT_connected_structural_members,
ui.BIM_UL_structural_analysis_models,
ui.BIM_UL_structural_activities,
ui.BIM_PT_show_structural_activities,
ui.BIM_PT_structural_load_cases,
ui.BIM_UL_structural_loads,
ui.BIM_PT_structural_loads,
@@ -30,6 +30,50 @@ def refresh():
StructuralLoadCasesData.is_loaded = False
StructuralLoadsData.is_loaded = False
BoundaryConditionsData.is_loaded = False
LoadGroupDecorationData.is_loaded = False
class LoadGroupDecorationData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"load groups to show": cls.load_groups_to_show()}
cls.is_loaded = True
@classmethod
def load_groups_to_show(cls):
ret = []
abrv = {
"LOAD_CASE": "L.Case: ",
"LOAD_COMBINATION": "L.Comb: ",
"LOAD_GROUP": "L.Gr: ",
"USERDEFINED": "U.Def: ",
"NOTDEFINED": "N.Def: ",
}
models = tool.Ifc.get().by_type("IfcStructuralAnalysisModel")
m = models[0]
props = bpy.context.scene.BIMStructuralProperties
if props.activity_type == "Action":
groups = m.LoadedBy or []
for g in groups:
ret.append((str(g.id()), ". " + abrv[g.PredefinedType] + " " + g.Name, ""))
related_objects = [rel.RelatedObjects for rel in g.IsGroupedBy]
for item in related_objects:
for subgoup in [sg for sg in item if sg.is_a("IfcStructuralLoadGroup")]:
ret.append((str(subgoup.id()), ". " + abrv[subgoup.PredefinedType] + subgoup.Name, ""))
if props.activity_type == "External Reaction":
groups = m.HasResults or []
for g in groups:
result_name = g.ResultForLoadGroup.Name or ""
group_name = g.Name or ""
ret.append((str(g.id()), group_name + " " + result_name, ""))
if len(ret) == 0:
ret.append(("", "", ""))
return ret
class StructuralBoundaryConditionsData:
@@ -0,0 +1,156 @@
# 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/>.
from mathutils import Vector
import numpy as np
import bpy
import gpu
import blf
from bpy.types import SpaceView3D
from gpu_extras.batch import batch_for_shader
from typing import Iterable, Union
from bonsai.bim.module.structural.load_decoration_data import ShaderInfo
class LoadsDecorator:
"""Decorator to show strucutural loads in 3D"""
is_installed = False
handlers = []
decoration_data = None
text_info = []
shader_info = []
depth_array = None
@classmethod
def install(cls, context: bpy.types.Context) -> None:
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_load_values, ((context,)), "WINDOW", "POST_PIXEL")
)
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (), "WINDOW", "POST_VIEW"))
cls.decoration_data = ShaderInfo()
cls.update()
cls.is_installed = True
@classmethod
def uninstall(cls) -> None:
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
@classmethod
def update(cls) -> None:
cls.decoration_data.update()
cls.text_info = cls.decoration_data.text_info
cls.shader_info = cls.decoration_data.info
def __call__(self) -> None:
"""set gpu configurations to draw 3D representations"""
# set open gl configurations
original_blend = gpu.state.blend_get()
original_depth_test = gpu.state.depth_test_get()
gpu.state.blend_set("ALPHA")
gpu.state.depth_test_set("LESS_EQUAL")
self.draw_batch()
# restore opengl configurations
gpu.state.blend_set(original_blend)
gpu.state.depth_test_set(original_depth_test)
def draw_batch(self) -> None:
"""draw the 3D representation of loads"""
if not self.decoration_data.is_empty:
for info in self.shader_info:
shader = info["shader"]
args = info["args"]
indices = info["indices"]
batch = batch_for_shader(shader, "TRIS", args, indices=indices)
matrix = bpy.context.region_data.perspective_matrix
shader.bind()
shader.uniform_float("viewProjectionMatrix", matrix)
for key, value in info["uniforms"]:
shader.uniform_float(key, value)
batch.draw(shader)
def draw_load_values(self, context: bpy.types.Context) -> None:
"""draw text representing the load values"""
# getting depth buffer info, code adapted from:
# https://blender.stackexchange.com/questions/177185/is-there-a-way-to-render-depth-buffer-into-a-texture-with-gpu-bgl-python-modules
framebuffer = gpu.state.active_framebuffer_get()
width = context.region.width
height = context.region.height
depth_buffer = framebuffer.read_depth(0, 0, width, height)
depth_array = np.array(depth_buffer.to_list())
f = context.area.spaces.active.clip_end
n = context.area.spaces.active.clip_start
self.depth_array = n / (f - (f - n) * depth_array) * (f - n)
for info in self.text_info:
text_position = self.location_3d_to_region_2d(info["position"], context)
if text_position is not None:
font_id = 0
blf.position(font_id, text_position[0], text_position[1], text_position[2])
blf.size(font_id, 20.0)
blf.color(font_id, 0.9, 0.9, 0.9, 1.0)
blf.draw(font_id, info["text"])
def location_3d_to_region_2d(self, coord: Iterable, context: bpy.types.Context) -> Union[Vector, None]:
"""Convert from 3D space to 2D screen space.
Filter out the text supposed to be hidden by 3D elements, using the depth array.
It also hides text that are distant from the camera view to avoid clutter"""
coord = Vector(coord)
rv3d = context.region_data
perspective = rv3d.view_perspective
view_matrix = rv3d.view_matrix
point_view_space = view_matrix @ coord
if perspective == "ORTHO" or -10 < point_view_space.z < 0:
prj = rv3d.perspective_matrix @ Vector((coord[0], coord[1], coord[2], 1.0))
width_half = context.region.width / 2.0
height_half = context.region.height / 2.0
coord_2d = Vector(
(
width_half + width_half * (prj.x / prj.w),
height_half + height_half * (prj.y / prj.w),
point_view_space.z,
)
)
if (
coord_2d[0] < 0
or coord_2d[0] > context.region.width
or coord_2d[1] > context.region.height
or coord_2d[1] < 0
):
return None
depth = self.depth_array[int(coord_2d[1])][int(coord_2d[0])]
if -0.98 * point_view_space.z > depth:
return None
return coord_2d
return None
@@ -0,0 +1,993 @@
# 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 bmesh
import numpy as np
from math import sin
from mathutils import Vector
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.attribute
import ifcopenshell.util.unit as ifcunit
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.structural.shader import DecorationShader
from typing import Literal, TypedDict, Iterable
MemberInfo = TypedDict(
"MemberInfo",
{"member": ifcopenshell.entity_instance, "activities": list[tuple[ifcopenshell.entity_instance, float]]},
)
LoadConfigItem = TypedDict(
"LoadConfigItem", {"pos": float, "descr": Literal["start", "end", "middle"], "load values": np.ndarray}
)
DiscreteConfigItem = TypedDict("DiscreteConfigItem", {"pos": float, "values": list[float]})
ParsedLoad = TypedDict(
"ParsedLoad",
{
"constant force": list[float],
"quadratic force": list[float],
"sinus force": list[float],
"linear load configuration": list[list[LoadConfigItem]],
"point load configuration": list[list[DiscreteConfigItem]],
},
)
LoadByDirection = TypedDict(
"LoadByDirection", {"constant": float, "quadratic": float, "sinus": float, "polyline": list[list[float]]}
)
ProcessedLoad = TypedDict(
"ProcessedLoad",
{"linear loads": LoadByDirection, "max linear load": float, "discrete loads": list[list[DiscreteConfigItem]]},
)
class ShaderInfo:
def __init__(self) -> None:
self.is_empty = True
self.shader = DecorationShader()
self.curve_members: dict[str, MemberInfo] = {}
self.point_members: dict[str, MemberInfo] = {}
self.surface_members: dict[str, MemberInfo] = {}
self.text_info = []
self.info = []
self.force_unit = ""
self.moment_unit = ""
self.linear_force_unit = ""
self.linear_moment_unit = ""
self.planar_force_unit = ""
def update(self) -> None:
self.info = []
self.text_info = []
self.curve_members = {}
self.point_members = {}
self.surface_members = {}
self.get_force_units()
self.get_strucutural_elements_and_activities()
self.get_linear_loads()
self.get_point_loads()
self.get_planar_loads()
if len(self.info):
self.is_empty = False
def get_force_units(self) -> None:
def get_unit_symbol(unit: ifcopenshell.entity_instance) -> str:
prefix_symbols = {
"EXA": "E",
"PETA": "P",
"TERA": "T",
"GIGA": "G",
"MEGA": "M",
"KILO": "k",
"HECTO": "h",
"DECA": "da",
"DECI": "d",
"CENTI": "c",
"MILLI": "m",
"MICRO": "μ",
"NANO": "n",
"PICO": "p",
"FEMTO": "f",
"ATTO": "a",
}
unit_symbols = {
# si units
"CUBIC_METRE": "m3",
"GRAM": "g",
"SECOND": "s",
"SQUARE_METRE": "m2",
"METRE": "m",
"NEWTON": "N",
"PASCAL": "Pa",
# conversion based units
"pound-force": "lbf",
"pound-force per square inch": "psi",
"thou": "th",
"inch": "in",
"foot": "ft",
"yard": "yd",
"mile": "mi",
"square thou": "th2",
"square inch": "in2",
"square foot": "ft2",
"square yard": "yd2",
"acre": "ac",
"square mile": "mi2",
"cubic thou": "th3",
"cubic inch": "in3",
"cubic foot": "ft3",
"cubic yard": "yd3",
"cubic mile": "mi3",
"litre": "L",
"fluid ounce UK": "fl oz",
"fluid ounce US": "fl oz",
"pint UK": "pt",
"pint US": "pt",
"gallon UK": "gal",
"gallon US": "gal",
"degree": "°",
"ounce": "oz",
"pound": "lb",
"ton UK": "ton",
"ton US": "ton",
"lbf": "lbf",
"kip": "kip",
"psi": "psi",
"ksi": "ksi",
"minute": "min",
"hour": "hr",
"day": "day",
"btu": "btu",
"fahrenheit": "°F",
}
symbol = ""
if unit.is_a("IfcSIUnit"):
symbol += prefix_symbols.get(unit.Prefix, "")
symbol += unit_symbols.get(unit.Name.replace("METER", "METRE"), "?")
return symbol
length_units = [u for u in tool.Ifc.get().by_type("IfcNamedUnit") if u.UnitType == "LENGTHUNIT"]
force_units = [u for u in tool.Ifc.get().by_type("IfcNamedUnit") if u.UnitType == "FORCEUNIT"]
linear_force_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "LINEARFORCEUNIT"]
linear_moment_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "LINEARMOMENTUNIT"]
planar_force_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "PLANARFORCEUNIT"]
conversion_force_unit = [u for u in force_units if u.is_a("IfcConversionBasedUnit")]
if len(conversion_force_unit) == 0:
conversion_force_unit.append(force_units[0])
self.force_unit = ifcunit.get_unit_symbol(conversion_force_unit[0])
conversion_length_unit = [u for u in length_units if u.is_a("IfcConversionBasedUnit")]
if len(conversion_length_unit) == 0:
conversion_length_unit.append(length_units[0])
length_unit = ifcunit.get_unit_symbol(conversion_length_unit[0])
self.moment_unit = self.force_unit + "." + length_unit
first = ""
second = ""
for e in linear_force_units[0].Elements:
if e.Unit.UnitType == "FORCEUNIT":
first = ifcunit.get_unit_symbol(e.Unit)
if e.Unit.UnitType == "LENGTHUNIT":
second = ifcunit.get_unit_symbol(e.Unit)
self.linear_force_unit = first + "/" + second
first = ""
second = ""
for e in linear_moment_units[0].Elements:
if e.Unit.UnitType == "FORCEUNIT":
first = ifcunit.get_unit_symbol(e.Unit)
if e.Unit.UnitType == "LENGTHUNIT":
second = ifcunit.get_unit_symbol(e.Unit)
self.linear_moment_unit = first + "." + second + "/" + second
first = ""
second = ""
for e in planar_force_units[0].Elements:
if e.Unit.UnitType == "FORCEUNIT":
first = ifcunit.get_unit_symbol(e.Unit)
if e.Unit.UnitType == "LENGTHUNIT":
second = ifcunit.get_unit_symbol(e.Unit) + "2"
if e.Unit.UnitType == "AREAUNIT":
second = ifcunit.get_unit_symbol(e.Unit)
self.planar_force_unit = first + "/" + second
def get_strucutural_elements_and_activities(self) -> None:
"""fills self.point_members, self.curve_members and self.surface_members dictionaries"""
def populate_members_dict(
dict_name: Literal["point_members", "curve_members", "surface_members"],
element: ifcopenshell.entity_instance,
activity: ifcopenshell.entity_instance,
factor: float,
) -> None:
"""
fills self.point_members, self.curve_members and self.surface_members dictionaries
thoses dicts will contain the strucutural member global id as key and a second dict as value
the second dict contais two keys, as follow:
{
"member": the strucutral member itself
"activities: list[(activity, factor)] for each activity applied to the member
}
dict_name: "point_members", "curve_members" or "surface_members"
element: IfcStructuralMember
activity: IfcStructuralActivity
factor: float to multiply the loads values in the structural activity
"""
dic = getattr(self, dict_name, None)
if dic is None:
return
member = dic.get(element.GlobalId)
if member is None:
dic.update({element.GlobalId: {"member": element, "activities": [(activity, factor)]}})
else:
member["activities"].append((activity, factor))
def recursive_subgroups(
groups: list[ifcopenshell.entity_instance],
rec_limit: int,
activity_type: Literal["Action", "External Reaction"],
factor: float = 1,
) -> None:
"""
Recursively fills self.point_members, self.curve_members and self.surface_members dictionaries
with the structural members to wich the activities in the load group and its subgroups are applied
it also creates a list with all the activities applied to that member that are in the same
load group or subgroup (see populate_members_dict description)
groups: list of Ifc load case, load group or load combination
rec_limit: maximum number of recursions
activity_type: "Action" or "External Reaction"
factor: the factor applied to the group, default = 1
this factor will be multiplied by the group coefficient and the factor of a
IfcRelAssignsToGroupByFactor relationship of subgroups in load combinations
"""
if rec_limit == 0 or len(groups) == 0:
return None
for group in groups:
subgorups = []
activities = []
relationship = [rel for rel in group.IsGroupedBy]
coef = getattr(group, "Coefficient", 1.0)
group_coef = coef if coef is not None else 1.0
rel_factor = 1.0
for rel in relationship:
if rel.is_a("IfcRelAssignsToGroupByFactor"):
rel_factor = rel.Factor if rel.Factor is not None else 1.0
objects = rel.RelatedObjects
subgorups = [sg for sg in objects if sg.is_a("IfcStructuralLoadGroup")]
activities = [a for a in objects if a.is_a("IfcStructuralActivity")]
factor = factor * group_coef * rel_factor
for activity in activities:
if len(activity.AssignedToStructuralItem):
element = activity.AssignedToStructuralItem[0].RelatingElement
if element is not None:
if activity_type == "Action":
if element.is_a("IfcStructuralCurveMember"):
populate_members_dict("curve_members", element, activity, factor)
elif element.is_a("IfcStructuralPointConnection"):
populate_members_dict("point_members", element, activity, factor)
elif element.is_a("IfcStructuralSurfaceMember"):
populate_members_dict("surface_members", element, activity, factor)
elif (
activity_type == "External Reaction"
and getattr(element, "AppliedCondition", None) is not None
):
if element.is_a("IfcStructuralCurveMember"):
populate_members_dict("curve_members", element, activity, factor)
elif element.is_a("IfcStructuralPointConnection"):
populate_members_dict("point_members", element, activity, factor)
elif element.is_a("IfcStructuralSurfaceMember"):
populate_members_dict("surface_members", element, activity, factor)
recursive_subgroups(subgorups, rec_limit - 1, activity_type, factor=factor)
props = bpy.context.scene.BIMStructuralProperties
group_definition_id = int(props.load_group_to_show)
file = IfcStore.get_file()
groups = [file.by_id(group_definition_id)]
recursive_subgroups(groups, 10, props.activity_type)
def get_planar_loads(self) -> None:
"""get the necessary information to render the planar load representation in 3D and its text information"""
list_of_surfaces = self.surface_members
shader = self.shader.new("PLANAR LOAD")
maximum = 0
for value in list_of_surfaces.values():
surf = value["member"]
activity_list = value["activities"]
if len(activity_list) == 0:
continue
rotation = self.get_surface_member_rotation(surf)
values = self.get_planar_loads_values(activity_list, rotation)
if maximum == 0:
maximum = max([abs(float(i)) for i in values])
if maximum == 0:
continue
props = bpy.context.scene.BIMStructuralProperties
reference_frame = props.reference_frame
orientation = np.eye(3)
if reference_frame == "LOCAL_COORDS":
orientation = rotation
blender_object: bpy.types.Object = IfcStore.get_element(getattr(surf, "GlobalId", None))
mat = blender_object.matrix_world
mesh: bpy.types.Mesh = blender_object.data
positions = []
indices = []
coord = []
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.edges.ensure_lookup_table()
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
for v in bm.verts:
p1 = np.array(mat @ v.co)
positions.append(p1)
p2 = p1 - (orientation @ values) * 0.2 / maximum
positions.append(p2)
coord.append((float(p1[0] + p1[1]), 0, 1))
coord.append((float(p1[0] + p1[1]), 1, 1))
for e in bm.edges:
if len(e.link_faces) > 1:
continue
indices.append((2 * e.verts[0].index, 2 * e.verts[0].index + 1, 2 * e.verts[1].index))
indices.append((2 * e.verts[0].index + 1, 2 * e.verts[1].index, 2 * e.verts[1].index + 1))
for p in bm.faces:
indices.append((2 * p.verts[0].index + 1, 2 * p.verts[1].index + 1, 2 * p.verts[2].index + 1))
bmesh.ops.dissolve_limit(bm, angle_limit=0.01, verts=bm.verts, edges=bm.edges)
bm.faces.ensure_lookup_table()
center = bm.faces[0].calc_center_bounds()
self.text_info.append(
{
"position": mat @ center - Vector((orientation @ values) * 0.2 / maximum),
"text": f"{values[2]:.5f} {self.planar_force_unit}",
}
)
self.info.append(
{
"shader": shader,
"args": {"position": positions, "coord": coord},
"indices": indices,
"uniforms": [["color", (0.2, 0, 1, 1)], ["spacing", 0.2]],
}
)
def get_planar_loads_values(
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
) -> np.ndarray:
"""
returns a numpy array with the sum of the values of structural activities
applied loads in each direction, multiplied by the factors in load combinations
"""
values = np.zeros((3))
for item in activity_list:
activity = item[0]
factor = item[1]
load = activity.AppliedLoad
temp = np.zeros((3))
if load is not None and load.is_a("IfcStructuralLoadPlanarForce"):
temp[0] = load.PlanarForceX if load.PlanarForceX is not None else 0
temp[1] = load.PlanarForceY if load.PlanarForceY is not None else 0
temp[2] = load.PlanarForceZ if load.PlanarForceZ is not None else 0
temp = temp * factor
transform = self.get_activity_transform_matrix(activity, element_rotation_matrix)
values += transform @ temp
return values
def get_surface_member_rotation(self, surface_member: ifcopenshell.entity_instance) -> np.ndarray:
"""returns the rotation matrix of a structural surface member"""
representation = ifcopenshell.util.representation.get_representation(surface_member, "Model")
repr_item = representation.Items[0]
placement = ifcopenshell.util.placement.get_axis2placement(repr_item.FaceSurface.Position)
rotation = placement[0:3, 0:3]
return rotation
def get_point_connection_rotation(self, point_connection: ifcopenshell.entity_instance) -> np.ndarray:
"""returns the rotation matrix of a structural point connection"""
if point_connection.ConditionCoordinateSystem is not None:
placement = ifcopenshell.util.placement.get_axis2placement(point_connection.ConditionCoordinateSystem)
else:
placement = np.eye(4)
rotation = placement[0:3, 0:3]
return rotation
def get_curve_member_rotation(self, curve_member: ifcopenshell.entity_instance) -> np.ndarray:
"""returns the rotation matrix of a structural surface member"""
z = curve_member.Axis.DirectionRatios
edge = curve_member.Representation.Representations[0].Items[0]
origin = edge.EdgeStart.VertexGeometry.Coordinates
end = edge.EdgeEnd.VertexGeometry.Coordinates
x = [c2 - c1 for c1, c2 in zip(origin, end)]
placement = ifcopenshell.util.placement.a2p(origin, z, x)
rotation = placement[0:3, 0:3]
return rotation
def get_activity_transform_matrix(
self, activity: ifcopenshell.entity_instance, element_rotation_matrix: np.ndarray
) -> np.ndarray:
"provides the transformation matrix to convert between reference frames"
global_or_local = activity.GlobalOrLocal
props = bpy.context.scene.BIMStructuralProperties
reference_frame = props.reference_frame
transform_matrix = np.eye(3)
if reference_frame == "LOCAL_COORDS" and global_or_local != reference_frame:
transform_matrix = np.linalg.inv(element_rotation_matrix)
elif reference_frame == "GLOBAL_COORDS" and global_or_local != reference_frame:
transform_matrix = element_rotation_matrix
return transform_matrix
def get_point_loads(self) -> None:
"""get the necessary information to render the point load representation in 3D and its text information"""
list_of_point_connections = self.point_members
for value in list_of_point_connections.values():
conn = value["member"]
activity_list = value["activities"]
if len(activity_list) == 0:
continue
blender_object = IfcStore.get_element(getattr(conn, "GlobalId", None))
if blender_object.type == "MESH":
conn_location = blender_object.matrix_world @ blender_object.data.vertices[0].co
rotation = self.get_point_connection_rotation(conn)
loads = self.get_point_loads_values(activity_list, rotation)
self.get_point_shader_args(loads, conn_location, rotation)
def get_point_shader_args(self, loads: Iterable, location: np.ndarray, rotation: np.ndarray) -> None:
"""get the args to the point shader"""
location = np.array(location)
indices = []
direction_dict = {
"fx": (np.array((1, 0, 0)), np.array((0, 1, 0)), np.array((0, 0, 1))),
"fy": (np.array((0, 1, 0)), np.array((1, 0, 0)), np.array((0, 0, 1))),
"fz": (np.array((0, 0, 1)), np.array((0, 1, 0)), np.array((1, 0, 0))),
"mx": (np.array((0, 1, 0)), np.array((0, 0, 1))),
"my": (np.array((1, 0, 0)), np.array((0, 0, 1))),
"mz": (np.array((1, 0, 0)), np.array((0, 1, 0))),
}
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
props = bpy.context.scene.BIMStructuralProperties
reference_frame = props.reference_frame
if reference_frame == "LOCAL_COORDS":
for key in keys:
tup = direction_dict[key]
li = []
for item in tup:
li.append(rotation @ item)
direction_dict[key] = li
for i, key in enumerate(keys):
if loads[i] == 0:
continue
color = (1, 0, 0, 1)
if i in [1, 4]:
color = (0, 1, 0, 1)
elif i in [2, 5]:
color = (0, 0, 1, 1)
d1 = -(direction_dict[key][0] * loads[i])
d1 = d1 / np.linalg.norm(d1)
if i < 3:
d2 = direction_dict[key][1]
d3 = direction_dict[key][2]
p1 = location
p2 = location + d1 + d2
p3 = location + d1 - d2
p4 = location + d1 + d3
p5 = location + d1 - d3
position = [p1, p2, p3, p4, p5]
indices = [(0, 1, 2), (0, 3, 4)]
c1 = (0, 0, 0)
c2 = (1, 1, 0)
c3 = (-1, 1, 0)
coords_for_shader = [c1, c2, c3, c2, c3]
shader = self.shader.new("SINGLE FORCE")
self.info.append(
{
"shader": shader,
"args": {"position": position, "coord": coords_for_shader},
"indices": indices,
"uniforms": [["color", color], ["spacing", 0.2]],
}
)
self.text_info.append({"position": location + d1, "text": f"{loads[i]:.2f} {self.force_unit}"})
else:
d2 = d2 = direction_dict[key][1]
p1 = location - d2
p2 = location + d1 + d2
p3 = location - d1 + d2
position = [p1, p2, p3]
indices = [(0, 1, 2)]
c1 = (-1, 0, 0)
c2 = (1, 1, 0)
c3 = (1, -1, 0)
coords_for_shader = [c1, c2, c3]
shader = self.shader.new("SINGLE MOMENT")
self.info.append(
{
"shader": shader,
"args": {"position": position, "coord": coords_for_shader},
"indices": indices,
"uniforms": [["color", color]],
}
)
self.text_info.append(
{"position": location + 0.25 * (d1 + d2), "text": f"{loads[i]:.2f} {self.moment_unit}"}
)
def get_point_loads_values(
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
) -> np.ndarray:
"""returns a numpy array with the sum of the point load values"""
result_list = np.zeros(6)
attr_list = ["ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ"]
for item in activity_list:
activity = item[0]
factor = item[1]
load = activity.AppliedLoad
temp = np.zeros(6)
for i, attr in enumerate(attr_list):
value = 0 if getattr(load, attr, 0) is None else getattr(load, attr, 0)
temp[i] += value * factor
transform_3 = self.get_activity_transform_matrix(activity, element_rotation_matrix)
transform_6 = np.zeros((6, 6))
transform_6[0:3, 0:3] = transform_3
transform_6[3:6, 3:6] = transform_3
result_list += transform_6 @ temp
return result_list
def get_linear_loads(self) -> None:
position = []
indices = []
sin_quad_lin = []
coords_for_shader = []
color = []
info = []
maxforce = 0
list_of_curve_members = self.curve_members
for value in list_of_curve_members.values():
member = value["member"]
activity_list = value["activities"]
if len(activity_list) == 0:
continue
blender_object = IfcStore.get_element(getattr(member, "GlobalId", None))
start_co = blender_object.matrix_world @ blender_object.data.vertices[0].co
end_co = blender_object.matrix_world @ blender_object.data.vertices[1].co
x_axis = Vector(end_co - start_co).normalized()
z_direction = getattr(member, "Axis")
# local coordinates
z_axis = Vector(getattr(z_direction, "DirectionRatios", None)).normalized()
y_axis = z_axis.cross(x_axis).normalized()
z_axis = x_axis.cross(y_axis).normalized()
rotation = self.get_curve_member_rotation(member)
props = bpy.context.scene.BIMStructuralProperties
reference_frame = props.reference_frame
is_local = reference_frame == "LOCAL_COORDS"
x_match = abs(Vector((1, 0, 0)).dot(x_axis)) > 0.99
y_match = abs(Vector((0, 1, 0)).dot(x_axis)) > 0.99
z_match = abs(Vector((0, 0, 1)).dot(x_axis)) > 0.99
direction_dict = {
"fx": y_axis + z_axis if is_local else Vector((1, 0, 0)) if not x_match else Vector((0, 1, 1)),
"fy": y_axis if is_local else Vector((0, 1, 0)) if not y_match else Vector((1, 0, 1)),
"fz": z_axis if is_local else Vector((0, 0, 1)) if not z_match else Vector((1, 1, 0)),
"mx": z_axis - y_axis if is_local or x_match else Vector((1, 0, 0)).cross(x_axis),
"my": (
z_axis
if is_local
else Vector((-1, 0, 1)) if y_match else Vector((0, 1, 0)).cross(x_axis).normalized()
),
"mz": (
y_axis
if is_local
else Vector((-1, 1, 0)) if z_match else Vector((0, 0, 1)).cross(x_axis).normalized()
),
}
match_dict = {"fx": x_match or is_local, "fy": y_match, "fz": z_match}
member_length = Vector(end_co - start_co).length
processed_loads = self.process_total_linear_loads(activity_list, rotation, member_length)
linear_loads = processed_loads["linear loads"]
maxforce = max(maxforce, processed_loads["max linear load"])
point_loads = processed_loads["discrete loads"]
if len(point_loads) > 0:
for item in point_loads:
for sub_item in item:
pos = sub_item["pos"]
values = sub_item["values"]
pos_vector = start_co + x_axis * pos
self.get_point_shader_args(values, pos_vector, rotation)
if linear_loads is None:
continue
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
for key in keys:
polyline = linear_loads[key]["polyline"]
sinus = linear_loads[key]["sinus"]
quadratic = linear_loads[key]["quadratic"]
constant = linear_loads[key]["constant"]
direction = direction_dict[key]
color_axis = (0, 0, 1, 1)
if "x" in key:
color_axis = (1, 0, 0, 1)
if "y" in key:
color_axis = (0, 1, 0, 1)
if "f" in key:
unit = self.linear_force_unit
if match_dict[key]:
shader = self.shader.new("PARALLEL DISTRIBUTED FORCE")
else:
shader = self.shader.new("PERPENDICULAR DISTRIBUTED FORCE")
else:
unit = self.linear_moment_unit
shader = self.shader.new("DISTRIBUTED MOMENT")
counter = 0
for i in range(len(polyline) - 1):
current = Vector(polyline[i] + [0])
nextitem = Vector(polyline[i + 1] + [0])
if any([current.y, nextitem.y, constant, quadratic, sinus]):
negative = -1 * direction + start_co + x_axis * current.x
positive = direction + start_co + x_axis * current.x
position.append(negative)
coords_for_shader.append((current.x, 1.0, member_length))
sin_quad_lin.append((sinus, quadratic, current.y + constant))
color.append(color_axis)
x = current.x / member_length
func = sin(x * 3.1416) * sinus + (-4.0 * x * x + 4.0 * x) * quadratic + constant + current.y
if func:
self.text_info.append(
{
"position": -1 * direction * func / maxforce + start_co + x_axis * current.x,
"text": f"{func:.2f} {unit}",
}
)
position.append(positive)
coords_for_shader.append((current[0], -1.0, member_length))
sin_quad_lin.append((sinus, quadratic, current.y + constant))
color.append(color_axis)
indices.append((0 + counter, 1 + counter, 2 + counter))
indices.append((3 + counter, 2 + counter, 1 + counter))
if i == len(polyline) - 2:
negative = -1 * direction + start_co + x_axis * nextitem.x
positive = direction + start_co + x_axis * nextitem.x
position.append(negative)
coords_for_shader.append((nextitem.x, 1.0, member_length))
sin_quad_lin.append((sinus, quadratic, nextitem.y + constant))
color.append(color_axis)
x = nextitem.x / member_length
func = (
sin(x * 3.1416) * sinus + (-4.0 * x * x + 4.0 * x) * quadratic + constant + nextitem.y
)
if func:
self.text_info.append(
{
"position": -1 * direction * func / maxforce + start_co + x_axis * nextitem.x,
"text": f"{func:.2f} {unit}",
}
)
position.append(positive)
coords_for_shader.append((nextitem.x, -1.0, member_length))
sin_quad_lin.append((sinus, quadratic, nextitem.y + constant))
color.append(color_axis)
counter += 2
if position:
self.info.append(
{
"shader": shader,
"args": {
"position": position,
"sin_quad_lin_forces": sin_quad_lin,
"coord": coords_for_shader,
},
"indices": indices,
"uniforms": [["color", color_axis], ["spacing", 0.2], ["maxload", maxforce]],
}
)
position = []
sin_quad_lin = []
coords_for_shader = []
indices = []
for info in self.info:
info["uniforms"][2][1] = maxforce
def process_total_linear_loads(
self,
activity_list: list[tuple[ifcopenshell.entity_instance, float]],
element_rotation_matrix: np.ndarray,
member_length: float,
) -> ProcessedLoad:
"""returns a dict with total values for applied loads in each direction
along with the maximum value for the loads in the member and the discrete loads applied
"""
loads_dict = self.parse_linear_loads_to_dict(activity_list, element_rotation_matrix)
const = loads_dict["constant force"]
quad = loads_dict["quadratic force"]
sinus = loads_dict["sinus force"]
loads = loads_dict["linear load configuration"]
unique_list = self.getuniquepositionlist(loads)
final_list = []
distributed_loads = None
max_load = 0
for pos in unique_list:
value = self.get_before_and_after(pos, loads)
if value["before"] == value["after"]:
final_list.append([pos] + value["before"])
else:
final_list.append([pos] + value["before"])
final_list.append([pos] + value["after"])
if len(final_list) == 0 and any(const + quad + sinus):
final_list.append([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
final_list.append([member_length, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
elif len(final_list) > 0:
if final_list[0][0] and any(
const + quad + sinus
): # if first item location is not 0 append an item at the zero
final_list = [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]] + final_list
else:
del final_list[0]
if abs(final_list[-1][0] - member_length) > 0.01 and any(const + quad + sinus):
final_list.append([member_length, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
else:
del final_list[-1]
if len(final_list) > 0:
array = np.array(final_list) # 7xn -> ["pos","fx","fy","fz","mx","my","mz"]
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
polyline = []
distributed_loads = {
"fx": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"fy": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"fz": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"mx": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"my": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
"mz": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
}
max_load = 0
for component, key in enumerate(keys):
if any([sinus[component], quad[component], const[component]]) or any(
item for item in array[:, component + 1]
):
for currentitem in final_list:
polyline.append([currentitem[0], currentitem[component + 1]])
max_load = max(
max_load,
abs(sinus[component] + quad[component] + const[component] + currentitem[component + 1]),
)
inner_dict = distributed_loads[key]
inner_dict["constant"] = const[component]
inner_dict["quadratic"] = quad[component]
inner_dict["sinus"] = sinus[component]
inner_dict["polyline"] = polyline
distributed_loads[key] = inner_dict
return {
"linear loads": distributed_loads,
"max linear load": max_load,
"discrete loads": loads_dict["point load configuration"],
}
def getuniquepositionlist(self, load_config_list: list[list[LoadConfigItem]]) -> list[float]:
"""return an ordereded list of unique locations based on the load configuration list
ex: load_config_list = [[{"pos":1.0,...},{"pos":3.0,...}],
[{"pos":2.0,...},{"pos":3.0,...}],
[{"pos":1.5,...},{"pos":2.5,...}]]
return = [1.0, 1.5, 2.0, 2.5, 3.0]
"""
unique = []
for config in load_config_list:
for info in config:
if info["pos"] in unique:
continue
unique.append(info["pos"])
unique.sort()
return unique
def interp1d(self, l1: list[float], l2: list[float], pos: float) -> float:
"""1d linear interpolation for the vector components"""
fac = (l2[1] - l1[1]) / (l2[0] - l1[0])
v = l1[1] + fac * (pos - l1[0])
return v
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int, key: str) -> np.ndarray:
"""interpolate the result vectors between load poits"""
result = np.zeros(6)
for i in range(6):
value1 = [loadinfo[start]["pos"], loadinfo[start][key][i]] # [position, force_component]
value2 = [loadinfo[end]["pos"], loadinfo[end][key][i]] # [position, force_component]
result[i] = self.interp1d(value1, value2, pos) # interpolated [position, force_component]
return result
def get_before_and_after(self, pos: float, load_config_list: list[list[LoadConfigItem]]) -> dict[str, list[float]]:
"""get total values for forces and moments before and after the position
example:
pos = 2.0
load_config_list = [[{"pos":1.0, "descr":"start, "load values":[1,0,0,0,0,0]},
{"pos":3.0, "descr":"end, "load values":[3,0,0,0,0,0]}],
[{"pos":2.0, "descr":"start, "load values":[1,0,0,0,0,0]},
{"pos":3.0, "descr":"end, "load values":[1,0,0,0,0,0]}],
[{"pos":1.5, "descr":"start, "load values":[1,0,0,0,0,0]},
{"pos":2.5, "descr":"end, "load values":[1,0,0,0,0,0]}],
return = {
"before": [3,0,0,0,0,0], ->(fx, fy, fz, mx, my, mz)
" after": [4,0,0,0,0,0] ->(fx, fy, fz, mx, my, mz)
}
"""
load_before = np.zeros(6)
load_after = np.zeros(6)
for config in load_config_list:
if pos < config[0]["pos"] or pos > config[-1]["pos"]:
continue
start = 0
end = len(config) - 1
while end - start > 0:
if pos < config[start]["pos"] or pos > config[end]["pos"]:
break
if config[start]["pos"] == pos:
if config[start]["descr"] in ["start", "middle"]:
load_after += config[start]["load values"]
elif config[start]["descr"] in ["end", "middle"]:
load_before += config[start]["load values"]
elif config[end]["pos"] == pos:
if config[end]["descr"] in ["start", "middle"]:
load_after += config[end]["load values"]
elif config[end]["descr"] in ["end", "middle"]:
load_before += config[end]["load values"]
elif end - start == 1:
load_before += self.interpolate(pos, config, start, end, "load values")
load_after += self.interpolate(pos, config, start, end, "load values")
start += 1
end -= 1
return_value = {"before": load_before.tolist(), "after": load_after.tolist()}
return return_value
def parse_linear_loads_to_dict(
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
) -> ParsedLoad:
"""
get load list
activity_list: list of IfcStructuralCurveAction or IfcStructuralCurveReaction
applied in the structural curve member
global_to_local: transformation matrix from global coordinates to local coordinetes
return: dict{
"constant force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
constant distribution
"quadratic force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
quadratic distribution
"sinus force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
sinus distribution
"linear load configuration": list -> list of load configurations for linear
and polyline distributions of linear loads
}
description of "linear load configuration":
list[ -> one item (list)for each IfcStructuralCurveAction applied in the member
with IfcStructuralLoadConfiguration as the applied load
list[ -> one item (dict) for each item found in the
Locations attribute of IfcLoadConfiguration
dict{
"pos": float, -> local position along curve length
"descr": string, -> describe if the item is at the start, middle or end of the list
"load values": Array, -> linear force applied at that point
}
]
]
"""
constant = np.zeros(6)
quadratic = np.zeros(6)
sinus = np.zeros(6)
linear_load_configurations = []
point_load_configurations = []
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), "LENGTHUNIT")
def get_load_values(load, transform_matrix, factor=1.0):
result = np.zeros(6)
keys = ["LinearForceX", "LinearForceY", "LinearForceZ", "LinearMomentX", "LinearMomentY", "LinearMomentZ"]
for i, key in enumerate(keys):
value = 0 if getattr(load, key, 0) is None else getattr(load, key, 0)
result[i] += value * factor
return transform_matrix @ result
for item in activity_list:
activity = item[0]
factor = item[1]
load = activity.AppliedLoad
transform_3by3 = self.get_activity_transform_matrix(activity, element_rotation_matrix)
transform_6by6 = np.zeros((6, 6))
transform_6by6[0:3, 0:3] = transform_3by3
transform_6by6[3:6, 3:6] = transform_3by3
# values for linear loads
if load.is_a("IfcStructuralLoadConfiguration"):
locations = getattr(load, "Locations", [])
values = [l for l in getattr(load, "Values", None) if l.is_a() == "IfcStructuralLoadLinearForce"]
config_list = []
for i, l in enumerate(values):
load_values = get_load_values(l, transform_6by6, factor)
if i == 0:
descr = "start"
elif i == len(values) - 1:
descr = "end"
else:
descr = "middle"
config_list.append(
{"pos": locations[i][0] * unit_scale, "descr": descr, "load values": load_values}
)
linear_load_configurations.append(config_list)
# load configurations with point loads
values = [l for l in getattr(load, "Values", None) if l.is_a() == "IfcStructuralLoadSingleForce"]
attr_list = ["ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ"]
config_list = []
for i, val in enumerate(values):
result_list = [0, 0, 0, 0, 0, 0]
for j, attr in enumerate(attr_list):
value = 0 if getattr(val, attr, 0) is None else getattr(val, attr, 0)
result_list[j] += value * factor
config_list.append({"pos": locations[i][0] * unit_scale, "values": result_list})
point_load_configurations.append(config_list)
else:
load_values = get_load_values(load, transform_6by6, factor)
if "CONST" == getattr(activity, "PredefinedType", None) or activity.is_a("IfcStructuralLinearAction"):
constant += load_values
elif "PARABOLA" == getattr(activity, "PredefinedType", None):
quadratic += load_values
elif "SINUS" == getattr(activity, "PredefinedType", None):
sinus += load_values
return_value = {
"constant force": constant.tolist(),
"quadratic force": quadratic.tolist(),
"sinus force": sinus.tolist(),
"linear load configuration": linear_load_configurations,
"point load configuration": point_load_configurations,
}
return return_value
@@ -27,6 +27,45 @@ import bonsai.tool as tool
from math import degrees
from mathutils import Vector, Matrix
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.structural.decorator import LoadsDecorator
class ShowLoads(bpy.types.Operator):
"""Draw decorations to show strucutural actions in 3d view"""
bl_idname = "bim.show_loads"
bl_label = "Show loads in 3D View"
def modal(self, context, event):
if event.type == "F5":
LoadsDecorator.update()
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
if event.type == "ESC":
LoadsDecorator.uninstall()
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
return {"FINISHED"}
return {"PASS_THROUGH"}
def invoke(self, context, event):
collection = bpy.data.collections["IfcStructuralItem"]
collection.hide_viewport = False
context.window.cursor_modal_set("WAIT")
try:
LoadsDecorator.install(context)
except Exception as exc:
context.window.cursor_modal_restore()
raise exc
context.window.cursor_modal_restore()
context.window_manager.modal_handler_add(self)
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
return {"RUNNING_MODAL"}
class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator):
@@ -21,7 +21,12 @@ import bpy
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.module.structural.data import StructuralLoadCasesData, StructuralLoadsData, BoundaryConditionsData
from bonsai.bim.module.structural.data import (
StructuralLoadCasesData,
StructuralLoadsData,
BoundaryConditionsData,
LoadGroupDecorationData,
)
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -35,6 +40,16 @@ from bpy.props import (
)
def get_load_groups_to_show(self, context):
if not LoadGroupDecorationData.is_loaded:
LoadGroupDecorationData.load()
return LoadGroupDecorationData.data["load groups to show"]
def update_activity_type(self, context):
LoadGroupDecorationData.is_loaded = False
def get_applicable_structural_load_types(self, context):
if not StructuralLoadCasesData.is_loaded:
StructuralLoadCasesData.load()
@@ -152,6 +167,27 @@ class BIMStructuralProperties(PropertyGroup):
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
filtered_boundary_conditions: BoolProperty(name="Filtered Boundary Conditions", default=False)
show_loads: BoolProperty(name="Show Loads", default=False)
update_load_repr: BoolProperty(name="Update Load Representation", default=False)
enable_repr_auto_update: BoolProperty(name="Auto Update", default=False)
reference_frame: EnumProperty(
items=[
("GLOBAL_COORDS", "Global", "Show loads in global reference frame"),
("LOCAL_COORDS", "Local", "Show loads in local reference frame"),
],
name="Reference Frame",
)
activity_type: EnumProperty(
items=[
("Action", "Actions", "Show actions loads"),
("External Reaction", "External Reactions", "Show reactions on boundary conditions"),
("Internal Reactions", "Internal Reactions", "Show internal reactions on members"),
],
name="Activity Type",
update=update_activity_type,
)
load_group_to_show: EnumProperty(items=get_load_groups_to_show, name="Load Groups")
class BIMObjectStructuralProperties(PropertyGroup):
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
@@ -0,0 +1,294 @@
# 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 gpu
from typing import Literal
class DecorationShader:
"shader for the load decorations"
def __init__(self):
pass
def new(
self,
pattern: Literal[
"PERPENDICULAR DISTRIBUTED FORCE",
"PARALLEL DISTRIBUTED FORCE",
"DISTRIBUTED MOMENT",
"SINGLE FORCE",
"SINGLE MOMENT",
"PLANAR LOAD",
],
) -> gpu.types.GPUShader:
"""pattern: string description of the desired shader
Possible values
PERPENDICULAR DISTRIBUTED FORCE: pattern for distributed force
perpendicular to the curve member axis
PARALLEL DISTRIBUTED FORCE: pattern for distributed force
along the curve member axis
DISTRIBUTED MOMENT: pattern for distributed moment in curve members
SINGLE FORCE: pattern for single forces
SINGLE MOMENT: pattern for single moments
PLANAR LOAD: pattern for planar loads
"""
valid_patterns = {
"PERPENDICULAR DISTRIBUTED FORCE",
"PARALLEL DISTRIBUTED FORCE",
"DISTRIBUTED MOMENT",
"SINGLE FORCE",
"SINGLE MOMENT",
"PLANAR LOAD",
}
if pattern not in valid_patterns:
raise ValueError(
"""pattern must be one of:
PERPENDICULAR DISTRIBUTED FORCE
PARALLEL DISTRIBUTED FORCE,
DISTRIBUTED MOMENT,
SINGLE FORCE,
SINGLE MOMENT,
PLANAR LOAD"""
)
if "DISTRIBUTED" in pattern.upper():
shader = self.get_linear_shader(pattern)
return shader
elif "SINGLE" in pattern.upper():
shader = self.get_point_shader(pattern)
return shader
elif "PLANAR" in pattern.upper():
shader = self.get_planar_shader()
return shader
def get_linear_shader(
self, pattern: Literal["PERPENDICULAR DISTRIBUTED FORCE", "PARALLEL DISTRIBUTED FORCE", "DISTRIBUTED MOMENT"]
) -> gpu.types.GPUShader:
"""pattern: type of pattern
PERPENDICULAR DISTRIBUTED FORCE
PARALLEL DISTRIBUTED FORCE,
DISTRIBUTED MOMENT,
"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "forces")
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "viewProjectionMatrix")
shader_info.push_constant("VEC4", "color")
shader_info.push_constant("FLOAT", "spacing")
shader_info.push_constant("FLOAT", "maxload")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "VEC3", "sin_quad_lin_forces")
shader_info.vertex_in(2, "VEC3", "coord")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.vertex_source(
"void main()"
"{"
" gl_Position = viewProjectionMatrix * vec4(position, 1.0f);"
" co = coord;"
" forces = sin_quad_lin_forces;"
"}"
)
if pattern == "PERPENDICULAR DISTRIBUTED FORCE":
shader_info.fragment_source(
"void main()"
"{"
"float x = co.x;"
"float y = co.y;"
"float abs_y = abs(y);"
"float a = abs(mod(x,spacing)-0.5*spacing)*5.0;"
"float b = step(a,abs_y)*(step(abs_y,1.2*spacing));"
"float c = step(0.8*spacing,mod(x+0.4*spacing,spacing))*(step(1.2*spacing,abs_y));"
"float sinvalue = forces.x;"
"float quadraticvalue = forces.y;"
"float linearvalue = forces.z;"
"x = co.x/co.z;"
"float f = (sin(x*3.1416)*sinvalue"
"+(-4.*x*x+4.*x)*quadraticvalue"
"+linearvalue)/maxload;"
"float mask = step(0.,y)*step(y,f)+step(y,0.)*step(f,y);"
"float top = step(abs(y-f),0.2*1.2*spacing);"
"float d = clamp(top+b+c,0.0,0.9)*mask;"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
elif pattern == "PARALLEL DISTRIBUTED FORCE":
shader_info.fragment_source(
"void main()"
"{"
"float y = co.y;"
"float x = step(0.,y)*(co.z-co.x)+step(y,0.)*(co.x);"
"float abs_y = abs(y);"
"float a = abs(mod(abs_y,spacing)-0.5*spacing)*5.0;"
"float a2 = mod(x,3.0*spacing);"
"float b = step(a,a2)*step(a2,1.2*spacing);"
"float c = step(0.8*spacing,mod(abs_y+0.4*spacing,spacing))"
"*(step(1.2*spacing,a2))*step(a2,2.5*spacing);"
"float sinvalue = forces.x;"
"float quadraticvalue = forces.y;"
"float linearvalue = forces.z;"
"x = co.x/co.z;"
"float f = (sin(x*3.1416)*sinvalue"
"+(-4.*x*x+4.*x)*quadraticvalue"
"+linearvalue)/maxload;"
"float mask = step(0.,y)*step(y,f)+step(y,0.)*step(f,y);"
"float top = step(abs(y-f),0.2*1.2*spacing);"
"float d = clamp(top+b+c,0.0,0.9)*mask;"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
elif pattern == "DISTRIBUTED MOMENT":
shader_info.fragment_source(
"void main()"
"{"
"float x = step(co.y,0.)*(co.x)+step(0.,co.y)*(co.z-co.x);"
"float y = step(co.y,-0.00001)*(co.y)+step(0.,co.y)*(0.-co.y);"
"x = mod((0.5/spacing)*x,1.4)-0.7;"
"y = mod((0.5/spacing)*y,1.4)-0.7;"
"float abs_y = abs(y);"
"vec2 st = vec2(1.9*x,y);"
"vec2 orig = vec2(0.,0.);"
"float circ = step(distance(st,orig),0.33)*step(0.27,distance(st,orig));"
"float tri_mask = step(st.y,st.x)+step(-st.x,st.y);"
"float circ_arrow = step(st.x,4.0*st.y-0.75)*step(0.25*st.y-0.34,st.x)*(1.-tri_mask);"
"float circmask = step(distance(st,orig),0.1)+step(0.5,distance(st,orig))+step(st.x,0.);"
"float body = step(-0.03,st.y)*step(st.y,0.03)*step(-0.3,x)*step(x,0.576);"
"float body_arrow = step(-0.5+3.*st.y,x)*step(-0.5-3.*st.y,x)*step(x,-0.3);"
"float d = clamp(circmask*(body+body_arrow)+circ_arrow+circ*tri_mask,0.,1.);"
"float sinvalue = forces.x;"
"float quadraticvalue = forces.y;"
"float linearvalue = forces.z;"
"x = co.x/co.z;"
"float f = (sin(x*3.1416)*sinvalue"
"+(-4.*x*x+4.*x)*quadraticvalue"
"+linearvalue)/maxload;"
"float mask = step(0.,co.y)*step(co.y,f)+step(co.y,0.)*step(f,co.y);"
"float top = step(abs(co.y-f),0.2*1.2*spacing);"
"d = clamp(top+d,0.0,0.9)*mask;"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
def get_point_shader(self, pattern: Literal["SINGLE FORCE", "SINGLE MOMENT"]) -> gpu.types.GPUShader:
"""param: pattern: type of pattern
SINGLE FORCE,
SINGLE MOMENT"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "viewProjectionMatrix")
shader_info.push_constant("VEC4", "color")
shader_info.push_constant("FLOAT", "spacing")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "VEC3", "coord")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.vertex_source(
"void main()" "{" " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" " co = coord;" "}"
)
if pattern == "SINGLE FORCE":
shader_info.fragment_source(
"void main()"
"{"
"float body = step(abs(co.x),0.2*spacing)*step(2.*spacing,co.y);"
"float arrow = step(3.5*abs(co.x)+0.02,abs(co.y))*step(co.y,2.*spacing)*step(0.,co.y);"
"float d = clamp(body+arrow,0.0,0.5);"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
elif pattern == "SINGLE MOMENT":
shader_info.fragment_source(
"void main()"
"{"
"float circ = step(distance(co.xy,vec2(0.,0.)),0.33)*step(0.27,distance(co.xy,vec2(0.,0.)));"
"float mask = step(co.y,co.x)+step(-co.x,co.y);"
"float circ_arrow = step(co.x,4.0*co.y-0.75)*step(0.25*co.y-0.34,co.x)*(1.-mask);"
"float d = clamp(circ_arrow+circ*mask,0.0,0.5);"
"if (d == 0.0) discard;"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
def get_planar_shader(self) -> gpu.types.GPUShader:
"""shader for planar loads"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "viewProjectionMatrix")
shader_info.push_constant("VEC4", "color")
shader_info.push_constant("FLOAT", "spacing")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "VEC3", "coord")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.vertex_source(
"void main()" "{" " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" " co = coord;" "}"
)
shader_info.fragment_source(
"void main()"
"{"
"float x = co.x;"
"float y = co.y;"
"float abs_y = abs(y);"
"float a = abs(mod(x,spacing)-0.5*spacing)*5.0;"
"float b = step(a,abs_y)*(step(abs_y,1.2*spacing));"
"float c = step(0.8*spacing,mod(x+0.4*spacing,spacing))*(step(1.2*spacing,abs_y));"
"float mask = step(0.,y)*step(y,0.98)+step(y,0.)*step(0.98,y);"
"float top = step(abs(y-0.98),0.2*1.2*spacing);"
"float d = clamp(0.2*y+(top+b+c)*mask,0.0,0.4);"
"FragColor = vec4(color.xyz,d*color.w);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
@@ -445,6 +445,37 @@ class BIM_UL_structural_activities(UIList):
row.label(text=item.applied_load_class)
class BIM_PT_show_structural_activities(Panel):
bl_label = "Show Loads"
bl_idname = "BIM_PT_show_structural_activities"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_structural"
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
self.props = context.scene.BIMStructuralProperties
row = self.layout.row(align=True)
row.operator(
"bim.show_loads",
text="Show loads",
icon="HIDE_OFF",
)
row = self.layout.row(align=True)
row.prop(self.props, "reference_frame")
row = self.layout.row(align=True)
row.prop(self.props, "activity_type")
row = self.layout.row(align=True)
row.prop(self.props, "load_group_to_show")
class BIM_PT_structural_loads(Panel):
bl_label = "Structural Loads"
bl_idname = "BIM_PT_structural_loads"

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