iTwin / itwinjs-core

Monorepo for iTwin.js Library
https://www.itwinjs.org
MIT License
583 stars 211 forks source link

Display properties of 3d element when 2d element in sheet is selected #5466

Closed ezmobivietnam closed 1 year ago

ezmobivietnam commented 1 year ago

Describe the bug The EmphasizeElements APIs (override, isolate, hide, and emphasize) are not working on a sheet model but they work on the drawing model.

To Reproduce Steps to reproduce the behavior:

  1. Go to the sample 'Emphasize Elements - 2D Viewer Widget'
  2. Open the tab "2D View Selector" -> select a Drawing model (e.g. 'Plan-First Floor') -> then select an element on that drawing model.
  3. Open the tab "Emphasize Element Selector", Click on the Apply buttons below the labels 'Emphasize', 'Hide', 'Isolate', 'Override'
  4. Observe that the selected element was emphasized/hidden/isolated/overridden successfully.
  5. Repeat step 2, open the tab "2D View Selector" -> select a Sheet model (e.g. Floor Plan [Sheet]) -> then select the same element on that sheet model.
  6. Open the tab "Emphasize Element Selector", Click on the Apply buttons below the labels 'Emphasize', 'Hide', 'Isolate', and 'Override'.
  7. Observe that the selected element was NOT emphasized/hidden/isolated/overridden.

Expected behavior The selected elements on the sheet model should be emphasized/hidden/isolated/overridden.

Sandbox https://www.itwinjs.org/sandboxes/Thanh%20DinhDuong/Emphasize%20Elements%20-%202D%20Viewer%20Widget

Desktop (please complete the applicable information):

pmconne commented 1 year ago

Sheets display drawings and spatial views via view attachments. A view attachment is like a snapshot image that is set up when deciding how the view should look when displayed on the sheet. Each attachment specifies its own view, and each view has its own category selector, subcategory overrides, view flags, etc which define the symbology overrides to apply when drawing that view onto the sheet. If the sheet view's own symbology overrides were applied to the attached views, then the attached views would not render properly - e.g., turning off a category or constructions in the sheet view would turn them off in the attached views as well.

Please explain the goal - i.e., user-facing feature/behavior, not a technical implementation detail - that you are trying to achieve so we can determine an appropriate solution.

ezmobivietnam commented 1 year ago

The use case would be below: On the Civil frontstage, the user would only want to see the sheets, not the drawings. So the drawings would not be displayed on the Civil frontstage. And when the user opens a sheet in the second view to review the design and selects an element on the sheet, he/she would like to see the 3D representation properties of the selected 2D element instead of seeing the 2D properties as current.

So a solution for this use case is: when the user selects a 2D element, the app will try to look up its 3D representation element which is displaying on a 3D view (the main view/first view), and select that 3D element then. As a result, the properties of the 3D element will be displayed on the Property widget accordingly. However, when the 3D element on the main view was selected, because the unified selection is enabled, the current selection on the sheet view was lost so he/she cannot know which one is being selected. To resolve this issue, we would like to override/change the default color of the selected 2D element from black to another color such as RED so that the user could be aware of which 2D element has just been selected.

Another use case is to sync a 2D sheet model with a 3D model. When a certain condition meets, some 2D elements in the sheet could be hidden intentionally.

grigasp commented 1 year ago

when the user selects a 2D element, the app will try to look up its 3D representation element which is displaying on a 3D view (the main view/first view), and select that 3D element then. As a result, the properties of the 3D element will be displayed on the Property widget accordingly.

There's an option to tell the property grid to load properties of the related 3d element instead of the selected 2d one. Let me know if that works for you and you need guidance on how to set that up.

simnorm commented 1 year ago

@grigasp I believe that Binh (@ezmobivietnam) had already tried that, but it would select the element in the Drawing model, not the element from the 3D model. Could you confirm Binh?

grigasp commented 1 year ago

@grigasp I believe that Binh (@ezmobivietnam) had already tried that, but it would select the element in the Drawing model, not the element from the 3D model. Could you confirm Binh?

