@itwin/core-frontend
Version:
iTwin.js frontend components
991 lines • 91 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Tools
*/
import { BeEvent, Id64 } from "@itwin/core-bentley";
import { AxisOrder, ClipMaskXYZRangePlanes, ClipPlane, ClipPrimitive, ClipShape, ClipUtilities, ClipVector, ConvexClipPlaneSet, FrameBuilder, Geometry, GrowableXYZArray, LineString3d, Loop, Matrix3d, Path, Plane3dByOriginAndUnitNormal, Point3d, PolygonOps, PolylineOps, Range1d, Range3d, Ray3d, Transform, Vector3d, } from "@itwin/core-geometry";
import { ClipStyle, ColorDef, LinePixels, Placement2d } from "@itwin/core-common";
import { AccuDrawHintBuilder, ContextRotationId } from "../AccuDraw";
import { CoordSystem } from "../CoordSystem";
import { LocateResponse } from "../ElementLocateManager";
import { IModelApp } from "../IModelApp";
import { EditManipulator } from "./EditManipulator";
import { PrimitiveTool } from "./PrimitiveTool";
import { BeButtonEvent, CoordinateLockOverrides, CoreTools, EventHandled } from "./Tool";
import { ToolAssistance, ToolAssistanceImage, ToolAssistanceInputMethod } from "./ToolAssistance";
import { GraphicType } from "../common/render/GraphicType";
/** A tool to define a clip volume for a view
* @public @extensions
*/
export class ViewClipTool extends PrimitiveTool {
_clipEventHandler;
constructor(_clipEventHandler) {
super();
this._clipEventHandler = _clipEventHandler;
}
/** @internal */
static _orientationName = "enumAsOrientation";
/** @internal */
static enumAsOrientationMessage(str) { return CoreTools.translate(`Settings.Orientation.${str}`); }
/** @internal */
static _getEnumAsOrientationDescription = () => {
return {
name: ViewClipTool._orientationName,
displayLabel: CoreTools.translate("Settings.Orientation.Label"),
typename: "enum",
enum: {
choices: [
{ label: ViewClipTool.enumAsOrientationMessage("Top"), value: ContextRotationId.Top },
{ label: ViewClipTool.enumAsOrientationMessage("Front"), value: ContextRotationId.Front },
{ label: ViewClipTool.enumAsOrientationMessage("Left"), value: ContextRotationId.Left },
{ label: ViewClipTool.enumAsOrientationMessage("Bottom"), value: ContextRotationId.Bottom },
{ label: ViewClipTool.enumAsOrientationMessage("Back"), value: ContextRotationId.Back },
{ label: ViewClipTool.enumAsOrientationMessage("Right"), value: ContextRotationId.Right },
{ label: ViewClipTool.enumAsOrientationMessage("View"), value: ContextRotationId.View },
{ label: ViewClipTool.enumAsOrientationMessage("Face"), value: ContextRotationId.Face },
],
},
};
};
/** @internal */
requireWriteableTarget() { return false; }
/** @internal */
isCompatibleViewport(vp, isSelectedViewChange) { return (super.isCompatibleViewport(vp, isSelectedViewChange) && undefined !== vp && vp.view.allow3dManipulations()); }
/** @internal */
async onPostInstall() {
await super.onPostInstall();
this.setupAndPromptForNextAction();
}
/** @internal */
async onUnsuspend() { this.showPrompt(); }
/** @internal */
async onRestartTool() { return this.exitTool(); }
/** @internal */
showPrompt() { }
/** @internal */
setupAndPromptForNextAction() { this.showPrompt(); }
/** @internal */
async onResetButtonUp(_ev) {
await this.onReinitialize();
return EventHandled.No;
}
/** @internal */
static getPlaneInwardNormal(orientation, viewport) {
const matrix = AccuDrawHintBuilder.getContextRotation(orientation, viewport);
if (undefined === matrix)
return undefined;
return matrix.getColumn(2).negate();
}
static enableClipVolume(viewport) {
if (viewport.viewFlags.clipVolume)
return false;
viewport.viewFlags = viewport.viewFlags.with("clipVolume", true);
return true;
}
static setViewClip(viewport, clip) {
viewport.view.setViewClip(clip);
viewport.setupFromView();
return true;
}
static doClipToConvexClipPlaneSet(viewport, planes) {
const prim = ClipPrimitive.createCapture(planes);
const clip = ClipVector.createEmpty();
clip.appendReference(prim);
return this.setViewClip(viewport, clip);
}
static doClipToPlane(viewport, origin, normal, clearExistingPlanes) {
const plane = Plane3dByOriginAndUnitNormal.create(origin, normal);
if (undefined === plane)
return false;
let planeSet;
if (!clearExistingPlanes) {
const existingClip = viewport.view.getViewClip();
if (undefined !== existingClip && 1 === existingClip.clips.length) {
const existingPrim = existingClip.clips[0];
if (!(existingPrim instanceof ClipShape)) {
const existingPlaneSets = existingPrim.fetchClipPlanesRef();
if (undefined !== existingPlaneSets && 1 === existingPlaneSets.convexSets.length)
planeSet = existingPlaneSets.convexSets[0];
}
}
}
if (undefined === planeSet)
planeSet = ConvexClipPlaneSet.createEmpty();
planeSet.addPlaneToConvexSet(ClipPlane.createPlane(plane));
return this.doClipToConvexClipPlaneSet(viewport, planeSet);
}
static doClipToShape(viewport, xyPoints, transform, zLow, zHigh) {
const clip = ClipVector.createEmpty();
clip.appendShape(xyPoints, zLow, zHigh, transform);
return this.setViewClip(viewport, clip);
}
static doClipToRange(viewport, range, transform) {
if (range.isNull || range.isAlmostZeroX || range.isAlmostZeroY)
return false;
const clip = ClipVector.createEmpty();
const block = ClipShape.createBlock(range, range.isAlmostZeroZ ? ClipMaskXYZRangePlanes.XAndY : ClipMaskXYZRangePlanes.All, false, false, transform);
clip.appendReference(block);
return this.setViewClip(viewport, clip);
}
static doClipClear(viewport) {
if (!ViewClipTool.hasClip(viewport))
return false;
return this.setViewClip(viewport);
}
/** @internal */
static getClipRayTransformed(origin, direction, transform) {
const facePt = origin.clone();
const faceDir = direction.clone();
if (undefined !== transform) {
transform.multiplyPoint3d(facePt, facePt);
transform.multiplyVector(faceDir, faceDir);
faceDir.normalizeInPlace();
}
return Ray3d.createCapture(facePt, faceDir);
}
/** @internal */
static getOffsetValueTransformed(offset, transform) {
if (undefined === transform)
return offset;
const lengthVec = Vector3d.create(offset);
transform.multiplyVector(lengthVec, lengthVec);
const localOffset = lengthVec.magnitude();
return (offset < 0 ? -localOffset : localOffset);
}
/** @internal */
static addClipPlanesLoops(builder, loops, outline) {
for (const geom of loops) {
if (!(geom instanceof Loop))
continue;
if (outline)
builder.addPath(Path.createArray(geom.children));
else
builder.addLoop(geom);
}
}
/** @internal */
static addClipShape(builder, shape, extents) {
const shapePtsLo = ViewClipTool.getClipShapePoints(shape, extents.low);
const shapePtsHi = ViewClipTool.getClipShapePoints(shape, extents.high);
for (let i = 0; i < shapePtsLo.length; i++)
builder.addLineString([shapePtsLo[i].clone(), shapePtsHi[i].clone()]);
builder.addLineString(shapePtsLo);
builder.addLineString(shapePtsHi);
}
/** @internal */
static drawClip(context, clip, viewExtents, options) {
const clipShape = ViewClipTool.isSingleClipShape(clip);
const clipPlanes = (undefined === clipShape ? ViewClipTool.isSingleConvexClipPlaneSet(clip) : undefined);
if (undefined === clipShape && undefined === clipPlanes)
return;
const viewRange = (viewExtents ? viewExtents : context.viewport.computeViewRange());
const clipPlanesLoops = (undefined !== clipPlanes ? ClipUtilities.loopsOfConvexClipPlaneIntersectionWithRange(clipPlanes, viewRange) : undefined);
if (undefined === clipShape && (undefined === clipPlanesLoops || 0 === clipPlanesLoops.length))
return;
const color = (options && options.color ? options.color : EditManipulator.HandleUtils.adjustForBackgroundColor(ColorDef.white, context.viewport));
const builderVis = context.createGraphicBuilder(GraphicType.WorldDecoration, clipShape ? clipShape.transformFromClip : undefined, (options ? options.id : undefined));
const builderHid = context.createGraphicBuilder(GraphicType.WorldOverlay, clipShape ? clipShape.transformFromClip : undefined);
builderVis.setSymbology(color, ColorDef.black, (options && options.visibleWidth ? options.visibleWidth : 3));
builderHid.setSymbology(color, ColorDef.black, (options && options.hiddenWidth ? options.hiddenWidth : 1), (options && options.hiddenStyle ? options.hiddenStyle : LinePixels.Code2));
if (undefined !== clipPlanesLoops) {
ViewClipTool.addClipPlanesLoops(builderVis, clipPlanesLoops, true);
ViewClipTool.addClipPlanesLoops(builderHid, clipPlanesLoops, true);
if (options && options.fillClipPlanes) {
const fill = (options.fill ? options.fill : EditManipulator.HandleUtils.adjustForBackgroundColor(ColorDef.from(0, 255, 255, 225), context.viewport));
const builderFill = context.createGraphicBuilder(GraphicType.WorldDecoration);
builderFill.setSymbology(fill, fill, 0);
ViewClipTool.addClipPlanesLoops(builderFill, (options.hasPrimaryPlane ? [clipPlanesLoops[0]] : clipPlanesLoops), false);
context.addDecorationFromBuilder(builderFill);
}
}
else if (undefined !== clipShape) {
const clipExtents = ViewClipTool.getClipShapeExtents(clipShape, viewRange);
ViewClipTool.addClipShape(builderVis, clipShape, clipExtents);
ViewClipTool.addClipShape(builderHid, clipShape, clipExtents);
}
context.addDecorationFromBuilder(builderVis);
context.addDecorationFromBuilder(builderHid);
}
static isHilited(vp, id) {
return (undefined !== id ? vp.iModel.hilited.elements.has(Id64.getLowerUint32(id), Id64.getUpperUint32(id)) : false);
}
static isFlashed(vp, id) {
return (undefined !== id ? vp.lastFlashedElementId === id : false);
}
static drawClipShape(context, shape, extents, color, weight, id) {
const builder = context.createGraphicBuilder(GraphicType.WorldDecoration, shape.transformFromClip, id); // Use WorldDecoration not WorldOverlay to make sure handles have priority...
builder.setSymbology(color, ColorDef.black, weight);
ViewClipTool.addClipShape(builder, shape, extents);
context.addDecorationFromBuilder(builder);
// NOTE: We want to display hidden edges when clip decoration isn't hilited (not selected or drawn in dynamics).
// This isn't required and is messy looking when the clip is being drawn hilited.
// If the clip decoration is being flashed, draw using the hilite color to match the pickable world decoration display.
if (!this.isHilited(context.viewport, id)) {
const builderHid = context.createGraphicBuilder(GraphicType.WorldOverlay, shape.transformFromClip);
builderHid.setSymbology(this.isFlashed(context.viewport, id) ? context.viewport.hilite.color : color, ColorDef.black, 1, LinePixels.Code2);
ViewClipTool.addClipShape(builderHid, shape, extents);
context.addDecorationFromBuilder(builderHid);
}
}
/** @internal */
static getClipShapePoints(shape, z) {
const points = [];
for (const pt of shape.polygon)
points.push(Point3d.create(pt.x, pt.y, z));
return points;
}
/** @internal */
static getClipShapeExtents(shape, viewRange) {
let zLow = shape.zLow;
let zHigh = shape.zHigh;
if (undefined === zLow || undefined === zHigh) {
const zVec = Vector3d.unitZ();
const origin = shape.polygon[0];
const corners = viewRange.corners();
if (undefined !== shape.transformToClip)
shape.transformToClip.multiplyPoint3dArrayInPlace(corners);
for (const corner of corners) {
const delta = Vector3d.createStartEnd(origin, corner);
const projection = delta.dotProduct(zVec);
if (undefined === shape.zLow && (undefined === zLow || projection < zLow))
zLow = projection;
if (undefined === shape.zHigh && (undefined === zHigh || projection > zHigh))
zHigh = projection;
}
}
if (undefined === zLow || undefined === zHigh)
return Range1d.createNull();
return Range1d.createXX(zLow, zHigh);
}
/** @internal */
static isSingleClipShape(clip) {
if (1 !== clip.clips.length)
return undefined;
const prim = clip.clips[0];
if (!(prim instanceof ClipShape))
return undefined;
if (!prim.isValidPolygon)
return undefined;
return prim;
}
static drawClipPlanesLoops(context, loops, color, weight, dashed, fill, id) {
if (loops.length < 1)
return;
const builderEdge = context.createGraphicBuilder(GraphicType.WorldDecoration, undefined, id); // Use WorldDecoration not WorldOverlay to make sure handles have priority...
builderEdge.setSymbology(color, ColorDef.black, weight, dashed ? LinePixels.Code2 : undefined);
ViewClipTool.addClipPlanesLoops(builderEdge, loops, true);
context.addDecorationFromBuilder(builderEdge);
// NOTE: We want to display hidden edges when clip decoration isn't hilited (not selected or drawn in dynamics).
// This isn't required and is messy looking when the clip is being drawn hilited.
// If the clip decoration is being flashed, draw using the hilite color to match the pickable world decoration display.
if (!this.isHilited(context.viewport, id)) {
const builderEdgeHid = context.createGraphicBuilder(GraphicType.WorldOverlay);
builderEdgeHid.setSymbology(this.isFlashed(context.viewport, id) ? context.viewport.hilite.color : color, ColorDef.black, 1, LinePixels.Code2);
ViewClipTool.addClipPlanesLoops(builderEdgeHid, loops, true);
context.addDecorationFromBuilder(builderEdgeHid);
}
if (undefined === fill)
return;
const builderFace = context.createGraphicBuilder(GraphicType.WorldDecoration, undefined);
builderFace.setSymbology(fill, fill, 0);
ViewClipTool.addClipPlanesLoops(builderFace, loops, false);
context.addDecorationFromBuilder(builderFace);
}
/** @internal */
static isSingleConvexClipPlaneSet(clip) {
if (1 !== clip.clips.length)
return undefined;
const prim = clip.clips[0];
if (prim instanceof ClipShape)
return undefined;
const planeSets = prim.fetchClipPlanesRef();
return (undefined !== planeSets && 1 === planeSets.convexSets.length) ? planeSets.convexSets[0] : undefined;
}
/** @internal */
static isSingleClipPlane(clip) {
const clipPlanes = ViewClipTool.isSingleConvexClipPlaneSet(clip);
if (undefined === clipPlanes || 1 !== clipPlanes.planes.length)
return undefined;
return clipPlanes.planes[0];
}
static areClipsEqual(clipA, clipB) {
if (clipA === clipB)
return true;
if (clipA.clips.length !== clipB.clips.length)
return false;
for (let iPrim = 0; iPrim < clipA.clips.length; iPrim++) {
const primA = clipA.clips[iPrim];
const primB = clipB.clips[iPrim];
const planesA = primA.fetchClipPlanesRef();
const planesB = primB.fetchClipPlanesRef();
if (undefined !== planesA && undefined !== planesB) {
if (planesA.convexSets.length !== planesB.convexSets.length)
return false;
for (let iPlane = 0; iPlane < planesA.convexSets.length; iPlane++) {
const planeSetA = planesA.convexSets[iPlane];
const planeSetB = planesB.convexSets[iPlane];
if (planeSetA.planes.length !== planeSetB.planes.length)
return false;
for (let iClipPlane = 0; iClipPlane < planeSetA.planes.length; iClipPlane++) {
const planeA = planeSetA.planes[iClipPlane];
const planeB = planeSetB.planes[iClipPlane];
if (!planeA.isAlmostEqual(planeB))
return false;
}
}
}
else if (undefined === planesA && undefined === planesB) {
continue;
}
else {
return false;
}
}
return true;
}
static hasClip(viewport) {
return (undefined !== viewport.view.getViewClip());
}
}
/** A tool to remove a clip volume for a view
* @public @extensions
*/
export class ViewClipClearTool extends ViewClipTool {
static toolId = "ViewClip.Clear";
static iconSpec = "icon-section-tool";
/** @internal */
isCompatibleViewport(vp, isSelectedViewChange) { return (super.isCompatibleViewport(vp, isSelectedViewChange) && undefined !== vp && ViewClipTool.hasClip(vp)); }
/** @internal */
showPrompt() {
const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, CoreTools.translate("ViewClip.Clear.Prompts.FirstPoint"));
IModelApp.notifications.setToolAssistance(ToolAssistance.createInstructions(mainInstruction));
}
async doClipClear(viewport) {
if (!ViewClipTool.doClipClear(viewport))
return false;
if (undefined !== this._clipEventHandler)
this._clipEventHandler.onClearClip(viewport);
await this.onReinitialize();
return true;
}
/** @internal */
async onPostInstall() {
await super.onPostInstall();
if (undefined !== this.targetView)
await this.doClipClear(this.targetView);
}
/** @internal */
async onDataButtonDown(_ev) {
if (undefined === this.targetView)
return EventHandled.No;
return await this.doClipClear(this.targetView) ? EventHandled.Yes : EventHandled.No;
}
}
/** A tool to define a clip volume for a view by specifying a plane
* @public
*/
export class ViewClipByPlaneTool extends ViewClipTool {
_clearExistingPlanes;
static toolId = "ViewClip.ByPlane";
static iconSpec = "icon-section-plane";
/** @internal */
_orientationValue = { value: ContextRotationId.Face };
constructor(clipEventHandler, _clearExistingPlanes = false) {
super(clipEventHandler);
this._clearExistingPlanes = _clearExistingPlanes;
}
/** @internal */
get orientation() { return this._orientationValue.value; }
set orientation(option) { this._orientationValue.value = option; }
/** @internal */
supplyToolSettingsProperties() {
const initialValue = IModelApp.toolAdmin.toolSettingsState.getInitialToolSettingValue(this.toolId, ViewClipTool._orientationName);
initialValue && (this._orientationValue = initialValue);
const toolSettings = new Array();
const settingsItem = { value: this._orientationValue, property: ViewClipTool._getEnumAsOrientationDescription(), editorPosition: { rowPriority: 0, columnIndex: 2 } };
toolSettings.push(settingsItem);
return toolSettings;
}
/** @internal */
async applyToolSettingPropertyChange(updatedValue) {
if (updatedValue.propertyName === ViewClipTool._orientationName) {
this._orientationValue = updatedValue.value;
if (this._orientationValue) {
IModelApp.toolAdmin.toolSettingsState.saveToolSettingProperty(this.toolId, { propertyName: ViewClipTool._orientationName, value: this._orientationValue });
return true;
}
}
return false;
}
/** @internal */
showPrompt() {
const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, CoreTools.translate("ViewClip.ByPlane.Prompts.FirstPoint"));
const mouseInstructions = [];
const touchInstructions = [];
if (!ToolAssistance.createTouchCursorInstructions(touchInstructions))
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, CoreTools.translate("ElementSet.Inputs.AcceptPoint"), false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClick, CoreTools.translate("ElementSet.Inputs.AcceptPoint"), false, ToolAssistanceInputMethod.Mouse));
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.TwoTouchTap, CoreTools.translate("ElementSet.Inputs.Exit"), false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.RightClick, CoreTools.translate("ElementSet.Inputs.Exit"), false, ToolAssistanceInputMethod.Mouse));
const sections = [];
sections.push(ToolAssistance.createSection(mouseInstructions, ToolAssistance.inputsLabel));
sections.push(ToolAssistance.createSection(touchInstructions, ToolAssistance.inputsLabel));
const instructions = ToolAssistance.createInstructions(mainInstruction, sections);
IModelApp.notifications.setToolAssistance(instructions);
}
/** @internal */
setupAndPromptForNextAction() {
IModelApp.accuSnap.enableSnap(true);
super.setupAndPromptForNextAction();
}
/** @internal */
async onDataButtonDown(ev) {
if (undefined === this.targetView)
return EventHandled.No;
const normal = ViewClipTool.getPlaneInwardNormal(this.orientation, this.targetView);
if (undefined === normal)
return EventHandled.No;
ViewClipTool.enableClipVolume(this.targetView);
if (!ViewClipTool.doClipToPlane(this.targetView, ev.point, normal, this._clearExistingPlanes))
return EventHandled.No;
if (undefined !== this._clipEventHandler)
this._clipEventHandler.onNewClipPlane(this.targetView);
await this.onReinitialize();
return EventHandled.Yes;
}
}
/** A tool to define a clip volume for a view by specifying a shape
* @public
*/
export class ViewClipByShapeTool extends ViewClipTool {
static toolId = "ViewClip.ByShape";
static iconSpec = "icon-section-shape";
/** @internal */
_orientationValue = { value: ContextRotationId.Top };
/** @internal */
_points = [];
/** @internal */
_matrix;
/** @internal */
_zLow;
/** @internal */
_zHigh;
/** @internal */
get orientation() { return this._orientationValue.value; }
set orientation(option) { this._orientationValue.value = option; }
/** @internal */
supplyToolSettingsProperties() {
const initialValue = IModelApp.toolAdmin.toolSettingsState.getInitialToolSettingValue(this.toolId, ViewClipTool._orientationName);
initialValue && (this._orientationValue = initialValue);
const toolSettings = new Array();
toolSettings.push({ value: this._orientationValue, property: ViewClipTool._getEnumAsOrientationDescription(), editorPosition: { rowPriority: 0, columnIndex: 2 } });
return toolSettings;
}
/** @internal */
async applyToolSettingPropertyChange(updatedValue) {
if (updatedValue.propertyName === ViewClipTool._orientationName) {
this._orientationValue = updatedValue.value;
if (!this._orientationValue)
return false;
this._points.length = 0;
this._matrix = undefined;
AccuDrawHintBuilder.deactivate();
this.setupAndPromptForNextAction();
IModelApp.toolAdmin.toolSettingsState.saveToolSettingProperty(this.toolId, { propertyName: ViewClipTool._orientationName, value: this._orientationValue });
return true;
}
return false;
}
/** @internal */
showPrompt() {
let mainMsg = "ViewClip.ByShape.Prompts.";
switch (this._points.length) {
case 0:
mainMsg += "FirstPoint";
break;
case 1:
mainMsg += "SecondPoint";
break;
case 2:
mainMsg += "ThirdPoint";
break;
default:
mainMsg += "NextPoint";
break;
}
const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, CoreTools.translate(mainMsg));
const mouseInstructions = [];
const touchInstructions = [];
if (!ToolAssistance.createTouchCursorInstructions(touchInstructions))
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, CoreTools.translate("ElementSet.Inputs.AcceptPoint"), false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClick, CoreTools.translate("ElementSet.Inputs.AcceptPoint"), false, ToolAssistanceInputMethod.Mouse));
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.TwoTouchTap, CoreTools.translate("ElementSet.Inputs.Exit"), false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.RightClick, CoreTools.translate("ElementSet.Inputs.Exit"), false, ToolAssistanceInputMethod.Mouse));
if (this._points.length > 1)
mouseInstructions.push(ToolAssistance.createModifierKeyInstruction(ToolAssistance.ctrlKey, ToolAssistanceImage.LeftClick, CoreTools.translate("ElementSet.Inputs.AdditionalPoint"), false, ToolAssistanceInputMethod.Mouse));
if (0 !== this._points.length)
mouseInstructions.push(ToolAssistance.createKeyboardInstruction(ToolAssistance.createKeyboardInfo([ToolAssistance.ctrlKey, "Z"]), CoreTools.translate("ElementSet.Inputs.UndoLastPoint"), false, ToolAssistanceInputMethod.Mouse));
const sections = [];
sections.push(ToolAssistance.createSection(mouseInstructions, ToolAssistance.inputsLabel));
sections.push(ToolAssistance.createSection(touchInstructions, ToolAssistance.inputsLabel));
const instructions = ToolAssistance.createInstructions(mainInstruction, sections);
IModelApp.notifications.setToolAssistance(instructions);
}
/** @internal */
setupAndPromptForNextAction() {
IModelApp.accuSnap.enableSnap(true);
super.setupAndPromptForNextAction();
if (0 === this._points.length)
return;
if (undefined === this._matrix)
return;
const hints = new AccuDrawHintBuilder();
hints.setOrigin(this._points[this._points.length - 1]);
if (1 === this._points.length) {
hints.setMatrix(this._matrix);
hints.setModeRectangular();
}
else if (this._points.length > 1 && !(this._points[this._points.length - 1].isAlmostEqual(this._points[this._points.length - 2]))) {
const xVec = Vector3d.createStartEnd(this._points[this._points.length - 2], this._points[this._points.length - 1]);
const zVec = this._matrix.getColumn(2);
const matrix = Matrix3d.createRigidFromColumns(xVec, zVec, AxisOrder.XZY);
if (undefined !== matrix)
hints.setMatrix(matrix); // Rotate AccuDraw x axis to last segment preserving current up vector...
}
hints.setLockZ = true;
hints.sendHints();
}
getClipPoints(ev) {
const points = [];
if (undefined === this.targetView || this._points.length < 1)
return points;
for (const pt of this._points)
points.push(pt.clone());
if (undefined === this._matrix)
return points;
const vp = ev.viewport;
if (undefined === vp)
return points;
const normal = this._matrix.getColumn(2);
let currentPt = AccuDrawHintBuilder.projectPointToPlaneInView(ev.point, points[0], normal, vp, true);
if (undefined === currentPt)
currentPt = ev.point.clone();
if (2 === points.length && !ev.isControlKey) {
const xDir = Vector3d.createStartEnd(points[0], points[1]);
const xLen = xDir.magnitude();
xDir.normalizeInPlace();
const yDir = xDir.crossProduct(normal);
yDir.normalizeInPlace();
const cornerPt = AccuDrawHintBuilder.projectPointToLineInView(currentPt, points[1], yDir, vp, true);
if (undefined !== cornerPt) {
points.push(cornerPt);
cornerPt.plusScaled(xDir, -xLen, currentPt);
}
}
points.push(currentPt);
if (points.length > 2)
points.push(points[0].clone());
return points;
}
/** @internal */
isValidLocation(ev, isButtonEvent) {
return (this._points.length > 0 ? true : super.isValidLocation(ev, isButtonEvent));
}
/** @internal */
decorate(context) {
if (context.viewport !== this.targetView)
return;
const ev = new BeButtonEvent();
IModelApp.toolAdmin.fillEventFromCursorLocation(ev);
if (undefined === ev.viewport)
return;
const points = this.getClipPoints(ev);
if (points.length < 2)
return;
const builderAccVis = context.createGraphicBuilder(GraphicType.WorldDecoration);
const builderAccHid = context.createGraphicBuilder(GraphicType.WorldOverlay);
const colorAccVis = EditManipulator.HandleUtils.adjustForBackgroundColor(ColorDef.white, context.viewport);
const colorAccHid = colorAccVis.withAlpha(100);
const fillAccVis = context.viewport.hilite.color.withAlpha(25);
builderAccVis.setSymbology(colorAccVis, fillAccVis, 3);
builderAccHid.setSymbology(colorAccHid, fillAccVis, 1, LinePixels.Code2);
if (points.length > 2)
builderAccHid.addShape(points);
builderAccVis.addLineString(points);
builderAccHid.addLineString(points);
context.addDecorationFromBuilder(builderAccVis);
context.addDecorationFromBuilder(builderAccHid);
}
/** @internal */
async onMouseMotion(ev) {
if (this._points.length > 0 && undefined !== ev.viewport)
ev.viewport.invalidateDecorations();
}
/** @internal */
async onDataButtonDown(ev) {
if (undefined === this.targetView)
return EventHandled.No;
if (this._points.length > 1 && !ev.isControlKey) {
const points = this.getClipPoints(ev);
if (points.length < 3)
return EventHandled.No;
const transform = Transform.createOriginAndMatrix(points[0], this._matrix);
transform.multiplyInversePoint3dArrayInPlace(points);
ViewClipTool.enableClipVolume(this.targetView);
if (!ViewClipTool.doClipToShape(this.targetView, points, transform, this._zLow, this._zHigh))
return EventHandled.No;
if (undefined !== this._clipEventHandler)
this._clipEventHandler.onNewClip(this.targetView);
await this.onReinitialize();
return EventHandled.Yes;
}
if (undefined === this._matrix && undefined === (this._matrix = AccuDrawHintBuilder.getContextRotation(this.orientation, this.targetView)))
return EventHandled.No;
const currPt = ev.point.clone();
if (this._points.length > 0) {
const vp = ev.viewport;
const planePt = (vp ? AccuDrawHintBuilder.projectPointToPlaneInView(currPt, this._points[0], this._matrix.getColumn(2), vp, true) : undefined);
if (undefined !== planePt)
currPt.setFrom(planePt);
}
this._points.push(currPt);
this.setupAndPromptForNextAction();
return EventHandled.No;
}
/** @internal */
async onUndoPreviousStep() {
if (0 === this._points.length)
return false;
this._points.pop();
this.setupAndPromptForNextAction();
return true;
}
}
/** A tool to define a clip volume for a view by specifying range corners
* @public
*/
export class ViewClipByRangeTool extends ViewClipTool {
static toolId = "ViewClip.ByRange";
static iconSpec = "icon-section-range";
/** @internal */
_corner;
/** @internal */
showPrompt() {
const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, CoreTools.translate(undefined === this._corner ? "ViewClip.ByRange.Prompts.FirstPoint" : "ViewClip.ByRange.Prompts.NextPoint"));
const mouseInstructions = [];
const touchInstructions = [];
if (!ToolAssistance.createTouchCursorInstructions(touchInstructions))
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, CoreTools.translate("ElementSet.Inputs.AcceptPoint"), false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClick, CoreTools.translate("ElementSet.Inputs.AcceptPoint"), false, ToolAssistanceInputMethod.Mouse));
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.TwoTouchTap, CoreTools.translate("ElementSet.Inputs.Exit"), false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.RightClick, CoreTools.translate("ElementSet.Inputs.Exit"), false, ToolAssistanceInputMethod.Mouse));
if (undefined !== this._corner)
mouseInstructions.push(ToolAssistance.createKeyboardInstruction(ToolAssistance.createKeyboardInfo([ToolAssistance.ctrlKey, "Z"]), CoreTools.translate("ElementSet.Inputs.UndoLastPoint"), false, ToolAssistanceInputMethod.Mouse));
const sections = [];
sections.push(ToolAssistance.createSection(mouseInstructions, ToolAssistance.inputsLabel));
sections.push(ToolAssistance.createSection(touchInstructions, ToolAssistance.inputsLabel));
const instructions = ToolAssistance.createInstructions(mainInstruction, sections);
IModelApp.notifications.setToolAssistance(instructions);
}
/** @internal */
setupAndPromptForNextAction() {
IModelApp.accuSnap.enableSnap(true);
super.setupAndPromptForNextAction();
}
getClipRange(range, transform, ev) {
if (undefined === this.targetView || undefined === this._corner)
return false;
// Creating clip aligned with ACS when ACS context lock is enabled...
const matrix = AccuDrawHintBuilder.getContextRotation(ContextRotationId.Top, this.targetView);
Transform.createOriginAndMatrix(this._corner, matrix, transform);
const pt1 = transform.multiplyInversePoint3d(this._corner);
const pt2 = transform.multiplyInversePoint3d(ev.point);
if (undefined === pt1 || undefined === pt2)
return false;
range.setFrom(Range3d.create(pt1, pt2));
return true;
}
/** @internal */
decorate(context) {
if (context.viewport !== this.targetView || undefined === this._corner)
return;
const ev = new BeButtonEvent();
IModelApp.toolAdmin.fillEventFromCursorLocation(ev);
if (undefined === ev.viewport)
return;
const range = Range3d.create();
const transform = Transform.createIdentity();
if (!this.getClipRange(range, transform, ev))
return;
const builderAccVis = context.createGraphicBuilder(GraphicType.WorldDecoration, transform);
const builderAccHid = context.createGraphicBuilder(GraphicType.WorldOverlay, transform);
const colorAccVis = EditManipulator.HandleUtils.adjustForBackgroundColor(ColorDef.white, context.viewport);
const colorAccHid = colorAccVis.withAlpha(100);
builderAccVis.setSymbology(colorAccVis, ColorDef.black, 3);
builderAccHid.setSymbology(colorAccHid, ColorDef.black, 1, LinePixels.Code2);
builderAccVis.addRangeBox(range);
builderAccHid.addRangeBox(range);
context.addDecorationFromBuilder(builderAccVis);
context.addDecorationFromBuilder(builderAccHid);
}
/** @internal */
async onMouseMotion(ev) {
if (undefined !== this._corner && undefined !== ev.viewport)
ev.viewport.invalidateDecorations();
}
/** @internal */
async onDataButtonDown(ev) {
if (undefined === this.targetView)
return EventHandled.No;
if (undefined !== this._corner) {
const range = Range3d.create();
const transform = Transform.createIdentity();
if (!this.getClipRange(range, transform, ev))
return EventHandled.No;
ViewClipTool.enableClipVolume(this.targetView);
if (!ViewClipTool.doClipToRange(this.targetView, range, transform))
return EventHandled.No;
if (undefined !== this._clipEventHandler)
this._clipEventHandler.onNewClip(this.targetView);
await this.onReinitialize();
return EventHandled.Yes;
}
this._corner = ev.point.clone();
this.setupAndPromptForNextAction();
return EventHandled.No;
}
/** @internal */
async onUndoPreviousStep() {
if (undefined === this._corner)
return false;
this._corner = undefined;
this.setupAndPromptForNextAction();
return true;
}
}
/** A tool to define a clip volume for a view using the element aligned box or axis aligned box.
* @public
*/
export class ViewClipByElementTool extends ViewClipTool {
_alwaysUseRange;
static toolId = "ViewClip.ByElement";
static iconSpec = "icon-section-element";
constructor(clipEventHandler, _alwaysUseRange = false) {
super(clipEventHandler);
this._alwaysUseRange = _alwaysUseRange;
}
/** @internal */
showPrompt() {
const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, CoreTools.translate("ViewClip.ByElement.Prompts.FirstPoint"));
const mouseInstructions = [];
const touchInstructions = [];
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, CoreTools.translate("ElementSet.Inputs.AcceptElement"), false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClick, CoreTools.translate("ElementSet.Inputs.AcceptElement"), false, ToolAssistanceInputMethod.Mouse));
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.TwoTouchTap, CoreTools.translate("ElementSet.Inputs.Exit"), false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.RightClick, CoreTools.translate("ElementSet.Inputs.Exit"), false, ToolAssistanceInputMethod.Mouse));
const sections = [];
sections.push(ToolAssistance.createSection(mouseInstructions, ToolAssistance.inputsLabel));
sections.push(ToolAssistance.createSection(touchInstructions, ToolAssistance.inputsLabel));
const instructions = ToolAssistance.createInstructions(mainInstruction, sections);
IModelApp.notifications.setToolAssistance(instructions);
}
/** @internal */
async onPostInstall() {
await super.onPostInstall();
if (undefined !== this.targetView && this.targetView.iModel.selectionSet.isActive) {
let useSelection = true;
this.targetView.iModel.selectionSet.elements.forEach((val) => {
if (Id64.isInvalid(val) || Id64.isTransient(val))
useSelection = false;
});
if (useSelection) {
await this.doClipToSelectedElements(this.targetView);
return;
}
}
this.initLocateElements(true, false, "default", CoordinateLockOverrides.All);
}
/** @internal */
async doClipToSelectedElements(viewport) {
if (await this.doClipToElements(viewport, viewport.iModel.selectionSet.elements, this._alwaysUseRange))
return true;
await this.exitTool();
return false;
}
async doClipToElements(viewport, ids, alwaysUseRange = false, modelId) {
try {
const placements = await viewport.iModel.elements.getPlacements(ids, { type: viewport.view.is3d() ? "3d" : "2d" });
if (0 === placements.length)
return false;
const displayTransform = modelId && 1 === placements.length ? viewport.view.computeDisplayTransform({ modelId, elementId: placements[0].elementId }) : undefined;
const range = new Range3d();
const transform = Transform.createIdentity();
if (!alwaysUseRange && 1 === placements.length) {
const placement = placements[0];
range.setFrom(placement instanceof Placement2d ? Range3d.createRange2d(placement.bbox, 0) : placement.bbox);
transform.setFrom(placement.transform); // Use ElementAlignedBox for single selection...
displayTransform?.multiplyTransformTransform(transform, transform);
}
else {
for (const placement of placements)
range.extendRange(placement.calculateRange());
if (displayTransform)
transform.setFrom(displayTransform);
}
if (range.isNull)
return false;
range.scaleAboutCenterInPlace(1.001); // pad range slightly...
if (range.isAlmostZeroX || range.isAlmostZeroY) {
if (range.isAlmostZeroZ)
return false;
// Invalid XY range for clip, see if XZ or YZ can be used instead...
const canUseXZ = !range.isAlmostZeroX;
const canUseYZ = !canUseXZ && !range.isAlmostZeroY;
if (!canUseXZ && !canUseYZ)
return false;
const zDir = canUseXZ ? Vector3d.unitY() : Vector3d.unitX();
const indices = Range3d.faceCornerIndices(canUseXZ ? 3 : 1);
const corners = range.corners();
const points = [];
for (const index of indices)
points.push(corners[index]);
transform.multiplyPoint3dArrayInPlace(points);
transform.multiplyVector(zDir, zDir);
transform.setFrom(Transform.createOriginAndMatrix(points[0], Matrix3d.createRigidHeadsUp(zDir)));
transform.multiplyInversePoint3dArrayInPlace(points);
ViewClipTool.enableClipVolume(viewport);
if (!ViewClipTool.doClipToShape(viewport, points, transform))
return false;
if (undefined !== this._clipEventHandler)
this._clipEventHandler.onNewClip(viewport);
await this.onReinitialize();
return true;
}
ViewClipTool.enableClipVolume(viewport);
if (!ViewClipTool.doClipToRange(viewport, range, transform))
return false;
if (undefined !== this._clipEventHandler)
this._clipEventHandler.onNewClip(viewport);
await this.onReinitialize();
return true;
}
catch {
return false;
}
}
/** @internal */
async onDataButtonDown(ev) {
if (undefined === this.targetView)
return EventHandled.No;
const hit = await IModelApp.locateManager.doLocate(new LocateResponse(), true, ev.point, ev.viewport, ev.inputSource);
if (undefined === hit || !hit.isElementHit)
return EventHandled.No;
return await this.doClipToElements(this.targetView, hit.sourceId, this._alwaysUseRange, hit.modelId) ? EventHandled.Yes : EventHandled.No;
}
}
/** @internal Interactive tool base class to modify a view's clip */
export class ViewClipModifyTool extends EditManipulator.HandleTool {
_anchorIndex;
_ids;
_controls;
_clipView;
_clip;
_viewRange;
_restoreClip = true;
_currentDistance = 0.0;
_clipStyle;
constructor(manipulator, clip, vp, hitId, ids, controls) {
super(manipulator);
this._anchorIndex = ids.indexOf(hitId);
this._ids = ids;
this._controls = controls;
this._clipView = vp;
this._clip = clip;
this._viewRange = vp.computeViewRange();
// Don't request section-cut graphics while the user is modifying the clip. We'll restore this when the tool exits.
this._clipStyle = vp.clipStyle;
if (this._clipStyle.produceCutGeometry) {
vp.clipStyle = ClipStyle.fromJSON({
...this._clipStyle.toJSON(),
produceCutGeometry: false,
});
}
}
get wantAccuSnap() { return false; }
init() {
super.init();
AccuDrawHintBuilder.deactivate();
}
getOffsetValue(ev, transformFromClip) {
if (-1 === this._anchorIndex || undefined === ev.viewport || ev.viewport !== this._clipView)
return undefined;
// NOTE: Use AccuDraw z instead of view z if AccuDraw is explicitly enabled...
const anchorRay = ViewClipTool.getClipRayTransformed(this._controls[this._anchorIndex].origin, this._controls[this._anchorIndex].direction, transformFromClip);
const projectedPt = AccuDrawHintBuilder.projectPointToLineInView(ev.point, anchorRay.origin, anchorRay.direction, ev.viewport, true);
if (undefined === projectedPt)
return undefined;
const offsetVec = Vector3d.createStartEnd(anchorRay.origin, projectedPt);
let offset = offsetVec.normalizeWithLength(offsetVec).mag;
if (offset < Geometry.smallMetricDistance)
return undefined;
if (offsetVec.dotProduct(anchorRay.direction) < 0.0)
offset *= -1.0;
this._currentDistance = offset;
return offset;
}
drawAnchorOffset(context, color, weight, transformFromClip) {
if (-1 === this._anchorIndex || Math.abs(this._currentDistance) < Geometry.smallMetricDistance)
return;
const anchorRay = ViewClipTool.getClipRayTransformed(this._controls[this._anchorIndex].origin, this._controls[this._anchorIndex].direction, transformFromClip);
anchorRay.direction.scaleToLength(this._currentDistance, anchorRay.direction);
const pt1 = anchorRay.fractionToPoint(0.0);
const pt2 = anchorRay.fractionToPoint(1.0);
const builder = context.createGraphicBuilder(GraphicType.ViewOverlay);
context.viewport.worldToView(pt1, pt1);
pt1.z = 0.0;
context.viewport.worldToView(pt2, pt2);
pt2.z = 0.0;
builder.setSymbology(color, ColorDef.black, weight, LinePixels.Code5);
builder.addLineString([pt1, pt2]);
builder.setSymbology(color, ColorDef.black, weight + 7);
builder.addPointString([pt1, pt2]);
context.addDecorationFromBuilder(builder);
}
decorate(context) {
if (-1 === this._anchorIndex || context.viewport !== this._clipView)
return;
this.drawViewClip(context);
}
async onMouseMotion(ev) {
if (!this.updateViewClip(ev, false))
return;
this._clipView.invalidateDecorations();
}
accept(ev) {
if (!this.updateViewClip(ev, true))
return false;
this._restoreClip = false;
return true;
}
async onCleanup() {
if (this._restoreClip && ViewClipTool.hasClip(this._clipView))
ViewClipTool.setViewClip(this._clipView, this._clip);
this._clipView.clipStyle = this._clipStyle;
}
}
/** @internal Interactive tool to modify a view's clip defined by a ClipShape */
export class ViewClipShapeModifyTool extends ViewClipModifyTool {
updateViewClip(ev, _isAccept) {
const clipShape = ViewClipTool.isSingleClipShape(this._clip);
if (undefined === clipShape)
return false;
const offset = this.getOffsetValue(ev, clipShape.transformFromClip);
if (undefined === offset)
return false;
const offsetAll = ev.isShiftKey;
const localOffset = ViewClipTool.getOffsetValueTransformed(offset, clipShape.transformToClip);
const shapePts = ViewClipTool.getClipShapePoints(clipShape, 0.0);
con