diff --git a/packages/core/src/editor/structure.ts b/packages/core/src/editor/structure.ts index e744b2c40..daa9fcbd0 100644 --- a/packages/core/src/editor/structure.ts +++ b/packages/core/src/editor/structure.ts @@ -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 { diff --git a/packages/core/src/scene-graph-hit-test.ts b/packages/core/src/scene-graph-hit-test.ts index 851fe2f4f..e093f338d 100644 --- a/packages/core/src/scene-graph-hit-test.ts +++ b/packages/core/src/scene-graph-hit-test.ts @@ -1,3 +1,5 @@ +import { degToRad } from './geometry' + import type { SceneGraph, SceneNode, NodeType } from './scene-graph' const CONTAINER_TYPES = new Set([ @@ -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( diff --git a/src/components/LayerTree.vue b/src/components/LayerTree.vue index c2e6fd950..3c6971450 100644 --- a/src/components/LayerTree.vue +++ b/src/components/LayerTree.vue @@ -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" /> diff --git a/src/components/PagesPanel.vue b/src/components/PagesPanel.vue index bba2e2bb4..db5337977 100644 --- a/src/components/PagesPanel.vue +++ b/src/components/PagesPanel.vue @@ -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" />
{ 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() diff --git a/tests/engine/hit-test-scope.test.ts b/tests/engine/hit-test-scope.test.ts index 4dc0542b7..3d854f1ac 100644 --- a/tests/engine/hit-test-scope.test.ts +++ b/tests/engine/hit-test-scope.test.ts @@ -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)', () => {