Do you need 3d element to be selected or to see it's properties in the property grid? Or both?

ezmobivietnam commented 1 year ago

Hi @grigasp , Thanks so much for your help!

I need the 2D element to be selected and the properties of its 3D representation element will be displayed on the property grid/widget.

E.g. What I have been trying to do is when the user selects a 2D element in the sheet view then the app will display the properties of the 3D representation element that is associated with the selected 2D element on the properties widget. And the selection/focus still stays on the sheet view (not move to the 3D view).

Below is what I have tried to do to fulfill the requirement above.

When an element of a sheet is selected, I will convert and add its 3D representation ID to the selection set of the imodel. As a result, the properties of the selected 3D element will be displayed on the Property widget. However, I led to the two issues below:

  1. If I only add the 3D representation ID of the selected 2D element to the imodel's selection set then the highlight on the selected 2D element will be lost. In this case, the user would NOT know which element he/she has just selected.
  2. If I add both the 2D element ID and its 3D representation ID to the imodel's selection set then the highlight on the selected 2D element still remains but the Property widget will try to display the properties of both two elements with the values "Varies".

Thanks!

pmconne commented 1 year ago

I updated the defect title and removed the display label. This problem sounds like it should be solved using the presentation system, not by changing the selection set under the hood and trying to fake a hilite effect on the no-longer-selected 2d element.

ezmobivietnam commented 1 year ago

@pmconne , @grigasp , how would I resolve my issue by using the presentation system? Please give me a hint. I still don't have any ideas at the moment. Thanks!

pmconne commented 1 year ago

I can't answer that. Hopefully @grigasp can. Please show the code you used when you "already tried that" as @simnorm mentioned.

ezmobivietnam commented 1 year ago

Here is a piece of the code: export class SheetViewerSelectionTool extends SelectionTool { public static override toolId = "SheetSelectionTool";

// A map from 2D element ID (key) to 3D element ID (value) private _elementIdMap = new Map<string, string>();

public static override startTool(): Promise { const tool = new SheetViewerSelectionTool(); return tool.run(); }

public async onRestartTool(): Promise { const tool = new SheetViewerSelectionTool(); if (!(await tool.run())) { await this.exitTool(); } }

public override async onPostInstall(): Promise { await super.onPostInstall(); this.changeLocateState(true, false, "default"); }

public override async onCleanup(): Promise { this._elementIdMap.clear(); return super.onCleanup(); }

public override async onDataButtonUp( ev: BeButtonEvent ): Promise { const eventHandled = await super.onDataButtonUp(ev);

const hit = await IModelApp.locateManager.doLocate(
  new LocateResponse(),
  true,
  ev.point,
  ev.viewport,
  ev.inputSource
);

if (hit && hit.isElementHit && ev.viewport?.view.is2d()) {
  const element2dId = hit.sourceId;
  this.overrideElements(element2dId, ev.viewport);
}

return eventHandled;

}

// override this function to convert the selection id from 2d to 3d while // trying to minimize the change to the original code. public async processSelection( elementId: Id64Arg, process: SelectionProcessing ): Promise { const converted3dIdSet = new Set(); for (const id of Id64.iterable(elementId)) { const converted3dId = this._elementIdMap.get(id) ?? (await convert2dElementId(id, this.iModel)); if (converted3dId) { converted3dIdSet.add(converted3dId); this._elementIdMap.set(id, converted3dId); } }

if (!converted3dIdSet.size) {
  return false;
}

return super.updateSelection(converted3dIdSet, process);

}

private overrideElements(elementIds: Id64Arg, vp: Viewport) { const emphasizer = EmphasizeElements.getOrCreate(vp); emphasizer.clearOverriddenElements(vp); emphasizer.overrideElements( elementIds, vp, ColorDef.blue, FeatureOverrideType.ColorOnly, true ); } }

pmconne commented 1 year ago

@grigasp said:

There's an option to tell the property grid to load properties of the related 3d element instead of the selected 2d one. Let me know if that works for you and you need guidance on how to set that up.

