mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0716e5038e | |||
| b928902e3c |
@@ -91,6 +91,7 @@ modules = {
|
||||
"light": None,
|
||||
"alignment": None,
|
||||
"clip_box": None,
|
||||
"status_render": None,
|
||||
# Uncomment this line to enable loading of the demo module. Happy hacking!
|
||||
# The name "demo" must correlate to a folder name in `bim/module/`.
|
||||
# "demo": None,
|
||||
|
||||
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_D
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2,#31));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -35,5 +35,6 @@ DATA;
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#29=IFCSIMPLEPROPERTYTEMPLATE('0lP6Y8q9v2QhDnR4sT7uVx',$,'PerspectiveShiftX','Horizontal perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#30=IFCSIMPLEPROPERTYTEMPLATE('2mR8b1NcW5EoFyG7hJ9kLp',$,'PerspectiveShiftY','Vertical perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#31=IFCSIMPLEPROPERTYTEMPLATE('2dFtucOLv6oBy6wzom$LMq',$,'RenderOverrides','JSON list of Bonsai render override rules (selection filter plus exposure/gamma/transparency) applied to this drawing on render.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -63,7 +63,13 @@ class BIM_PT_camera(Panel):
|
||||
self.layout.use_property_split = True
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
|
||||
col = self.layout.column(align=True)
|
||||
header, body = self.layout.panel("drawing_settings", default_closed=True)
|
||||
header.label(text="Drawing Settings", icon="PREFERENCES")
|
||||
if not body:
|
||||
return
|
||||
body.use_property_split = True
|
||||
|
||||
col = body.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "has_underlay", icon="OUTLINER_OB_IMAGE")
|
||||
row.prop(dprops, "should_use_underlay_cache", text="", icon="FILE_REFRESH")
|
||||
@@ -78,44 +84,44 @@ class BIM_PT_camera(Panel):
|
||||
row = col.row(align=True)
|
||||
row.prop(dprops, "should_draw_linked_projects")
|
||||
if dprops.should_draw_linked_projects:
|
||||
header, panel = self.layout.panel("links_to_draw")
|
||||
header.label(text="Linked Projects to Draw", icon="OUTPUT")
|
||||
links_header, links_panel = body.panel("links_to_draw")
|
||||
links_header.label(text="Linked Projects to Draw", icon="OUTPUT")
|
||||
|
||||
pprops = tool.Project.get_project_props()
|
||||
links = list(pprops.get_loaded_links())
|
||||
if panel:
|
||||
if links_panel:
|
||||
if links:
|
||||
for link in links:
|
||||
row = panel.row(align=True)
|
||||
row = links_panel.row(align=True)
|
||||
split = row.split(factor=0.9)
|
||||
split.label(text=link.filepath, icon="FILE")
|
||||
split.prop(link, "include_in_drawings", text="")
|
||||
else:
|
||||
panel.label(text="No IFC projects linked and loaded.")
|
||||
links_panel.label(text="No IFC projects linked and loaded.")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row = body.row(align=True)
|
||||
row.prop(props, "target_view")
|
||||
|
||||
if props.target_view == "MODEL_VIEW":
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(props, "camera_type")
|
||||
if props.camera_type == "PERSP":
|
||||
row = self.layout.row(align=True)
|
||||
row = body.row(align=True)
|
||||
row.prop(camera_data, "shift_x", text="Camera Shift X/Y:")
|
||||
row.prop(camera_data, "shift_y", text="")
|
||||
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(props, "linework_mode")
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(props, "generate_material_layers")
|
||||
if props.linework_mode == "OPENCASCADE":
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(props, "fill_mode")
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(props, "cut_mode")
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(props, "width")
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(props, "height")
|
||||
|
||||
render = context.scene.render
|
||||
@@ -126,7 +132,7 @@ class BIM_PT_camera(Panel):
|
||||
and str(render.engine) == tool.Blender.get_eevee_name()
|
||||
and ((megapixels := (render.resolution_x * render.resolution_y / 10**6)) > MEGAPIXELS_WARNING_THRESHOLD)
|
||||
):
|
||||
box = self.layout.box()
|
||||
box = body.box()
|
||||
box.label(
|
||||
text=f"Resulting image size is {render.resolution_x} x {render.resolution_y} ({round(megapixels, 2)} MP).",
|
||||
icon="ERROR",
|
||||
@@ -136,20 +142,20 @@ class BIM_PT_camera(Panel):
|
||||
)
|
||||
box.label(text="Underlay render might crash if VRAM requirement is not met.")
|
||||
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(camera_data, "clip_end", text="Depth")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row = body.row(align=True)
|
||||
row.prop(props, "diagram_scale", text="Scale")
|
||||
row.prop(props, "is_nts", text="", icon="MOD_EDGESPLIT")
|
||||
|
||||
if props.diagram_scale == "CUSTOM":
|
||||
row = self.layout.row(align=True)
|
||||
row = body.row(align=True)
|
||||
row.prop(props, "custom_scale_numerator", text="Custom Scale")
|
||||
row.prop(props, "custom_scale_denominator", text="")
|
||||
|
||||
if props.has_underlay:
|
||||
row = self.layout.row()
|
||||
row = body.row()
|
||||
row.prop(props, "dpi")
|
||||
|
||||
|
||||
|
||||
@@ -648,6 +648,7 @@ class ApplyFilterFromText(Operator):
|
||||
filter_structure = json_data.get("filter_structure", [])
|
||||
filter_groups = tool.Search.get_filter_groups(module)
|
||||
tool.Search.import_filter_structure(filter_structure, filter_groups)
|
||||
tool.Search.on_filter_query_edited(module, context)
|
||||
self.report({"INFO"}, "Filter configuration applied successfully")
|
||||
|
||||
if len(context.window_manager.windows) > 1:
|
||||
@@ -681,6 +682,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
|
||||
tool.Search.import_filter_query(self.query, filter_groups)
|
||||
except:
|
||||
return
|
||||
tool.Search.on_filter_query_edited(module, context)
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# status_render — developer notes
|
||||
|
||||
Per-drawing render overrides: select IFC elements with a filter and apply render
|
||||
effects (exposure, gamma, transparency) to them. Aimed at drawing underlays
|
||||
("existing faded", "demolished ghosted") but also works for plain F12 renders.
|
||||
|
||||
## Two-layer architecture
|
||||
|
||||
Effects live where they can actually be shown, both driven by the one
|
||||
**Enable Render Overrides** toggle:
|
||||
|
||||
| Layer | Effect | Mechanism | Applied | Visible |
|
||||
|-------|--------|-----------|---------|---------|
|
||||
| Live material | Transparency | Temp material copy with a Transparent BSDF mixed into the surface (`surface_render_method = "BLENDED"`) | Persistently while enabled | Rendered viewport **and** render |
|
||||
| Render compositor | Exposure / gamma | Cryptomatte object matte → Exposure/Gamma → Mix, per rule, chained | Built per render, removed after | Render only |
|
||||
|
||||
Transparency is a *real* material change so the geometry behind shows through
|
||||
(compositing can't reveal occluded geometry after an opaque render). Because it's
|
||||
a material, EEVEE Next renders it live in the viewport — that's the WYSIWYG path.
|
||||
|
||||
Exposure/gamma stay in the compositor because that's the faithful colour-management
|
||||
tonemap. They are render-only: the viewport compositor can't read render passes
|
||||
(Cryptomatte), so per-object masking isn't available there.
|
||||
|
||||
## Why Cryptomatte (not the Object Index pass)
|
||||
|
||||
EEVEE Next always renders the Object Index pass as 0 (Blender bug #121690).
|
||||
Cryptomatte works in both EEVEE and Cycles. Note: node-socket string subscripting
|
||||
keys by `.identifier`, not `.name` — relevant if ever touching pass sockets again.
|
||||
|
||||
## Apply / restore seam
|
||||
|
||||
- `sync_live_effects(scene)` — single source of truth for live material state:
|
||||
restore all temp materials, then apply transparency for the active camera's
|
||||
enabled rules. Idempotent; safe to call any time.
|
||||
- `build_compositor(scene, props)` / `clear_compositor(scene)` — render-only colour
|
||||
nodes; clearing leaves live materials untouched.
|
||||
|
||||
## Lifecycle / handlers (operator.py)
|
||||
|
||||
- enable toggle + transparency slider (prop `update=`), add/remove rule → `sync_live_effects`
|
||||
- `render_init` → sync materials, then `build_compositor`
|
||||
- `render_complete` / `render_cancel` → `clear_compositor` (materials persist)
|
||||
- `save_pre` → `restore_transparency` (never bake temp materials into the .blend)
|
||||
- `save_post`, `load_post` → `sync_live_effects` (re-apply the live preview)
|
||||
|
||||
## Storage / gating
|
||||
|
||||
- Per-drawing: stored on the camera datablock as `Camera.BIMRenderOverrideProperties`
|
||||
(self-contained — the core drawing module does not depend on this one).
|
||||
- Per-rule filters reuse the shared Search system; resolver keys are
|
||||
`status_render_{rule_index}` (see `tool/search.py`).
|
||||
- The render path is the compositor, which only runs for F12 and `render.render()`
|
||||
drawing underlays. Viewport/OpenGL drawings bypass it, so the panel disables the
|
||||
toggle when the *applied* shading style (`EPset_Drawing.CurrentShadingStyle`) is
|
||||
not "Default" render type. The drawing also needs an underlay for the override to
|
||||
appear in it.
|
||||
|
||||
## IFC persistence (coarse auto-sync)
|
||||
|
||||
Rules are mirrored into the IFC model so they travel with the drawing, not just the
|
||||
`.blend`. The camera props remain the working/edit copy; IFC is the source of truth.
|
||||
|
||||
- **Property:** `EPset_Drawing.RenderOverrides` — a JSON list of
|
||||
`{name, query, exposure, gamma, transparency}` per rule. `query` uses the same
|
||||
`tool.Search.export/import_filter_query` serialization as the drawing
|
||||
Include/Exclude filters.
|
||||
- **Write (coarse):** on add/remove rule, and on `save_pre` for every drawing camera
|
||||
(`save_rules_to_ifc`). Deliberately *not* on every slider tick (would spam IFC).
|
||||
Field edits (exposure/gamma/transparency/filter) therefore persist at the next
|
||||
add/remove or at the next save.
|
||||
- **Read:** rules are pulled from the pset by `ensure_rules_loaded` (guarded: it only
|
||||
loads when the camera props are empty, so it never clobbers in-session edits). This
|
||||
runs from:
|
||||
- `data.refresh()` — called by `bonsai.bim.handler.refresh_ui_data()`, which fires
|
||||
at the end of `load_project_elements`. **This is the path that handles opening an
|
||||
IFC into a fresh session** — `load_post` fires *before* the IFC import creates the
|
||||
drawing cameras, so it can't see them.
|
||||
- `load_post` — for reopening a `.blend` (cameras already exist; usually a no-op
|
||||
since their props came from the file).
|
||||
- `render_init` — belt-and-braces before a render.
|
||||
`deserialize_rules` uses direct id-property writes so it doesn't fire the
|
||||
live-preview update per rule.
|
||||
- **Not persisted to IFC:** the `enabled` toggle (session/working state, lives in the
|
||||
`.blend` only). So rules are portable across `.blend` files; whether the preview is
|
||||
currently *on* is not.
|
||||
|
||||
Caveats:
|
||||
- The pset writes are raw `ifcopenshell.api.pset.edit_pset` calls (matching
|
||||
`edit_element_filter`), not wrapped in `tool.Ifc.Operator`, so they are not in
|
||||
Bonsai's IFC undo stack and may not flip the "unsaved IFC" indicator. They are
|
||||
rewritten from the camera props on the next save, so the stored JSON self-heals.
|
||||
- Saving the IFC *without* a `.blend` save after a slider/filter edit may miss that
|
||||
edit (it's flushed in `save_pre`, a `.blend`-save handler). Add/remove a rule or
|
||||
save the `.blend` to flush.
|
||||
|
||||
## Active-drawing switch
|
||||
|
||||
Switching the active drawing reassigns `scene.camera` but fires no handler. A msgbus
|
||||
subscription on `(bpy.types.Scene, "camera")` (`subscribe_camera_change`) catches it
|
||||
and re-syncs the live materials (the previous drawing's transparency is removed, the
|
||||
new drawing's applied). The notify defers via `bpy.app.timers` so material datablock
|
||||
edits happen outside the msgbus notification context. msgbus is cleared on file load,
|
||||
so we re-subscribe in `load_post` (and on register).
|
||||
|
||||
## Filter edits
|
||||
|
||||
Editing a rule's filter via `bim.edit_filter_query` (or `bim.apply_filter_from_text`)
|
||||
re-syncs the live preview: those operators call `tool.Search.on_filter_query_edited`,
|
||||
which dispatches to `operator.sync_live_effects` for `status_render_*` modules. This
|
||||
keeps the coupling in `tool.Search` (which already special-cases `status_render` in
|
||||
`get_filter_groups`), leaving the shared search operators generic.
|
||||
|
||||
## Deferred robustness (TODO when the concept proves out)
|
||||
|
||||
- **Undo** can desync live state — re-toggle to refresh.
|
||||
- Transparency slider re-syncs on every increment (creates/removes temp materials);
|
||||
fine for now, could debounce on large scenes.
|
||||
- Exposure/gamma in the viewport would require re-expressing them as material tweaks
|
||||
(approximate); intentionally not done — they stay faithful + render-only.
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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
|
||||
|
||||
from . import data, operator, prop, ui # noqa: F401 (data is accessed via refresh_ui_data)
|
||||
|
||||
classes = (
|
||||
operator.AddRenderOverrideRule,
|
||||
operator.RemoveRenderOverrideRule,
|
||||
prop.BIMRenderOverrideRule,
|
||||
prop.BIMRenderOverrideProperties,
|
||||
ui.BIM_UL_render_override_rules,
|
||||
ui.BIM_PT_status_render,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Camera.BIMRenderOverrideProperties = bpy.props.PointerProperty(type=prop.BIMRenderOverrideProperties)
|
||||
operator.register_handlers()
|
||||
|
||||
|
||||
def unregister():
|
||||
operator.unregister_handlers()
|
||||
del bpy.types.Camera.BIMRenderOverrideProperties
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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/>.
|
||||
|
||||
|
||||
def refresh():
|
||||
"""Called by bonsai.bim.handler.refresh_ui_data() after IFC operations, including
|
||||
project load (which fires too late for the blend load_post handler). Pulls each
|
||||
freshly-imported drawing camera's rules out of its IFC pset. Guarded so it never
|
||||
overwrites rules being edited in-session.
|
||||
"""
|
||||
from bonsai.bim.module.status_render import operator
|
||||
|
||||
for camera in operator.drawing_cameras():
|
||||
operator.ensure_rules_loaded(camera)
|
||||
@@ -0,0 +1,522 @@
|
||||
# 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/>.
|
||||
|
||||
"""Render-only exposure/gamma override for IFC elements selected by a filter query.
|
||||
|
||||
Blender's ``view_settings.exposure`` and ``view_settings.gamma`` are global
|
||||
colour-management settings and cannot be assigned per object. This module
|
||||
reproduces the effect for a subset of elements (selected via the shared Search
|
||||
filter system, e.g. ``EPset_Status.Status=EXISTING``) using a Cryptomatte object
|
||||
mask plus a small compositor graph, so the override only shows up in the final
|
||||
render and the viewport is untouched.
|
||||
|
||||
Cryptomatte (not the Object Index pass) is used because EEVEE Next always renders
|
||||
the Object Index pass as 0 (Blender bug #121690); Cryptomatte works in both EEVEE
|
||||
and Cycles.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
import json
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.selector
|
||||
import bonsai.tool as tool
|
||||
|
||||
# Marker stored on every node we create so re-running can clean up its own work
|
||||
# without touching the user's other compositor nodes.
|
||||
MARKER = "bim_status_render_override"
|
||||
|
||||
|
||||
def get_filtered_elements(filter_groups):
|
||||
"""Resolve a rule's filter groups to a set of IFC elements.
|
||||
|
||||
Mirrors bim.search so behaviour matches the shared filter UI under both the
|
||||
legacy query and the set-operations preference.
|
||||
"""
|
||||
ifc = tool.Ifc.get()
|
||||
if not ifc or not len(filter_groups):
|
||||
return set()
|
||||
|
||||
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
|
||||
# Migrate old "!" prefix filters to the filter_mode system, as bim.search does.
|
||||
for filter_group in filter_groups:
|
||||
for ifc_filter in filter_group.filters:
|
||||
if ifc_filter.type in ("entity", "instance") and ifc_filter.value.startswith("!"):
|
||||
ifc_filter.value = ifc_filter.value[1:]
|
||||
ifc_filter.filter_mode = "SUBTRACT"
|
||||
return tool.Search.execute_filter_groups(filter_groups)
|
||||
|
||||
query = tool.Search.export_filter_query(filter_groups)
|
||||
if not query:
|
||||
return set()
|
||||
return ifcopenshell.util.selector.filter_elements(ifc, query)
|
||||
|
||||
|
||||
def get_rule_objects(rule):
|
||||
"""Return the Blender mesh objects for the IFC elements a rule's filter matches."""
|
||||
return [
|
||||
obj
|
||||
for element in get_filtered_elements(rule.filter_groups)
|
||||
if isinstance(obj := tool.Ifc.get_object(element), bpy.types.Object) and obj.type == "MESH"
|
||||
]
|
||||
|
||||
|
||||
def clear_marked_nodes(tree):
|
||||
for node in list(tree.nodes):
|
||||
if node.get(MARKER):
|
||||
tree.nodes.remove(node)
|
||||
|
||||
|
||||
def get_or_create(tree, bl_idname):
|
||||
"""Reuse an existing user node of this type, or create a fresh one."""
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname == bl_idname and not node.get(MARKER):
|
||||
return node
|
||||
return tree.nodes.new(bl_idname)
|
||||
|
||||
|
||||
# Transparency is a real material effect (so geometry behind shows through), applied
|
||||
# before the render and restored after. These hold the swap state between the render
|
||||
# handlers. Only one render runs at a time, so module-level state is safe.
|
||||
_transparency_restore = [] # list of (object, slot_index, original_material)
|
||||
_temp_materials = [] # temporary transparent materials to delete on restore
|
||||
|
||||
|
||||
def make_transparent_material(material, amount):
|
||||
"""Copy a material and mix a Transparent BSDF into its surface by ``amount``."""
|
||||
dup = material.copy()
|
||||
dup[MARKER] = True
|
||||
dup.use_nodes = True
|
||||
tree = dup.node_tree
|
||||
output = next((n for n in tree.nodes if n.type == "OUTPUT_MATERIAL" and n.is_active_output), None)
|
||||
output = output or next((n for n in tree.nodes if n.type == "OUTPUT_MATERIAL"), None)
|
||||
if output and output.inputs["Surface"].links:
|
||||
surface = output.inputs["Surface"].links[0].from_socket
|
||||
transparent = tree.nodes.new("ShaderNodeBsdfTransparent")
|
||||
mix = tree.nodes.new("ShaderNodeMixShader")
|
||||
mix.inputs["Fac"].default_value = amount # 0 = opaque, 1 = fully transparent
|
||||
tree.links.new(surface, mix.inputs[1])
|
||||
tree.links.new(transparent.outputs[0], mix.inputs[2])
|
||||
tree.links.new(mix.outputs[0], output.inputs["Surface"])
|
||||
# Enable real alpha blending (EEVEE Next; fall back to the legacy property name).
|
||||
if hasattr(dup, "surface_render_method"):
|
||||
dup.surface_render_method = "BLENDED"
|
||||
elif hasattr(dup, "blend_method"):
|
||||
dup.blend_method = "BLEND"
|
||||
return dup
|
||||
|
||||
|
||||
def apply_transparency(objects, amount):
|
||||
"""Swap each object's materials for transparent copies, remembering the originals."""
|
||||
for obj in objects:
|
||||
for index, slot in enumerate(obj.material_slots):
|
||||
material = slot.material
|
||||
if material is None or material.get(MARKER):
|
||||
continue # no material, or already a temp material from another rule
|
||||
dup = make_transparent_material(material, amount)
|
||||
_temp_materials.append(dup)
|
||||
_transparency_restore.append((obj, index, material))
|
||||
slot.material = dup
|
||||
|
||||
|
||||
def restore_transparency():
|
||||
"""Put the original materials back and delete the temporary transparent copies."""
|
||||
for obj, index, material in _transparency_restore:
|
||||
try:
|
||||
obj.material_slots[index].material = material
|
||||
except (IndexError, ReferenceError):
|
||||
pass
|
||||
_transparency_restore.clear()
|
||||
for material in _temp_materials:
|
||||
try:
|
||||
bpy.data.materials.remove(material)
|
||||
except (ReferenceError, RuntimeError):
|
||||
pass
|
||||
_temp_materials.clear()
|
||||
|
||||
|
||||
def build_color_rule(tree, scene, view_layer, rule, objects, input_socket, x, y):
|
||||
"""Append one rule's exposure/gamma sub-graph to the compositor chain.
|
||||
|
||||
Applies the colour effects to ``input_socket`` only where the rule's Cryptomatte
|
||||
matte covers, and returns ``(output_image_socket, matte_socket)``.
|
||||
"""
|
||||
links = tree.links
|
||||
|
||||
def node(bl_idname, loc):
|
||||
n = tree.nodes.new(bl_idname)
|
||||
n[MARKER] = True
|
||||
n.location = loc
|
||||
return n
|
||||
|
||||
crypto = node("CompositorNodeCryptomatteV2", (x, y - 320))
|
||||
crypto.label = f"Matte: {rule.name}"
|
||||
crypto.source = "RENDER"
|
||||
crypto.scene = scene
|
||||
try:
|
||||
crypto.layer_name = f"{view_layer.name}.CryptoObject"
|
||||
except TypeError:
|
||||
pass # Keep the node's default layer; the enum item isn't available yet.
|
||||
crypto.matte_id = ", ".join(obj.name for obj in objects)
|
||||
links.new(input_socket, crypto.inputs["Image"])
|
||||
matte = crypto.outputs["Matte"]
|
||||
|
||||
exposure = node("CompositorNodeExposure", (x, y))
|
||||
exposure.label = f"Exposure: {rule.name}"
|
||||
exposure.inputs["Exposure"].default_value = rule.exposure
|
||||
links.new(input_socket, exposure.inputs["Image"])
|
||||
|
||||
gamma = node("CompositorNodeGamma", (x + 180, y))
|
||||
gamma.label = f"Gamma: {rule.name}"
|
||||
gamma.inputs["Gamma"].default_value = rule.gamma
|
||||
links.new(exposure.outputs["Image"], gamma.inputs["Image"])
|
||||
|
||||
mix = node("CompositorNodeMixRGB", (x + 360, y))
|
||||
mix.label = f"Apply: {rule.name}"
|
||||
mix.blend_type = "MIX"
|
||||
links.new(matte, mix.inputs["Fac"])
|
||||
links.new(input_socket, mix.inputs[1])
|
||||
links.new(gamma.outputs["Image"], mix.inputs[2])
|
||||
return mix.outputs["Image"], matte
|
||||
|
||||
|
||||
def build_compositor(scene, props):
|
||||
"""Build the compositor (exposure/gamma) chain for a render.
|
||||
|
||||
Transparency is NOT handled here -- it is a live material effect managed by
|
||||
sync_live_effects so it also shows in the viewport. This only adds the colour
|
||||
nodes layered per rule.
|
||||
"""
|
||||
scene.use_nodes = True
|
||||
# The node tree is only applied to renders when compositing is enabled.
|
||||
scene.render.use_compositing = True
|
||||
tree = scene.node_tree
|
||||
clear_marked_nodes(tree)
|
||||
|
||||
render_layers = get_or_create(tree, "CompositorNodeRLayers")
|
||||
composite = get_or_create(tree, "CompositorNodeComposite")
|
||||
view_layer = scene.view_layers.get(render_layers.layer) or scene.view_layers[0]
|
||||
|
||||
current = render_layers.outputs["Image"]
|
||||
x = render_layers.location.x + 320
|
||||
y = render_layers.location.y
|
||||
for rule in props.rules:
|
||||
objects = get_rule_objects(rule)
|
||||
if not objects:
|
||||
continue
|
||||
|
||||
# Exposure/gamma stay in the compositor, masked by Cryptomatte. Only build the
|
||||
# sub-graph when there is a colour change to make.
|
||||
if rule.exposure != 0.0 or rule.gamma != 1.0:
|
||||
# Cryptomatte reads object IDs from the render's CryptoObject passes, so the
|
||||
# pass must be enabled on the view layer the node samples.
|
||||
view_layer.use_pass_cryptomatte_object = True
|
||||
current, _ = build_color_rule(tree, scene, view_layer, rule, objects, current, x, y)
|
||||
x += 700
|
||||
|
||||
composite.location = (x, y)
|
||||
tree.links.new(current, composite.inputs["Image"])
|
||||
|
||||
|
||||
def get_camera_override_props(camera):
|
||||
"""Per-drawing override props live on the camera datablock; None if not a camera."""
|
||||
if camera and camera.type == "CAMERA":
|
||||
return camera.data.BIMRenderOverrideProperties
|
||||
return None
|
||||
|
||||
|
||||
# --- IFC persistence -------------------------------------------------------
|
||||
# The camera props are the working/edit copy; the rules are mirrored into the IFC
|
||||
# model at EPset_Drawing.RenderOverrides (JSON) so they travel with the drawing.
|
||||
# Coarse auto-sync: written on add/remove and on save, read on file load. Each
|
||||
# rule's filter reuses the same query serialization as the drawing Include/Exclude
|
||||
# filters (tool.Search.export/import_filter_query).
|
||||
|
||||
PSET_PROP = "RenderOverrides"
|
||||
|
||||
|
||||
def serialize_rules(props):
|
||||
return [
|
||||
{
|
||||
"name": rule.name,
|
||||
"query": tool.Search.export_filter_query(rule.filter_groups),
|
||||
"exposure": round(rule.exposure, 6),
|
||||
"gamma": round(rule.gamma, 6),
|
||||
"transparency": round(rule.transparency, 6),
|
||||
}
|
||||
for rule in props.rules
|
||||
]
|
||||
|
||||
|
||||
def deserialize_rules(props, data):
|
||||
props.rules.clear()
|
||||
for entry in data:
|
||||
rule = props.rules.add()
|
||||
rule.name = entry.get("name", "Rule")
|
||||
# Direct id-property writes bypass the update callback so we don't trigger a
|
||||
# live re-sync for every rule mid-load (the caller syncs once at the end).
|
||||
rule["exposure"] = float(entry.get("exposure", 0.0))
|
||||
rule["gamma"] = float(entry.get("gamma", 1.0))
|
||||
rule["transparency"] = float(entry.get("transparency", 0.0))
|
||||
if query := (entry.get("query") or ""):
|
||||
try:
|
||||
tool.Search.import_filter_query(query, rule.filter_groups)
|
||||
except Exception:
|
||||
pass # Tolerate an unparseable stored query rather than failing the load.
|
||||
props.active_rule_index = min(props.active_rule_index, max(len(props.rules) - 1, 0))
|
||||
|
||||
|
||||
def save_rules_to_ifc(camera):
|
||||
"""Mirror a drawing camera's rules into EPset_Drawing.RenderOverrides."""
|
||||
drawing = tool.Ifc.get_entity(camera)
|
||||
props = get_camera_override_props(camera)
|
||||
if not drawing or props is None:
|
||||
return
|
||||
data = serialize_rules(props)
|
||||
existing = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", PSET_PROP)
|
||||
if not data and existing is None:
|
||||
return # Nothing to store and nothing stored -- don't dirty unconfigured drawings.
|
||||
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
|
||||
if pset is None:
|
||||
return
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
tool.Ifc.get(), pset=pset, properties={PSET_PROP: json.dumps(data) if data else None}
|
||||
)
|
||||
|
||||
|
||||
def load_rules_from_ifc(camera):
|
||||
"""Populate a drawing camera's rules from EPset_Drawing.RenderOverrides."""
|
||||
drawing = tool.Ifc.get_entity(camera)
|
||||
props = get_camera_override_props(camera)
|
||||
if not drawing or props is None:
|
||||
return
|
||||
raw = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", PSET_PROP)
|
||||
if not raw:
|
||||
return
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
deserialize_rules(props, data)
|
||||
|
||||
|
||||
def ensure_rules_loaded(camera):
|
||||
"""Load rules from IFC only when the camera props are empty (i.e. freshly imported).
|
||||
|
||||
The empty guard means this never clobbers rules being edited in-session: once a
|
||||
drawing has rules in its props, IFC is no longer read back into them.
|
||||
"""
|
||||
props = get_camera_override_props(camera)
|
||||
if props is None or len(props.rules):
|
||||
return
|
||||
load_rules_from_ifc(camera)
|
||||
|
||||
|
||||
def drawing_cameras():
|
||||
"""Yield every camera object linked to an IFC drawing."""
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == "CAMERA" and tool.Ifc.get_entity(obj):
|
||||
yield obj
|
||||
|
||||
|
||||
# --- Apply / restore seam --------------------------------------------------
|
||||
# Two layers, so each effect lives where it can actually be shown:
|
||||
# * Live material layer (transparency): applied persistently while the toggle is
|
||||
# on, so it shows in the Rendered viewport (WYSIWYG). Driven by sync_live_effects.
|
||||
# * Render compositor layer (exposure/gamma): built just before a render and
|
||||
# removed just after, because the viewport compositor can't read Cryptomatte.
|
||||
# The same enable toggle drives both.
|
||||
|
||||
|
||||
def sync_live_effects(scene):
|
||||
"""Establish the correct live (viewport) material state for the active drawing.
|
||||
|
||||
Removes any previous temp materials, then -- if the toggle is on -- applies
|
||||
transparency for the active camera's rules. Idempotent; safe to call any time.
|
||||
"""
|
||||
restore_transparency()
|
||||
props = get_camera_override_props(scene.camera)
|
||||
if not props or not props.enabled:
|
||||
return
|
||||
for rule in props.rules:
|
||||
if rule.transparency > 0:
|
||||
objects = get_rule_objects(rule)
|
||||
if objects:
|
||||
apply_transparency(objects, rule.transparency)
|
||||
|
||||
|
||||
def clear_compositor(scene):
|
||||
"""Remove the override compositor nodes and reconnect Render Layers -> Composite.
|
||||
|
||||
Leaves live materials untouched (those are managed by sync_live_effects).
|
||||
"""
|
||||
if not (scene.use_nodes and scene.node_tree):
|
||||
return
|
||||
tree = scene.node_tree
|
||||
clear_marked_nodes(tree)
|
||||
rlayers = next((n for n in tree.nodes if n.bl_idname == "CompositorNodeRLayers"), None)
|
||||
composite = next((n for n in tree.nodes if n.bl_idname == "CompositorNodeComposite"), None)
|
||||
if rlayers and composite and not composite.inputs["Image"].links:
|
||||
tree.links.new(rlayers.outputs["Image"], composite.inputs["Image"])
|
||||
|
||||
|
||||
# --- Handlers --------------------------------------------------------------
|
||||
|
||||
|
||||
@persistent
|
||||
def render_init_handler(scene, *args):
|
||||
# Ensure live materials match the current rules, then add the render-only compositor.
|
||||
ensure_rules_loaded(scene.camera)
|
||||
sync_live_effects(scene)
|
||||
props = get_camera_override_props(scene.camera)
|
||||
if props and props.enabled and len(props.rules):
|
||||
build_compositor(scene, props)
|
||||
|
||||
|
||||
@persistent
|
||||
def render_end_handler(scene, *args):
|
||||
# Remove the compositor; live materials persist for continued viewport preview.
|
||||
clear_compositor(scene)
|
||||
|
||||
|
||||
# Switching the active drawing reassigns scene.camera, but no handler fires for that.
|
||||
# A msgbus subscription re-syncs the live materials so the previous drawing's
|
||||
# transparency is removed and the new drawing's applied. msgbus subscriptions are
|
||||
# cleared on file load, so we re-subscribe in load_post.
|
||||
_msgbus_owner = object()
|
||||
|
||||
|
||||
def _deferred_camera_sync():
|
||||
scene = bpy.context.scene
|
||||
props = get_camera_override_props(scene.camera if scene else None)
|
||||
# Activating a drawing that already has rules auto-enables its overrides.
|
||||
if props and len(props.rules) and not props.enabled:
|
||||
props.enabled = True # the update callback runs sync_live_effects
|
||||
else:
|
||||
sync_live_effects(scene)
|
||||
return None # run once
|
||||
|
||||
|
||||
def _on_active_camera_changed(*args):
|
||||
# Defer out of the msgbus notification context before touching material datablocks.
|
||||
if not bpy.app.timers.is_registered(_deferred_camera_sync):
|
||||
bpy.app.timers.register(_deferred_camera_sync, first_interval=0.0)
|
||||
|
||||
|
||||
def subscribe_camera_change():
|
||||
bpy.msgbus.clear_by_owner(_msgbus_owner)
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=(bpy.types.Scene, "camera"),
|
||||
owner=_msgbus_owner,
|
||||
args=(),
|
||||
notify=_on_active_camera_changed,
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post_handler(*args):
|
||||
# IFC is the source of truth: fill any empty drawing camera from its pset. (For a
|
||||
# .blend reopen the props already came from the file; for IFC import the cameras
|
||||
# don't exist yet here -- that case is covered by data.refresh / refresh_ui_data.)
|
||||
for camera in drawing_cameras():
|
||||
ensure_rules_loaded(camera)
|
||||
subscribe_camera_change() # msgbus is cleared on file load; re-subscribe
|
||||
# Files are saved without the temp materials (see save_pre), so re-apply on load.
|
||||
sync_live_effects(bpy.context.scene)
|
||||
|
||||
|
||||
@persistent
|
||||
def save_pre_handler(*args):
|
||||
# Never write the temporary transparent materials into the .blend.
|
||||
restore_transparency()
|
||||
# Flush each drawing's current rules into the IFC model before it is saved.
|
||||
for camera in drawing_cameras():
|
||||
save_rules_to_ifc(camera)
|
||||
|
||||
|
||||
@persistent
|
||||
def save_post_handler(*args):
|
||||
# Put the live preview back after the (clean) save.
|
||||
sync_live_effects(bpy.context.scene)
|
||||
|
||||
|
||||
class AddRenderOverrideRule(bpy.types.Operator):
|
||||
bl_idname = "bim.add_render_override_rule"
|
||||
bl_label = "Add Render Override Rule"
|
||||
bl_description = "Add a new selection + effects rule"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return get_camera_override_props(context.scene.camera) is not None
|
||||
|
||||
def execute(self, context):
|
||||
props = get_camera_override_props(context.scene.camera)
|
||||
rule = props.rules.add()
|
||||
rule.name = f"Rule {len(props.rules)}"
|
||||
props.active_rule_index = len(props.rules) - 1
|
||||
sync_live_effects(context.scene)
|
||||
save_rules_to_ifc(context.scene.camera)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveRenderOverrideRule(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_render_override_rule"
|
||||
bl_label = "Remove Render Override Rule"
|
||||
bl_description = "Remove the active selection + effects rule"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = get_camera_override_props(context.scene.camera)
|
||||
return bool(props and props.rules)
|
||||
|
||||
def execute(self, context):
|
||||
props = get_camera_override_props(context.scene.camera)
|
||||
props.rules.remove(props.active_rule_index)
|
||||
props.active_rule_index = min(props.active_rule_index, len(props.rules) - 1)
|
||||
sync_live_effects(context.scene)
|
||||
save_rules_to_ifc(context.scene.camera)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
_HANDLERS = (
|
||||
("render_init", "render_init_handler"),
|
||||
("render_complete", "render_end_handler"),
|
||||
("render_cancel", "render_end_handler"),
|
||||
("load_post", "load_post_handler"),
|
||||
("save_pre", "save_pre_handler"),
|
||||
("save_post", "save_post_handler"),
|
||||
)
|
||||
|
||||
|
||||
def register_handlers():
|
||||
unregister_handlers()
|
||||
for collection_name, func_name in _HANDLERS:
|
||||
getattr(bpy.app.handlers, collection_name).append(globals()[func_name])
|
||||
subscribe_camera_change()
|
||||
|
||||
|
||||
def unregister_handlers():
|
||||
for collection_name, func_name in _HANDLERS:
|
||||
collection = getattr(bpy.app.handlers, collection_name)
|
||||
func = globals()[func_name]
|
||||
if func in collection:
|
||||
collection.remove(func)
|
||||
bpy.msgbus.clear_by_owner(_msgbus_owner)
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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
|
||||
from bpy.props import BoolProperty, CollectionProperty, FloatProperty, IntProperty, StringProperty
|
||||
from bpy.types import PropertyGroup
|
||||
from bonsai.bim.module.search.prop import BIMFilterGroup
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
def update_live_preview(self, context):
|
||||
"""Re-sync the live (viewport) material preview when the toggle or a live effect changes."""
|
||||
# Lazy import avoids any import-order coupling with the operator module.
|
||||
from bonsai.bim.module.status_render import operator
|
||||
|
||||
if context.scene:
|
||||
operator.sync_live_effects(context.scene)
|
||||
|
||||
|
||||
class BIMRenderOverrideRule(PropertyGroup):
|
||||
"""One selection (filter) plus the render effects applied to it."""
|
||||
|
||||
name: StringProperty(name="Name", default="Rule")
|
||||
# Each rule has its own filter, resolved by tool.Search.get_filter_groups(f"status_render_{i}").
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
exposure: FloatProperty(
|
||||
name="Exposure",
|
||||
description="Extra exposure stops applied to matching elements, on top of the scene's "
|
||||
"colour management. 0 = no change",
|
||||
default=0.0,
|
||||
soft_min=-10.0,
|
||||
soft_max=10.0,
|
||||
)
|
||||
gamma: FloatProperty(
|
||||
name="Gamma",
|
||||
description="Extra gamma applied to matching elements. 1 = no change",
|
||||
default=1.0,
|
||||
min=0.001,
|
||||
soft_max=5.0,
|
||||
)
|
||||
transparency: FloatProperty(
|
||||
name="Transparency",
|
||||
description="Render the matching elements with transparent materials so the geometry "
|
||||
"behind them shows through. Shown live in the Rendered viewport while enabled. "
|
||||
"0 = opaque, 1 = fully transparent",
|
||||
default=0.0,
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
subtype="FACTOR",
|
||||
update=update_live_preview,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
exposure: float
|
||||
gamma: float
|
||||
transparency: float
|
||||
|
||||
|
||||
class BIMRenderOverrideProperties(PropertyGroup):
|
||||
"""Per-drawing render overrides. Stored on the camera datablock so each drawing
|
||||
carries its own rules (registered as ``Camera.BIMRenderOverrideProperties``)."""
|
||||
|
||||
enabled: BoolProperty(
|
||||
name="Enable Render Overrides",
|
||||
description="While enabled, transparency is shown live in the Rendered viewport, and all "
|
||||
"overrides are applied to renders that run the compositor (F12 and Default-render drawing "
|
||||
"underlays). Exposure/gamma are render-only (the viewport compositor can't mask them)",
|
||||
default=False,
|
||||
update=update_live_preview,
|
||||
)
|
||||
rules: CollectionProperty(type=BIMRenderOverrideRule, name="Rules")
|
||||
active_rule_index: IntProperty(name="Active Rule")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
enabled: bool
|
||||
rules: bpy.types.bpy_prop_collection_idprop[BIMRenderOverrideRule]
|
||||
active_rule_index: int
|
||||
@@ -0,0 +1,114 @@
|
||||
# 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 ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.helper
|
||||
from bonsai.bim.module.search.data import SearchData
|
||||
|
||||
|
||||
def get_applied_drawing_style(camera):
|
||||
"""The shading style currently applied to the drawing (EPset_Drawing.CurrentShadingStyle),
|
||||
which is what actually drives the render type -- not whichever style is selected in the list."""
|
||||
drawing = tool.Ifc.get_entity(camera)
|
||||
if not drawing:
|
||||
return None
|
||||
name = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "CurrentShadingStyle")
|
||||
if not name:
|
||||
return None
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
return next((style for style in dprops.drawing_styles if style.name == name), None)
|
||||
|
||||
|
||||
class BIM_UL_render_override_rules(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
layout.prop(item, "name", text="", emboss=False, icon="SHADERFX")
|
||||
|
||||
|
||||
class BIM_PT_status_render(bpy.types.Panel):
|
||||
bl_label = "Render Overrides"
|
||||
bl_idname = "BIM_PT_status_render"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_camera"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Same as the sibling drawing panels: only for an active IFC drawing camera.
|
||||
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
camera = context.scene.camera
|
||||
props = camera.data.BIMRenderOverrideProperties
|
||||
|
||||
# The override needs the compositor, which only runs for Default-render drawings
|
||||
# (and F12). Gate on the *applied* shading style (CurrentShadingStyle), since that
|
||||
# is what drives the render -- not whichever style is highlighted in the list.
|
||||
applied_style = get_applied_drawing_style(camera)
|
||||
blocked = applied_style is not None and applied_style.render_type != "DEFAULT"
|
||||
|
||||
if blocked:
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Current Shading Style is not 'Default'", icon="ERROR")
|
||||
col.label(text="render type, so the compositor is bypassed.")
|
||||
col.label(text="Apply a Default-render shading style.")
|
||||
|
||||
header = layout.column()
|
||||
header.enabled = not blocked
|
||||
header.prop(props, "enabled", toggle=True, icon="RENDER_RESULT")
|
||||
|
||||
# When the compositor is bypassed, the rules can't do anything -- hide them so
|
||||
# only the greyed toggle and the explanation remain.
|
||||
if blocked:
|
||||
return
|
||||
|
||||
row = layout.row()
|
||||
row.template_list(
|
||||
"BIM_UL_render_override_rules", "", props, "rules", props, "active_rule_index", rows=3
|
||||
)
|
||||
col = row.column(align=True)
|
||||
col.operator("bim.add_render_override_rule", icon="ADD", text="")
|
||||
col.operator("bim.remove_render_override_rule", icon="REMOVE", text="")
|
||||
|
||||
if 0 <= props.active_rule_index < len(props.rules):
|
||||
rule = props.rules[props.active_rule_index]
|
||||
box = layout.box()
|
||||
box.active = props.enabled
|
||||
box.prop(rule, "name")
|
||||
|
||||
# Per-rule filter (same UI as bim.search), keyed to this rule's index.
|
||||
bonsai.bim.helper.draw_filter(
|
||||
box, rule.filter_groups, SearchData, f"status_render_{props.active_rule_index}"
|
||||
)
|
||||
|
||||
col = box.column(align=True)
|
||||
col.label(text="Effects:")
|
||||
col.prop(rule, "exposure")
|
||||
col.prop(rule, "gamma")
|
||||
col.prop(rule, "transparency")
|
||||
|
||||
if not blocked:
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
col.label(text="Applied automatically during F12 and", icon="INFO")
|
||||
col.label(text="Default-render drawing underlays, then")
|
||||
col.label(text="removed. The drawing needs an underlay.")
|
||||
@@ -85,6 +85,10 @@ class Search(bonsai.core.tool.Search):
|
||||
def get_filter_groups(cls, module: FilterModule) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]:
|
||||
if module == "search":
|
||||
return cls.get_search_props().filter_groups
|
||||
elif module.startswith("status_render_"):
|
||||
index = int(module.rsplit("_", 1)[1])
|
||||
assert (scene := bpy.context.scene) and (camera := scene.camera)
|
||||
return camera.data.BIMRenderOverrideProperties.rules[index].filter_groups
|
||||
elif module == "csv":
|
||||
return tool.Blender.get_csv_props().filter_groups
|
||||
elif module == "diff":
|
||||
@@ -101,11 +105,23 @@ class Search(bonsai.core.tool.Search):
|
||||
return getattr(props.clash_sets[int(clash_set_index)], ab)[int(clash_source_index)].filter_groups
|
||||
assert False, f"Unsupported module: {module}"
|
||||
|
||||
@classmethod
|
||||
def on_filter_query_edited(cls, module: str, context: bpy.types.Context) -> None:
|
||||
"""Notify the owning module that its filter query was just edited (via
|
||||
bim.edit_filter_query or bim.apply_filter_from_text), so it can react -- e.g.
|
||||
refresh a live viewport preview."""
|
||||
if module.startswith("status_render"):
|
||||
from bonsai.bim.module.status_render import operator
|
||||
|
||||
operator.sync_live_effects(context.scene)
|
||||
|
||||
@classmethod
|
||||
def import_filter_query(
|
||||
cls, query: str, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
) -> None:
|
||||
filter_groups.clear()
|
||||
if not query.strip():
|
||||
return # An empty query means "no filter"; clearing the groups is enough.
|
||||
transformer = ImportFilterQueryTransformer(filter_groups)
|
||||
transformer.transform(ifcopenshell.util.selector.filter_elements_grammar.parse(query))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user