Avoid placing a new node over a visualization (#8412)

- Closes #8368

# Important Notes
Could not repro the leftwards drift of new nodes. It is possible that this has already been fixed by another PR.
This commit is contained in:
somebody1234 2023-11-29 13:22:50 +10:00 committed by GitHub
parent 05d613fdc9
commit 195faed9e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 74 additions and 21 deletions

View File

@ -65,7 +65,7 @@ const interactionBindingsHandler = interactionBindings.handler({
// or the node that is being edited when creating from a port double click.
function environmentForNodes(nodeIds: IterableIterator<ExprId>): Environment {
const nodeRects = [...graphStore.nodeRects.values()]
const selectedNodeRects: Iterable<Rect> = [...nodeIds]
const selectedNodeRects = [...nodeIds]
.map((id) => graphStore.nodeRects.get(id))
.filter((item): item is Rect => item !== undefined)
const screenBounds = graphNavigator.viewport
@ -73,15 +73,13 @@ function environmentForNodes(nodeIds: IterableIterator<ExprId>): Environment {
return { nodeRects, selectedNodeRects, screenBounds, mousePosition } as Environment
}
const placementEnvironment = computed(() => {
return environmentForNodes(nodeSelection.selected.values())
})
const placementEnvironment = computed(() => environmentForNodes(nodeSelection.selected.values()))
// Return the position for a new node, assuming there are currently nodes selected. If there are no nodes
// selected, return undefined.
/** Return the position for a new node, assuming there are currently nodes selected. If there are no nodes
* selected, return `undefined`. */
function placementPositionForSelection() {
const hasNodeSelected = nodeSelection.selected.size > 0
if (!hasNodeSelected) return undefined
if (!hasNodeSelected) return
const gapBetweenNodes = 48.0
return previousNodeDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value, {
horizontalGap: gapBetweenNodes,
@ -98,12 +96,10 @@ function targetComponentBrowserPosition() {
const targetPos = targetNode?.position ?? Vec2.Zero
return targetPos.add(COMPONENT_BROWSER_TO_NODE_OFFSET)
} else {
const targetPos = placementPositionForSelection()
if (targetPos != undefined) {
return targetPos
} else {
return mouseDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value).position
}
return (
placementPositionForSelection() ??
mouseDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value).position
)
}
}

View File

@ -27,6 +27,7 @@ const props = defineProps<{
const emit = defineEmits<{
updateRect: [rect: Rect]
'update:vizRect': [rect: Rect | undefined]
updateContent: [updates: [range: ContentRange, content: string][]]
dragging: [offset: Vec2]
draggingCommited: []
@ -267,12 +268,14 @@ function portGroupStyle(port: PortData) {
<GraphVisualization
v-if="isVisualizationVisible"
:nodeSize="nodeSize"
:nodePosition="props.node.position"
:isCircularMenuVisible="menuVisible"
:currentType="props.node.vis"
:expressionId="props.node.rootSpan.astId"
:typename="expressionInfo?.typename"
@setVisualizationId="emit('setVisualizationId', $event)"
@setVisualizationVisible="emit('setVisualizationVisible', $event)"
@update:rect="emit('update:vizRect', $event)"
/>
<div
class="node"

View File

@ -55,6 +55,7 @@ const uploadingFiles = computed<[FileName, File][]>(() => {
:edited="id === graphStore.editedNodeInfo?.id"
@update:edited="graphStore.setEditedNode(id, $event)"
@updateRect="graphStore.updateNodeRect(id, $event)"
@update:vizRect="graphStore.updateVizRect(id, $event)"
@delete="graphStore.deleteNode"
@pointerenter="hoverNode(id)"
@pointerleave="hoverNode(undefined)"

View File

@ -13,25 +13,31 @@ import type { URLString } from '@/stores/visualization/compilerMessaging'
import { toError } from '@/util/error'
import type { Icon } from '@/util/iconName'
import type { Opt } from '@/util/opt'
import type { Vec2 } from '@/util/vec2'
import { Rect } from '@/util/rect'
import { Vec2 } from '@/util/vec2'
import type { ExprId, VisualizationIdentifier } from 'shared/yjsModel'
import { computed, onErrorCaptured, ref, shallowRef, watch, watchEffect } from 'vue'
import { computed, onErrorCaptured, onUnmounted, ref, shallowRef, watch, watchEffect } from 'vue'
const visPreprocessor = ref(DEFAULT_VISUALIZATION_CONFIGURATION)
const error = ref<Error>()
const TOP_WITHOUT_TOOLBAR_PX = 36
const TOP_WITH_TOOLBAR_PX = 72
const projectStore = useProjectStore()
const visualizationStore = useVisualizationStore()
const props = defineProps<{
currentType: Opt<VisualizationIdentifier>
isCircularMenuVisible: boolean
nodePosition: Vec2
nodeSize: Vec2
typename?: string | undefined
expressionId?: ExprId | undefined
data?: any | undefined
}>()
const emit = defineEmits<{
'update:rect': [rect: Rect | undefined]
setVisualizationId: [id: VisualizationIdentifier]
setVisualizationVisible: [visible: boolean]
}>()
@ -117,10 +123,45 @@ watchEffect(async () => {
}
})
const isBelowToolbar = ref(false)
let width = ref<number | null>(null)
let height = ref(150)
watchEffect(() =>
emit(
'update:rect',
new Rect(
props.nodePosition,
new Vec2(
width.value ?? props.nodeSize.x,
height.value + (isBelowToolbar.value ? TOP_WITH_TOOLBAR_PX : TOP_WITHOUT_TOOLBAR_PX),
),
),
),
)
onUnmounted(() => emit('update:rect', undefined))
provideVisualizationConfig({
fullscreen: false,
width: null,
height: 150,
get width() {
return width.value
},
set width(value) {
width.value = value
},
get height() {
return height.value
},
set height(value) {
height.value = value
},
get isBelowToolbar() {
return isBelowToolbar.value
},
set isBelowToolbar(value) {
isBelowToolbar.value = value
},
get types() {
return visualizationStore.types(props.typename)
},

View File

@ -3,7 +3,7 @@ import SvgIcon from '@/components/SvgIcon.vue'
import VisualizationSelector from '@/components/VisualizationSelector.vue'
import { useVisualizationConfig } from '@/providers/visualizationConfig'
import { PointerButtonMask, isClick, usePointer } from '@/util/events'
import { ref } from 'vue'
import { ref, watchEffect } from 'vue'
const props = defineProps<{
/** If true, the visualization should be `overflow: visible` instead of `overflow: hidden`. */
@ -16,6 +16,8 @@ const props = defineProps<{
const config = useVisualizationConfig()
watchEffect(() => (config.isBelowToolbar = props.belowToolbar))
const isSelectorVisible = ref(false)
function onWheel(event: WheelEvent) {

View File

@ -13,8 +13,9 @@ export interface VisualizationConfig {
readonly icon: Icon | URLString | undefined
readonly isCircularMenuVisible: boolean
readonly nodeSize: Vec2
isBelowToolbar: boolean
width: number | null
height: number | null
height: number
fullscreen: boolean
hide: () => void
updateType: (type: VisualizationIdentifier) => void

View File

@ -52,6 +52,7 @@ export const useGraphStore = defineStore('graph', () => {
proj.computedValueRegistry,
)
const nodeRects = reactive(new Map<ExprId, Rect>())
const vizRects = reactive(new Map<ExprId, Rect>())
const exprRects = reactive(new Map<ExprId, Rect>())
const editedNodeInfo = ref<NodeEditInfo>()
const imports = ref<{ import: Import; span: ContentRange }[]>([])
@ -295,7 +296,7 @@ export const useGraphStore = defineStore('graph', () => {
const { position } = nonDictatedPlacement(rect.size, {
nodeRects: [...nodeRects.entries()]
.filter(([id]) => db.nodeIdToNode.get(id))
.map(([, rect]) => rect),
.map(([id, rect]) => vizRects.get(id) ?? rect),
// The rest of the properties should not matter.
selectedNodeRects: [],
screenBounds: Rect.Zero,
@ -308,6 +309,11 @@ export const useGraphStore = defineStore('graph', () => {
}
}
function updateVizRect(id: ExprId, rect: Rect | undefined) {
if (rect) vizRects.set(id, rect)
else vizRects.delete(id)
}
function updateExprRect(id: ExprId, rect: Rect | undefined) {
const current = exprRects.get(id)
if (rect) {
@ -338,6 +344,7 @@ export const useGraphStore = defineStore('graph', () => {
unconnectedEdge,
edges,
nodeRects,
vizRects,
exprRects,
createEdgeFromOutput,
disconnectSource,
@ -353,6 +360,7 @@ export const useGraphStore = defineStore('graph', () => {
setNodeVisualizationVisible,
stopCapturingUndo,
updateNodeRect,
updateVizRect,
updateExprRect,
setEditedNode,
createNodeFromSource,

View File

@ -109,7 +109,7 @@ export function useSelection<T>(
overrideElemsToSelect.value = undefined
}
const pointer = usePointer((pos, event, eventType) => {
const pointer = usePointer((_pos, event, eventType) => {
if (eventType === 'start') {
readInitiallySelected()
} else if (pointer.dragging && anchor.value == null) {
@ -120,6 +120,7 @@ export function useSelection<T>(
}
selectionEventHandler(event)
})
return proxyRefs({
selected,
anchor,