diff --git a/.changeset/many-humans-pay.md b/.changeset/many-humans-pay.md new file mode 100644 index 00000000..6535a8f5 --- /dev/null +++ b/.changeset/many-humans-pay.md @@ -0,0 +1,5 @@ +--- +'@viamrobotics/motion-tools': minor +--- + +enable hoverlinking for pcds and poses diff --git a/package.json b/package.json index e531cdd4..2742245c 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "@bufbuild/protobuf": "1.10.1", "@neodrag/svelte": "^2.3.3", "@tanstack/svelte-query-devtools": "^6.0.2", + "expr-eval": "^2.0.2", "koota": "0.6.5", "lodash-es": "4.17.23", "uuid-tool": "^2.0.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa06b8c3..4e726102 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,10 +16,13 @@ importers: version: 2.3.3(svelte@5.45.9) '@tanstack/svelte-query-devtools': specifier: ^6.0.2 - version: 6.0.2(@tanstack/svelte-query@6.0.9(svelte@5.45.9))(svelte@5.45.9) + version: 6.0.2(@tanstack/svelte-query@6.0.18(svelte@5.45.9))(svelte@5.45.9) '@zag-js/dialog': specifier: '>=1.31' version: 1.32.0 + expr-eval: + specifier: ^2.0.2 + version: 2.0.2 koota: specifier: 0.6.5 version: 0.6.5(@types/react@19.2.2) @@ -1228,11 +1231,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.6': - resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==} - peerDependencies: - acorn: ^8.9.0 - '@sveltejs/acorn-typescript@1.0.8': resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} peerDependencies: @@ -1376,8 +1374,8 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 - '@tanstack/query-core@5.90.11': - resolution: {integrity: sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==} + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} '@tanstack/query-devtools@5.91.1': resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==} @@ -1388,8 +1386,8 @@ packages: '@tanstack/svelte-query': ^6.0.8 svelte: ^5.25.0 - '@tanstack/svelte-query@6.0.9': - resolution: {integrity: sha512-ezawzencc07h61M+p8R9Opp2CmpgGwrM05IsIGJiPkr1SrBPW8gDZ9sTdaQbEpzLNXMXaZUkq0MS+61Rw2EfSg==} + '@tanstack/svelte-query@6.0.18': + resolution: {integrity: sha512-iGS8osfrIVUW5pkV4Ig6pspNIMtiNjGnVTNJKDas0m/QaNDFFIKbgg74rCzcjwrTIvO38tMpzb4VUKklvAmjxw==} peerDependencies: svelte: ^5.25.0 @@ -1906,8 +1904,8 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} - ast-v8-to-istanbul@0.3.8: - resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==} + ast-v8-to-istanbul@0.3.11: + resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -2134,9 +2132,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.5.0: - resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} - devalue@5.6.2: resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} @@ -2326,6 +2321,9 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + expr-eval@2.0.2: + resolution: {integrity: sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2634,6 +2632,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5060,10 +5061,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': - dependencies: - acorn: 8.15.0 - '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 @@ -5202,20 +5199,20 @@ snapshots: tailwindcss: 4.1.13 vite: 7.1.11(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.5)(yaml@2.8.1) - '@tanstack/query-core@5.90.11': {} + '@tanstack/query-core@5.90.20': {} '@tanstack/query-devtools@5.91.1': {} - '@tanstack/svelte-query-devtools@6.0.2(@tanstack/svelte-query@6.0.9(svelte@5.45.9))(svelte@5.45.9)': + '@tanstack/svelte-query-devtools@6.0.2(@tanstack/svelte-query@6.0.18(svelte@5.45.9))(svelte@5.45.9)': dependencies: '@tanstack/query-devtools': 5.91.1 - '@tanstack/svelte-query': 6.0.9(svelte@5.45.9) + '@tanstack/svelte-query': 6.0.18(svelte@5.45.9) esm-env: 1.2.2 svelte: 5.45.9 - '@tanstack/svelte-query@6.0.9(svelte@5.45.9)': + '@tanstack/svelte-query@6.0.18(svelte@5.45.9)': dependencies: - '@tanstack/query-core': 5.90.11 + '@tanstack/query-core': 5.90.20 svelte: 5.45.9 '@testing-library/dom@10.4.1': @@ -5482,8 +5479,8 @@ snapshots: '@viamrobotics/svelte-sdk@1.0.1(@viamrobotics/sdk@0.58.0)(svelte@5.45.9)': dependencies: - '@tanstack/svelte-query': 6.0.9(svelte@5.45.9) - '@tanstack/svelte-query-devtools': 6.0.2(@tanstack/svelte-query@6.0.9(svelte@5.45.9))(svelte@5.45.9) + '@tanstack/svelte-query': 6.0.18(svelte@5.45.9) + '@tanstack/svelte-query-devtools': 6.0.2(@tanstack/svelte-query@6.0.18(svelte@5.45.9))(svelte@5.45.9) '@viamrobotics/sdk': 0.58.0 runed: 0.29.2(svelte@5.45.9) svelte: 5.45.9 @@ -5496,7 +5493,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.8 + ast-v8-to-istanbul: 0.3.11 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -5974,11 +5971,11 @@ snapshots: dependencies: tslib: 2.8.1 - ast-v8-to-istanbul@0.3.8: + ast-v8-to-istanbul@0.3.11: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 - js-tokens: 9.0.1 + js-tokens: 10.0.0 asynckit@0.4.0: {} @@ -6174,8 +6171,6 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.5.0: {} - devalue@5.6.2: {} didyoumean@1.2.2: {} @@ -6401,6 +6396,8 @@ snapshots: exponential-backoff@3.1.3: {} + expr-eval@2.0.2: {} + extend@3.0.2: {} extendable-error@0.1.7: {} @@ -6704,6 +6701,8 @@ snapshots: jiti@2.6.1: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -7443,13 +7442,13 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) '@types/estree': 1.0.8 acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.5.0 + devalue: 5.6.2 esm-env: 1.2.2 esrap: 2.2.1 is-reference: 3.0.3 diff --git a/src/lib/HoverUpdater.svelte.ts b/src/lib/HoverUpdater.svelte.ts new file mode 100644 index 00000000..69e4c6a9 --- /dev/null +++ b/src/lib/HoverUpdater.svelte.ts @@ -0,0 +1,156 @@ +import { Vector3 } from 'three' +import type { Entity } from 'koota' +import { traits } from '$lib/ecs' +import type { IntersectionEvent } from '@threlte/extras' + +export interface HoverInfo { + index: number + x: number + y: number + z: number + oX: number + oY: number + oZ: number + theta: number +} + +const hoverPosition = new Vector3() + +export const getClosestArrow = (positions: Float32Array, point: Vector3): HoverInfo => { + let smallestDistance = Infinity + let index = -1 + + for (let i = 0; i < positions.length; i += 6) { + const x = positions[i] / 1000 + const y = positions[i + 1] / 1000 + const z = positions[i + 2] / 1000 + + const distance = point.distanceToSquared({ x, y, z }) + + if (distance < smallestDistance) { + smallestDistance = distance + index = i + } + } + + return { + index: Math.floor(index / 6), + x: positions[index] / 1000, + y: positions[index + 1] / 1000, + z: positions[index + 2] / 1000, + oX: positions[index + 3], + oY: positions[index + 4], + oZ: positions[index + 5], + theta: 0, + } +} + +export const getClosestPoint = (positions: Float32Array, point: Vector3): HoverInfo => { + let smallestDistance = Infinity + let index = -1 + + for (let i = 0; i < positions.length; i += 3) { + const x = positions[i] + const y = positions[i + 1] + const z = positions[i + 2] + + const distance = point.distanceToSquared({ x, y, z }) + + if (distance < smallestDistance) { + smallestDistance = distance + index = i + } + } + + return { + index: Math.floor(index / 3), + x: positions[index], + y: positions[index + 1], + z: positions[index + 2], + oX: 0, + oY: 0, + oZ: 0, + theta: 0, + } +} + +export const getPointAtIndex = (positions: Float32Array, index: number): HoverInfo | null => { + if (index < 0 || index >= positions.length / 3) { + return null + } + return { + index, + x: positions[index * 3], + y: positions[index * 3 + 1], + z: positions[index * 3 + 2], + oX: 0, + oY: 0, + oZ: 0, + theta: 0, + } +} +export const getArrowAtIndex = (positions: Float32Array, index: number): HoverInfo | null => { + if (index < 0 || index >= positions.length / 6) { + return null + } + return { + index, + x: positions[index * 6] / 1000, + y: positions[index * 6 + 1] / 1000, + z: positions[index * 6 + 2] / 1000, + oX: positions[index * 6 + 3], + oY: positions[index * 6 + 4], + oZ: positions[index * 6 + 5], + theta: 0, + } +} + +export const updateHoverInfo = ( + entity: Entity, + hoverEvent: IntersectionEvent +): HoverInfo | null => { + const { index, point } = hoverEvent + if (index === -1) { + return null + } + + hoverPosition.set(point.x, point.y, point.z) + + let hoverInfo: HoverInfo | null = null + + if (entity.has(traits.Arrows)) { + const closestArrow = getClosestArrow( + entity.get(traits.Positions) as Float32Array, + hoverPosition + ) + if (closestArrow) { + hoverInfo = closestArrow + } + } else if (entity.has(traits.Points)) { + const positions = entity.get(traits.BufferGeometry)?.attributes.position.array as Float32Array + const closestPoint = getClosestPoint(positions, hoverPosition) + if (closestPoint) { + hoverInfo = closestPoint + } + } + + return hoverInfo +} + +export const getLinkedHoverInfo = (index: number, linkedEntity: Entity): HoverInfo | null => { + if (linkedEntity.has(traits.Arrows)) { + const closestArrow = getArrowAtIndex(linkedEntity.get(traits.Positions) as Float32Array, index) + if (closestArrow) { + return closestArrow + } + } else if (linkedEntity.has(traits.Points)) { + const positions = linkedEntity.get(traits.BufferGeometry)?.attributes.position + .array as Float32Array + const closestPoint = getPointAtIndex(positions, index) + if (closestPoint) { + return closestPoint + } + } + + return null +} diff --git a/src/lib/components/App.svelte b/src/lib/components/App.svelte index a2b099ba..f5d2dcf6 100644 --- a/src/lib/components/App.svelte +++ b/src/lib/components/App.svelte @@ -27,7 +27,7 @@ type DrawConnectionConfig, } from '$lib/hooks/useDrawConnectionConfig.svelte' import Camera from './overlay/widgets/Camera.svelte' - import HoveredEntities from './HoveredEntities.svelte' + import HoveredEntities from './hover/HoveredEntities.svelte' interface LocalConfigProps { getLocalPartConfig: () => Struct diff --git a/src/lib/components/HoveredEntities.svelte b/src/lib/components/HoveredEntities.svelte deleted file mode 100644 index 12dee849..00000000 --- a/src/lib/components/HoveredEntities.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -{#each hoveredEntities.current as entity (entity)} - {#if entity === displayEntity} - - {/if} -{/each} diff --git a/src/lib/components/HoveredEntityTooltip.svelte b/src/lib/components/HoveredEntityTooltip.svelte deleted file mode 100644 index 4f67ac86..00000000 --- a/src/lib/components/HoveredEntityTooltip.svelte +++ /dev/null @@ -1,242 +0,0 @@ - - - - -{#if tooltipData?.subEntityPosition} - -
- -
- -
- {#if tooltipData.closestArrow} -
-
index
-
{tooltipData.closestArrow.index}
-
- -
-
- world position - (m) -
-
-
- x {tooltipData.closestArrow.x.toFixed(2)} -
-
- y {tooltipData.closestArrow.y.toFixed(2)} -
-
- z {tooltipData.closestArrow.z.toFixed(2)} -
-
-
- -
-
- world orientation - (deg) -
-
-
- x {tooltipData.closestArrow.oX.toFixed(2)} -
-
- y {tooltipData.closestArrow.oY.toFixed(2)} -
-
- z {tooltipData.closestArrow.oZ.toFixed(2)} -
-
-
- {/if} - - {#if tooltipData.closestPoint} -
-
index
-
{tooltipData.closestPoint.index}
-
- -
-
- world position - (m) -
-
-
- x {tooltipData.closestPoint.x.toFixed(2)} -
-
- y {tooltipData.closestPoint.y.toFixed(2)} -
-
- z {tooltipData.closestPoint.z.toFixed(2)} -
-
-
- {/if} -
-
- -{/if} diff --git a/src/lib/components/SceneProviders.svelte b/src/lib/components/SceneProviders.svelte index c6cadca0..bb0d7a3d 100644 --- a/src/lib/components/SceneProviders.svelte +++ b/src/lib/components/SceneProviders.svelte @@ -21,6 +21,7 @@ import { provideResourceByName } from '$lib/hooks/useResourceByName.svelte' import { provide3DModels } from '$lib/hooks/use3DModels.svelte' import { providePointcloudObjects } from '$lib/hooks/usePointcloudObjects.svelte' + import { provideLinkedEntities } from '$lib/hooks/useLinked.svelte' interface Props { cameraPose?: CameraPose @@ -51,6 +52,7 @@ provideFramelessComponents() const { focus } = provideSelection() + provideLinkedEntities() {@render children({ focus: focus.current !== undefined })} diff --git a/src/lib/components/hover/HoveredEntities.svelte b/src/lib/components/hover/HoveredEntities.svelte new file mode 100644 index 00000000..8558363f --- /dev/null +++ b/src/lib/components/hover/HoveredEntities.svelte @@ -0,0 +1,23 @@ + + +{#if isHovered} + + + {#each linkedEntities.current as entity (entity)} + + {/each} +{/if} diff --git a/src/lib/components/hover/HoveredEntity.svelte b/src/lib/components/hover/HoveredEntity.svelte new file mode 100644 index 00000000..1122f87e --- /dev/null +++ b/src/lib/components/hover/HoveredEntity.svelte @@ -0,0 +1,15 @@ + + +{#if hoverInfo.current} + +{/if} diff --git a/src/lib/components/hover/HoveredEntityTooltip.svelte b/src/lib/components/hover/HoveredEntityTooltip.svelte new file mode 100644 index 00000000..39d538ed --- /dev/null +++ b/src/lib/components/hover/HoveredEntityTooltip.svelte @@ -0,0 +1,70 @@ + + +{#if hoverInfo} + +
+ +
+ +
+
+
index
+
{hoverInfo.index}
+
+ +
+
+ world position + (m) +
+
+
+ x {hoverInfo.x.toFixed(2)} +
+
+ y {hoverInfo.y.toFixed(2)} +
+
+ z {hoverInfo.z.toFixed(2)} +
+
+
+ +
+
+ world orientation + (deg) +
+
+
+ x {hoverInfo.oX.toFixed(2)} +
+
+ y {hoverInfo.oY.toFixed(2)} +
+
+ z {hoverInfo.oZ.toFixed(2)} +
+
+
+
+
+ +{/if} diff --git a/src/lib/components/hover/LinkedHoveredEntity.svelte b/src/lib/components/hover/LinkedHoveredEntity.svelte new file mode 100644 index 00000000..6f0635ed --- /dev/null +++ b/src/lib/components/hover/LinkedHoveredEntity.svelte @@ -0,0 +1,55 @@ + + + + +{#if hoverInfo} + +{/if} diff --git a/src/lib/components/overlay/AddRelationship.svelte b/src/lib/components/overlay/AddRelationship.svelte new file mode 100644 index 00000000..01339943 --- /dev/null +++ b/src/lib/components/overlay/AddRelationship.svelte @@ -0,0 +1,131 @@ + + + + +{#if showRelationshipOptions} +
+
+ + +
+
+ + +
+
+ + e.stopPropagation()} + id="relationship-formula-input" + aria-label="Math formula for index mapping" + bind:value={relationshipFormula} + placeholder="index" + /> +
+
+ +
+
+{/if} diff --git a/src/lib/components/overlay/Details.svelte b/src/lib/components/overlay/Details.svelte index cfe5473e..13fe0bb5 100644 --- a/src/lib/components/overlay/Details.svelte +++ b/src/lib/components/overlay/Details.svelte @@ -25,9 +25,11 @@ import { usePartConfig } from '$lib/hooks/usePartConfig.svelte' import { FrameConfigUpdater } from '$lib/FrameConfigUpdater.svelte' import { useEnvironment } from '$lib/hooks/useEnvironment.svelte' - import { traits, useTrait, useWorld } from '$lib/ecs' + import { traits, useTrait, useWorld, relations } from '$lib/ecs' import { useResourceByName } from '$lib/hooks/useResourceByName.svelte' import { useCameraControls } from '$lib/hooks/useControls.svelte' + import { useLinkedEntities } from '$lib/hooks/useLinked.svelte' + import AddRelationship from '$lib/components/overlay/AddRelationship.svelte' const { ...rest } = $props() @@ -45,7 +47,7 @@ const object3d = $derived(focusedObject3d.current ?? selectedObject3d.current) const worldPosition = $state({ x: 0, y: 0, z: 0 }) const worldOrientation = $state({ x: 0, y: 0, z: 1, th: 0 }) - + const linkedEntities = useLinkedEntities() const name = useTrait(() => entity, traits.Name) const parent = useTrait(() => entity, traits.Parent) const localPose = useTrait(() => entity, traits.EditedPose) @@ -53,11 +55,14 @@ const sphere = useTrait(() => entity, traits.Sphere) const capsule = useTrait(() => entity, traits.Capsule) const removable = useTrait(() => entity, traits.Removable) + const points = useTrait(() => entity, traits.Points) + const arrows = useTrait(() => entity, traits.Arrows) const framesAPI = useTrait(() => entity, traits.FramesAPI) const isFrameNode = $derived(!!framesAPI.current) const showEditFrameOptions = $derived(isFrameNode && partConfig.hasEditPermissions) + const showRelationshipOptions = $derived(points.current || arrows.current) const resourceName = $derived(name.current ? resourceByName.current[name.current] : undefined) @@ -611,6 +616,30 @@ {/if} +

Relationships

+ + {#if linkedEntities.current.length > 0} +
+
+ Linked entities + {#each linkedEntities.current as linkedEntity (linkedEntity)} + {@const linkedEntityName = linkedEntity.get(traits.Name)} + {@const linkType = entity.get(relations.SubEntityLink(linkedEntity))?.type} +
+ {linkedEntityName} ({linkType}) + { + entity.remove(relations.SubEntityLink(linkedEntity)) + }} + /> +
+ {/each} +
+
+ {/if} +

Actions

{#if focusedEntity.current} @@ -632,6 +661,10 @@ {/if} + {#if showRelationshipOptions} + + {/if} + {#if showEditFrameOptions && environment.current.isStandalone}