diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index 493ccc53e9f6..d0b79aaa6d2d 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -884,8 +884,7 @@ export class AccuDrawShortcuts { static defineACSByElement(): Promise; // (undocumented) static defineACSByPoints(): Promise; - // (undocumented) - static getACS(acsName: string | undefined, useOrigin: boolean, useRotation: boolean): BentleyStatus; + static getACS(acsName: string | undefined, useOrigin: boolean, useRotation: boolean): Promise; // (undocumented) static itemFieldAcceptInput(index: ItemField, str: string): Promise; // (undocumented) @@ -939,8 +938,7 @@ export class AccuDrawShortcuts { static suspendToggle(): void; // (undocumented) static updateACSByPoints(acs: AuxCoordSystemState, vp: Viewport, points: Point3d[], isDynamics: boolean): boolean; - // (undocumented) - static writeACS(_acsName: string): BentleyStatus; + static writeACS(acsName: string | undefined): Promise; } // @beta @@ -1416,7 +1414,7 @@ export abstract class AuxCoordSystemState extends ElementState implements AuxCoo constructor(props: AuxCoordSystemProps, iModel: IModelConnection); // (undocumented) static get className(): string; - static createNew(acsName: string, iModel: IModelConnection): AuxCoordSystemState; + static createNew(_acsName: string, iModel: IModelConnection): AuxCoordSystemState; // (undocumented) description?: string; // (undocumented) diff --git a/common/changes/@itwin/core-frontend/accudraw-named-acs-support_2026-07-23-13-45-55.json b/common/changes/@itwin/core-frontend/accudraw-named-acs-support_2026-07-23-13-45-55.json new file mode 100644 index 000000000000..ac11a63efe7e --- /dev/null +++ b/common/changes/@itwin/core-frontend/accudraw-named-acs-support_2026-07-23-13-45-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-frontend", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/core-frontend" +} \ No newline at end of file diff --git a/core/frontend/src/AuxCoordSys.ts b/core/frontend/src/AuxCoordSys.ts index e555467f0654..a1319815707c 100644 --- a/core/frontend/src/AuxCoordSys.ts +++ b/core/frontend/src/AuxCoordSys.ts @@ -9,7 +9,7 @@ import { JsonUtils } from "@itwin/core-bentley"; import { Angle, AngleSweep, Arc3d, Matrix3d, Point2d, Point3d, Transform, Vector3d, XAndY, XYAndZ, YawPitchRollAngles } from "@itwin/core-geometry"; import { - AuxCoordSystem2dProps, AuxCoordSystem3dProps, AuxCoordSystemProps, BisCodeSpec, Code, ColorDef, IModel, LinePixels, Npc, + AuxCoordSystem2dProps, AuxCoordSystem3dProps, AuxCoordSystemProps, Code, ColorDef, IModel, LinePixels, Npc, } from "@itwin/core-common"; import { ElementState } from "./EntityState"; import { IModelConnection } from "./IModelConnection"; @@ -79,13 +79,19 @@ export abstract class AuxCoordSystemState extends ElementState implements AuxCoo } /** Create a new AuxCoordSystemState. - * @param acsName the name for the new AuxCoordSystem - * @param iModel the iModel for which the ACS applies. - * @note call this method with the appropriate subclass (e.g. AuxCoordSystemSpatialState, AuxCoordSystem2dState, etc), not on AuxCoordSystemState directly + * @param _acsName unused, this method creates an empty code as it lacks sufficient information to do otherwise. + * @param iModel the iModel for which the ACS applies + * @note Inserting a new named ACS element requires creating a CodeProps for the type of view and the view's definition model. + * This method doesn't have the information to do this so it creates an empty code. + * @example + * ```typescript + * const codeSpecName = vp.view.isSpatialView() ? BisCodeSpec.auxCoordSystemSpatial : (vp.view.is3d() ? BisCodeSpec.auxCoordSystem3d : BisCodeSpec.auxCoordSystem2d); + * const codeSpec = await vp.iModel.codeSpecs.getByName(codeSpecName); + * const code = new Code({ spec: codeSpec.id, scope: vp.view.model, value: acsName }); + * ``` */ - public static createNew(acsName: string, iModel: IModelConnection): AuxCoordSystemState { - const myCode = new Code({ spec: BisCodeSpec.auxCoordSystemSpatial, scope: IModel.dictionaryId.toString(), value: acsName }); - return new AuxCoordSystemSpatialState({ model: IModel.dictionaryId, code: myCode, classFullName: this.classFullName }, iModel); + public static createNew(_acsName: string, iModel: IModelConnection): AuxCoordSystemState { + return new AuxCoordSystemSpatialState({ model: IModel.dictionaryId, code: Code.createEmpty(), classFullName: this.classFullName }, iModel); } public constructor(props: AuxCoordSystemProps, iModel: IModelConnection) { diff --git a/core/frontend/src/EntityState.ts b/core/frontend/src/EntityState.ts index eee2fe8b7036..fd46df9288d4 100644 --- a/core/frontend/src/EntityState.ts +++ b/core/frontend/src/EntityState.ts @@ -109,7 +109,7 @@ export class ElementState extends EntityState implements ElementProps { public override toJSON(): ElementProps { const val = super.toJSON() as ElementProps; if (Id64.isValid(this.code.spec)) - val.code = this.code; + val.code = this.code.toJSON(); // Ensure _value with get accessor is serialized... val.model = this.model; val.parent = this.parent; diff --git a/core/frontend/src/tools/AccuDrawTool.ts b/core/frontend/src/tools/AccuDrawTool.ts index c8645cd3b0db..7c3e67dc5541 100644 --- a/core/frontend/src/tools/AccuDrawTool.ts +++ b/core/frontend/src/tools/AccuDrawTool.ts @@ -6,14 +6,15 @@ * @module Tools */ -import { BentleyStatus } from "@itwin/core-bentley"; +import { AuxCoordSystemProps, BisCodeSpec, Code } from "@itwin/core-common"; import { AxisOrder, Geometry, Matrix3d, Plane3dByOriginAndUnitNormal, Point3d, Transform, Vector3d } from "@itwin/core-geometry"; import { AccuDraw, AccuDrawFlags, CompassMode, ContextMode, ItemField, KeyinStatus, LockedStates, RotationMode, ThreeAxes } from "../AccuDraw"; import { TentativeOrAccuSnap } from "../AccuSnap"; -import { ACSDisplayOptions, AuxCoordSystemState } from "../AuxCoordSys"; +import { ACSDisplayOptions, ACSType, AuxCoordSystemState } from "../AuxCoordSys"; import { SnapDetail } from "../HitDetail"; import { IModelApp } from "../IModelApp"; import { DecorateContext } from "../ViewContext"; +import { ViewState } from "../ViewState"; import { ScreenViewport, Viewport } from "../Viewport"; import { BeButtonEvent, CoreTools, Tool } from "./Tool"; import { AccuDrawShortcutImplementation, AccuDrawShortcutTool } from "./AccuDrawShortcutTool"; @@ -35,6 +36,9 @@ function normalizedCrossProduct(vec1: Vector3d, vec2: Vector3d, out: Vector3d): * @beta */ export class AccuDrawShortcuts { + // Monotonic token used to ignore stale async ACS operations. + private static _acsRequestGeneration = 0; + /** Disable/Enable AccuDraw for the session */ public static sessionToggle(): void { const accudraw = IModelApp.accuDraw; @@ -871,55 +875,88 @@ export class AccuDrawShortcuts { return IModelApp.tools.run("AccuDraw.DefineACSByPoints"); } - public static getACS(acsName: string | undefined, useOrigin: boolean, useRotation: boolean): BentleyStatus { + private static async getACSCode(view: ViewState, acsName: string): Promise { + const codeSpecName = view.isSpatialView() ? BisCodeSpec.auxCoordSystemSpatial : (view.is3d() ? BisCodeSpec.auxCoordSystem3d : BisCodeSpec.auxCoordSystem2d); + const codeSpec = await view.iModel.codeSpecs.getByName(codeSpecName); + return new Code({ spec: codeSpec.id, scope: view.model, value: acsName }); + } + + private static async getNamedACS(view: ViewState, acsName: string): Promise<{ validForView: boolean, acs?: AuxCoordSystemState }> { + const acsCode = await this.getACSCode(view, acsName); + const acsProps = await view.iModel.elements.loadProps(acsCode.toJSON()); + if (!acsProps) + return { validForView: false }; + + const namedAcs = AuxCoordSystemState.fromProps(acsProps, view.iModel); + const validForView = namedAcs.isValidForView(view); + + return { validForView, acs: namedAcs }; + } + + private static async createNewACS(view: ViewState, acsName: string): Promise { + // Want a CodeProps to support insert so ViewState.createAuxCoordSystem(acsName) is not called. + const acsCode = await this.getACSCode(view, acsName); + + // Determine the class name based on view type + let classFullName: string; + if (view.isSpatialView()) + classFullName = "BisCore:AuxCoordSystemSpatial"; + else if (view.is3d()) + classFullName = "BisCore:AuxCoordSystem3d"; + else + classFullName = "BisCore:AuxCoordSystem2d"; + + const acsProps: AuxCoordSystemProps = { model: acsCode.scope, code: acsCode.toJSON(), classFullName, type: ACSType.None }; + const acs = AuxCoordSystemState.fromProps(acsProps, view.iModel); + return acs; + } + + /** + * Set the AuxiliaryCoordinateSystem for the current AccuDraw viewport by name. + * Optionally updates the AccuDraw origin and rotation to match the ACS. + * @note When no name is specified, AccuDraw is updated using the view's current ACS. + * @returns AuxCoordSystemState or undefined if name does not exist or is invalid for the view. + * @throws Error if resolving the ACS CodeSpec or loading ACS element properties fails. + */ + public static async getACS(acsName: string | undefined, useOrigin: boolean, useRotation: boolean): Promise { const accudraw = IModelApp.accuDraw; if (!accudraw.isEnabled) - return BentleyStatus.ERROR; + return undefined; const vp = accudraw.currentView; if (!vp) - return BentleyStatus.ERROR; + return undefined; - let currRotation = 0, currBaseRotation = 0; - const axes = new ThreeAxes(); + const view = vp.view; + const requestGeneration = ++this._acsRequestGeneration; + const isRequestCurrent = () => accudraw.isEnabled && accudraw.currentView === vp && vp.view === view && requestGeneration === this._acsRequestGeneration; - if (!useRotation) { - // Save current rotation, event listener on ACS change will orient AccuDraw to ACS... - currRotation = accudraw.rotationMode; - currBaseRotation = accudraw.flags.baseRotation; - axes.setFrom(accudraw.axes); - } + let currentACS = view.auxiliaryCoordinateSystem; if (acsName && "" !== acsName) { - // // See if this ACS already exists... - // DgnCode acsCode = AuxCoordSystem:: CreateCode(vp -> GetViewControllerR().GetViewDefinition(), acsName); - // DgnElementId acsId = vp -> GetViewController().GetDgnDb().Elements().QueryElementIdByCode(acsCode); - - // if (!acsId.IsValid()) - // return ERROR; - - // AuxCoordSystemCPtr auxElm = vp -> GetViewController().GetDgnDb().Elements().Get(acsId); + const namedAcsResult = await this.getNamedACS(view, acsName); + if (!isRequestCurrent()) + return undefined; - // if (!auxElm.IsValid()) - // return ERROR; + if (!namedAcsResult.validForView) + return undefined; - // AuxCoordSystemPtr acsPtr = auxElm -> MakeCopy(); + const namedAcs = namedAcsResult.acs; + if (!namedAcs) + return undefined; - // if (!acsPtr.IsValid()) - // return ERROR; + if (!useOrigin) + namedAcs.setOrigin(currentACS.getOrigin()); - // AuxCoordSystemCR oldACS = vp -> GetViewController().GetAuxCoordinateSystem(); + if (!useRotation) + namedAcs.setRotation(currentACS.getRotation()); - // if (!useOrigin) - // acsPtr -> SetOrigin(oldACS.GetOrigin()); - - // if (!useRotation) - // acsPtr -> SetRotation(oldACS.GetRotation()); - - // AccuDraw:: UpdateAuxCoordinateSystem(* acsPtr, * vp); + AccuDraw.updateAuxCoordinateSystem(namedAcs, vp); // Sets AccuDrawFlags.OrientACS hint to orient AccuDraw to ACS... + currentACS = namedAcs; } - const currentACS = vp.view.auxiliaryCoordinateSystem; + if (!isRequestCurrent()) + return undefined; if (useOrigin) { accudraw.origin.setFrom(currentACS.getOrigin()); @@ -933,57 +970,73 @@ export class AccuDrawShortcuts { } else { this.itemFieldUnlockAll(); - accudraw.setRotationMode(currRotation); - accudraw.flags.baseRotation = currBaseRotation; - accudraw.axes.setFrom(axes); - if (RotationMode.ACS === accudraw.flags.baseRotation) { const acs = currentACS.clone(); - const rMatrix = accudraw.getRotation(); - acs.setRotation(rMatrix); - + acs.setRotation(accudraw.getRotation()); AccuDraw.updateAuxCoordinateSystem(acs, vp); } accudraw.published.flags &= ~AccuDrawFlags.OrientACS; } - return BentleyStatus.SUCCESS; + return currentACS; } - public static writeACS(_acsName: string): BentleyStatus { + /** + * Create a new AuxiliaryCoordinateSystem for the current AccuDraw viewport using the compass origin, orientation, and mode. + * @note When no name is specified, the view's current ACS is updated from AccuDraw. Caller can choose to create + * an EditCommand to insert/update as a named ACS element using the returned AuxCoordSystemState. + * @returns AuxCoordSystemState or undefined if ACS could not be created. + * @throws Error if resolving the ACS CodeSpec or loading ACS element properties fails. + */ + public static async writeACS(acsName: string | undefined): Promise { const accudraw = IModelApp.accuDraw; if (!accudraw.isEnabled) - return BentleyStatus.ERROR; + return undefined; const vp = accudraw.currentView; if (!vp) - return BentleyStatus.ERROR; + return undefined; + + const view = vp.view; + const requestGeneration = ++this._acsRequestGeneration; + const isRequestCurrent = () => accudraw.isEnabled && accudraw.currentView === vp && vp.view === view && requestGeneration === this._acsRequestGeneration; - // const origin = accudraw.origin; - // const rMatrix = accudraw.getRotation(); - // AuxCoordSystemPtr acsPtr = AuxCoordSystem:: CreateFrom(vp -> GetViewController().GetAuxCoordinateSystem()); - // acsPtr -> SetOrigin(origin); - // acsPtr -> SetRotation(rMatrix); - // acsPtr -> SetType(CompassMode.Polar == accudraw.getCompassMode() ? ACSType :: Cylindrical : ACSType:: Rectangular); - // acsPtr -> SetCode(AuxCoordSystem:: CreateCode(vp -> GetViewControllerR().GetViewDefinition(), nullptr != acsName ? acsName : "")); - // acsPtr -> SetDescription(""); + let acs: AuxCoordSystemState | undefined; + if (acsName && "" !== acsName) { + const namedAcsResult = await this.getNamedACS(view, acsName); + if (!isRequestCurrent()) + return undefined; + + acs = namedAcsResult.acs; + if (!acs) { + acs = await this.createNewACS(view, acsName); + if (!isRequestCurrent()) + return undefined; + } else if (!namedAcsResult.validForView) { + return undefined; + } + } else { + acs = view.auxiliaryCoordinateSystem.clone(); + acs.description = ""; + } - // if (acsName && '\0' != acsName[0]) { - // DgnDbStatus status; - // acsPtr -> Insert(& status); + if (!isRequestCurrent()) + return undefined; - // if (DgnDbStatus:: Success != status) - // return BentleyStatus.ERROR; - // } + if (!acs) + return undefined; - // AccuDraw:: UpdateAuxCoordinateSystem(* acsPtr, * vp); + acs.setOrigin(accudraw.origin); + acs.setRotation(accudraw.getRotation()); + acs.type = CompassMode.Polar === accudraw.compassMode ? ACSType.Cylindrical : ACSType.Rectangular; - // accudraw.flags.baseRotation = RotationMode.ACS; - // accudraw.SetRotationMode(RotationMode.ACS); + AccuDraw.updateAuxCoordinateSystem(acs, vp); + accudraw.flags.baseRotation = RotationMode.ACS; + accudraw.setRotationMode(RotationMode.ACS); - return BentleyStatus.SUCCESS; + return acs; } public static itemFieldUnlockAll(): void { diff --git a/full-stack-tests/core/src/frontend/standalone/ViewState.test.ts b/full-stack-tests/core/src/frontend/standalone/ViewState.test.ts index 027613022f04..657c7d996bdd 100644 --- a/full-stack-tests/core/src/frontend/standalone/ViewState.test.ts +++ b/full-stack-tests/core/src/frontend/standalone/ViewState.test.ts @@ -87,7 +87,6 @@ describe("ViewState", () => { assert.notDeepEqual(v2, viewState); const acs = v2.createAuxCoordSystem("test"); - assert.equal(acs.code.value, "test"); assert.instanceOf(acs, AuxCoordSystemSpatialState); acs.setOrigin({ x: 1, y: 1 }); assert.isTrue(acs.getOrigin().isExactEqual({ x: 1, y: 1, z: 0 }));