to which @simnorm replied:

I believe that Binh (@ezmobivietnam) had already tried that, but it would select the element in the Drawing model, not the element from the 3D model. Could you confirm Binh?

@ezmobivietnam the code you provided appears to implement your workaround. I am not interested in that - I want to see the code you used to try to "tell the property grid to load properties of the related 3d element instead of the selected 2d one".

ezmobivietnam commented 1 year ago

@grigasp , Besides the workaround solution I shared above (using the selection set), I don't know that there is an option to tell the property grid to load properties of the related 3d element instead of the selected 2d one. Please teach me. Thanks!

grigasp commented 1 year ago

The property grid uses presentation rules to load the properties. The default presentation rules used by the property grid tells it to load properties of selected element(s).

What you need is to supply an additional content rule that tells is to load related (3d) element properties when a 2d element is selected. The rule should use the ContentRelatedInstances specification and should either be specified before the default content rule in the rules list or have priority attribute above 1000.

Creating the ruleset could look like this:

import { Ruleset } from "@itwin/presentation-common";
import { DEFAULT_PROPERTY_GRID_RULESET } from "@itwin/presentation-components";

const myRuleset: Ruleset = {
  ...DEFAULT_PROPERTY_GRID_RULESET,
  rules: [
    {
      ruleType: "Content",
      condition: `SelectedNode.IsOfClass("2d_element_class_name", "2d_element_schema_name")`,
      specifications: [{
        specType: "ContentRelatedInstances",
        relationshipPaths: [{
          relationship: { 
            schemaName: "schema_name_of_relationship_between_2d_and_3d_elements", 
            className: "class_name_of_relationship_between_2d_and_3d_elements",
          },
          direction: "Forward", // or "Backward", depending on the relationship
        }],
      }],
    },
    ...DEFAULT_PROPERTY_GRID_RULESET.rules,
  ],
}

The last puzzle piece is getting property grid to use those rules and it depends on what component you use to create the property grid:

HTH

ezmobivietnam commented 1 year ago

@grigasp , thanks for the details guidelines. Much appreciated! :-)

ezmobivietnam commented 1 year ago

Hi @grigasp , we are using @itwin/property-grid-react package, PropertyGridUiItemsProvider, and PropertyGridProps in our app.

Following your guideline, I was able to create a custom ruleset and it worked well. Thanks so much!

I am on the next step of making this custom ruleset work only on our Civil frontstage to avoid the impact on the existing functionalities of the app. Below are the steps I have taken:

And I got an issue: The sheet viewer widget on the civil frontstage keeps using the default ruleset of the old PropertyGridUiItemsProvider, which uses the id PropertyGridUiItemsProvider, for displaying data on the property grid and the new ruleset seems not be used.

I am not sure if the steps I did above (especially cheating the code in step 2) are correct or not. do you have any idea about it?

Thanks!

grigasp commented 1 year ago

I'm not familiar with appui enough to guess why it might not work. Adding @GerardasB who may be able to help.

Create a new instance of PropertyGridUiItemsProvider, and change its default id from PropertyGridUiItemsProvider to PropertyGridRulesetUiItemsProvider. Note: I did cheat the code to be able to change the value of the readonly property id.

I don't think you need to do that. Instead, you can do this:

class MyUiItemsProvider implements UiItemsProvider {
  public readonly id = "MyUiItemsProvider";
  private _propertyGridProvider: PropertyGridUiItemsProvider;
  constructor(props: PropertyGridUiItemsProviderProps = {}) {
    this._propertyGridProvider = new PropertyGridUiItemsProvider(props);
  }

  public provideWidgets(stageId: string, stageUsage: string, location: StagePanelLocation, section?: StagePanelSection): ReadonlyArray<Widget> {
    return this._propertyGridProvider.provideWidgets(stageId, stageUsage, location, section);
  }
}
ezmobivietnam commented 1 year ago

Hi @grigasp and @pmconne , since I have succeeded in applying a customized ruleset to the app following your guideline so I will close this ticket here. Thanks so much!

Binh