Fix inline rename and rotated hit testing
This commit is contained in:
parent
69784953fb
commit
b826202b09
|
|
@ -1,10 +1,14 @@
|
|||
import { computeAbsoluteBounds } from '../geometry'
|
||||
import { computeLayout } from '../layout'
|
||||
|
||||
import type { LayoutMode, SceneNode } from '../scene-graph'
|
||||
import type { LayoutMode, NodeType, SceneNode } from '../scene-graph'
|
||||
import type { EditorContext } from './types'
|
||||
|
||||
export function createStructureActions(ctx: EditorContext) {
|
||||
function defaultNodeName(type: NodeType): string {
|
||||
return type.charAt(0) + type.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
function isTopLevel(parentId: string | null): boolean {
|
||||
return !parentId || parentId === ctx.graph.rootId || parentId === ctx.state.currentPageId
|
||||
}
|
||||
|
|
@ -376,7 +380,10 @@ export function createStructureActions(ctx: EditorContext) {
|
|||
}
|
||||
|
||||
function renameNode(id: string, name: string) {
|
||||
ctx.graph.updateNode(id, { name })
|
||||
const node = ctx.graph.getNode(id)
|
||||
if (!node) return
|
||||
const trimmedName = name.trim()
|
||||
ctx.graph.updateNode(id, { name: trimmedName || defaultNodeName(node.type) })
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { degToRad } from './geometry'
|
||||
|
||||
import type { SceneGraph, SceneNode, NodeType } from './scene-graph'
|
||||
|
||||
const CONTAINER_TYPES = new Set<NodeType>([
|
||||
|
|
@ -16,7 +18,21 @@ function hasVisibleFillOrStroke(node: SceneNode): boolean {
|
|||
}
|
||||
|
||||
function containsPoint(px: number, py: number, ax: number, ay: number, node: SceneNode): boolean {
|
||||
return px >= ax && px <= ax + node.width && py >= ay && py <= ay + node.height
|
||||
if (node.rotation === 0) {
|
||||
return px >= ax && px <= ax + node.width && py >= ay && py <= ay + node.height
|
||||
}
|
||||
|
||||
const cx = ax + node.width / 2
|
||||
const cy = ay + node.height / 2
|
||||
const dx = px - cx
|
||||
const dy = py - cy
|
||||
const rad = degToRad(-node.rotation)
|
||||
const cos = Math.cos(rad)
|
||||
const sin = Math.sin(rad)
|
||||
const localX = dx * cos - dy * sin + node.width / 2
|
||||
const localY = dx * sin + dy * cos + node.height / 2
|
||||
|
||||
return localX >= 0 && localX <= node.width && localY >= 0 && localY <= node.height
|
||||
}
|
||||
|
||||
function hitTestOpaqueContainer(
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function onLayerRightClick(e: MouseEvent) {
|
|||
class="min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-xs text-surface outline-none"
|
||||
:value="node.name"
|
||||
@blur="rename.commit(node.id, $event.target as HTMLInputElement)"
|
||||
@keydown="rename.onKeydown"
|
||||
@keydown.stop="rename.onKeydown"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ function handlePageDblClick(
|
|||
class="min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-xs text-surface outline-none"
|
||||
:value="pg.name"
|
||||
@blur="rename.commit(pg.id, $event.target as HTMLInputElement)"
|
||||
@keydown="rename.onKeydown"
|
||||
@keydown.stop="rename.onKeydown"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -194,6 +194,31 @@ test('clicking outside rename input commits', async () => {
|
|||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('clearing a layer name falls back to the default node name', async () => {
|
||||
await canvas.drawRect(980, 600, 50, 50)
|
||||
await canvas.waitForRender()
|
||||
|
||||
const row = layerRows().filter({ hasText: 'Rectangle' }).last()
|
||||
const countBefore = await layerRows().count()
|
||||
|
||||
await row.dblclick()
|
||||
|
||||
const input = page.locator('[data-test-id="layers-item-input"]')
|
||||
await expect(input).toBeVisible()
|
||||
await input.clear()
|
||||
await input.press('Enter')
|
||||
|
||||
await canvas.waitForRender()
|
||||
|
||||
const countAfter = await layerRows().count()
|
||||
expect(countAfter).toBe(countBefore)
|
||||
|
||||
const names = await getLayerNames()
|
||||
expect(names.filter((name) => name === 'Rectangle').length).toBeGreaterThan(0)
|
||||
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('double-click does not toggle tree expand', async () => {
|
||||
const rowCountBefore = await layerRows().count()
|
||||
|
||||
|
|
|
|||
|
|
@ -257,6 +257,33 @@ describe('hitTest — frame with children', () => {
|
|||
expect(hit).not.toBeNull()
|
||||
expect(hit!.id).toBe(child.id)
|
||||
})
|
||||
|
||||
test('rotated frame scope hit test finds children using rotated local bounds', () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = pageId(graph)
|
||||
const frame = graph.createNode('FRAME', page, {
|
||||
name: 'RotatedFrame',
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 200,
|
||||
height: 120,
|
||||
rotation: 45
|
||||
})
|
||||
const child = graph.createNode('RECTANGLE', frame.id, {
|
||||
name: 'InnerRect',
|
||||
x: 60,
|
||||
y: 30,
|
||||
width: 80,
|
||||
height: 40
|
||||
})
|
||||
|
||||
const hitInside = graph.hitTest(100, 60, frame.id)
|
||||
expect(hitInside).not.toBeNull()
|
||||
expect(hitInside!.id).toBe(child.id)
|
||||
|
||||
const hitOutside = graph.hitTest(10, 10, frame.id)
|
||||
expect(hitOutside).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('hitTest — opaque containers (COMPONENT/INSTANCE)', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue