Add Polygon and Star tools with rendering and flyout shortcuts
- POLYGON (triangle icon) and STAR (star icon) in the shapes flyout - Renderer draws regular polygons and stars using pointCount and starInnerRadius properties on SceneNode - Flyout dropdown now shows keyboard shortcuts (R, L, O) aligned right like Figma's context menu - Polygon defaults to 3 sides, Star to 5 points with 0.38 inner ratio - Fill, stroke, hover highlight, and selection outline all work
This commit is contained in:
parent
af3db9f0f1
commit
2ab75ad25a
|
|
@ -7,6 +7,8 @@ import IconLayoutGrid from '~icons/lucide/layout-grid'
|
|||
import IconSquare from '~icons/lucide/square'
|
||||
import IconCircle from '~icons/lucide/circle'
|
||||
import IconMinus from '~icons/lucide/minus'
|
||||
import IconTriangle from '~icons/lucide/triangle'
|
||||
import IconStar from '~icons/lucide/star'
|
||||
import IconPenTool from '~icons/lucide/pen-tool'
|
||||
import IconType from '~icons/lucide/type'
|
||||
import IconHand from '~icons/lucide/hand'
|
||||
|
|
@ -25,6 +27,8 @@ const toolIcons: Record<Tool, typeof IconSquare> = {
|
|||
RECTANGLE: IconSquare,
|
||||
ELLIPSE: IconCircle,
|
||||
LINE: IconMinus,
|
||||
POLYGON: IconTriangle,
|
||||
STAR: IconStar,
|
||||
PEN: IconPenTool,
|
||||
TEXT: IconType,
|
||||
HAND: IconHand
|
||||
|
|
@ -37,11 +41,27 @@ const toolLabels: Record<Tool, string> = {
|
|||
RECTANGLE: 'Rectangle',
|
||||
ELLIPSE: 'Ellipse',
|
||||
LINE: 'Line',
|
||||
POLYGON: 'Polygon',
|
||||
STAR: 'Star',
|
||||
PEN: 'Pen',
|
||||
TEXT: 'Text',
|
||||
HAND: 'Hand'
|
||||
}
|
||||
|
||||
const toolShortcuts: Record<Tool, string> = {
|
||||
SELECT: 'V',
|
||||
FRAME: 'F',
|
||||
SECTION: 'S',
|
||||
RECTANGLE: 'R',
|
||||
ELLIPSE: 'O',
|
||||
LINE: 'L',
|
||||
POLYGON: '',
|
||||
STAR: '',
|
||||
PEN: 'P',
|
||||
TEXT: 'T',
|
||||
HAND: 'H'
|
||||
}
|
||||
|
||||
function isActive(tool: typeof TOOLS[number]): boolean {
|
||||
if (tool.key === store.state.activeTool) return true
|
||||
return tool.flyout?.includes(store.state.activeTool) ?? false
|
||||
|
|
@ -99,7 +119,8 @@ function activeKeyForTool(tool: typeof TOOLS[number]): Tool {
|
|||
@select="store.setTool(sub)"
|
||||
>
|
||||
<component :is="toolIcons[sub]" class="size-3.5" />
|
||||
{{ toolLabels[sub] }}
|
||||
<span class="flex-1">{{ toolLabels[sub] }}</span>
|
||||
<span v-if="toolShortcuts[sub]" class="text-[11px] text-muted">{{ toolShortcuts[sub] }}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
|
|
|
|||
|
|
@ -835,6 +835,13 @@ export class SkiaRenderer {
|
|||
case 'LINE':
|
||||
canvas.drawLine(0, 0, node.width, node.height, paint)
|
||||
return
|
||||
case 'POLYGON':
|
||||
case 'STAR': {
|
||||
const path = this.makePolygonPath(node)
|
||||
canvas.drawPath(path, paint)
|
||||
path.delete()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const hasRadius =
|
||||
|
|
@ -1181,6 +1188,13 @@ export class SkiaRenderer {
|
|||
case 'LINE':
|
||||
canvas.drawLine(0, 0, node.width, node.height, this.fillPaint)
|
||||
break
|
||||
case 'POLYGON':
|
||||
case 'STAR': {
|
||||
const path = this.makePolygonPath(node)
|
||||
canvas.drawPath(path, this.fillPaint)
|
||||
path.delete()
|
||||
break
|
||||
}
|
||||
default:
|
||||
if (hasRadius) {
|
||||
canvas.drawRRect(this.makeRRect(node), this.fillPaint)
|
||||
|
|
@ -1206,6 +1220,13 @@ export class SkiaRenderer {
|
|||
canvas.drawOval(rect, this.strokePaint)
|
||||
}
|
||||
break
|
||||
case 'POLYGON':
|
||||
case 'STAR': {
|
||||
const path = this.makePolygonPath(node)
|
||||
canvas.drawPath(path, this.strokePaint)
|
||||
path.delete()
|
||||
break
|
||||
}
|
||||
default:
|
||||
if (hasRadius) {
|
||||
canvas.drawRRect(this.makeRRect(node), this.strokePaint)
|
||||
|
|
@ -1215,6 +1236,31 @@ export class SkiaRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
private makePolygonPath(node: SceneNode): Path {
|
||||
const path = new this.ck.Path()
|
||||
const cx = node.width / 2
|
||||
const cy = node.height / 2
|
||||
const rx = node.width / 2
|
||||
const ry = node.height / 2
|
||||
const n = Math.max(3, node.pointCount)
|
||||
const isStar = node.type === 'STAR'
|
||||
const innerRatio = isStar ? node.starInnerRadius : 1
|
||||
const totalPoints = isStar ? n * 2 : n
|
||||
const angleOffset = -Math.PI / 2
|
||||
|
||||
for (let i = 0; i < totalPoints; i++) {
|
||||
const angle = angleOffset + (2 * Math.PI * i) / totalPoints
|
||||
const isInner = isStar && i % 2 === 1
|
||||
const r = isInner ? innerRatio : 1
|
||||
const px = cx + rx * r * Math.cos(angle)
|
||||
const py = cy + ry * r * Math.sin(angle)
|
||||
if (i === 0) path.moveTo(px, py)
|
||||
else path.lineTo(px, py)
|
||||
}
|
||||
path.close()
|
||||
return path
|
||||
}
|
||||
|
||||
private makeRRect(node: SceneNode): Float32Array {
|
||||
if (node.independentCorners) {
|
||||
return new Float32Array([
|
||||
|
|
|
|||
|
|
@ -203,6 +203,9 @@ export interface SceneNode {
|
|||
borderLeftWeight: number
|
||||
independentStrokeWeights: boolean
|
||||
|
||||
pointCount: number
|
||||
starInnerRadius: number
|
||||
|
||||
componentId: string | null
|
||||
overrides: Record<string, unknown>
|
||||
}
|
||||
|
|
@ -279,6 +282,8 @@ function createDefaultNode(type: NodeType, overrides: Partial<SceneNode> = {}):
|
|||
borderBottomWeight: 0,
|
||||
borderLeftWeight: 0,
|
||||
independentStrokeWeights: false,
|
||||
pointCount: 5,
|
||||
starInnerRadius: 0.38,
|
||||
componentId: null,
|
||||
overrides: {},
|
||||
...overrides
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { computeVectorBounds } from '../engine/vector'
|
|||
import type { SceneNode, NodeType, Fill, LayoutMode, VectorVertex, VectorSegment, VectorRegion, VectorNetwork } from '../engine/scene-graph'
|
||||
import type { SnapGuide } from '../engine/snap'
|
||||
|
||||
export type Tool = 'SELECT' | 'FRAME' | 'SECTION' | 'RECTANGLE' | 'ELLIPSE' | 'LINE' | 'TEXT' | 'PEN' | 'HAND'
|
||||
export type Tool = 'SELECT' | 'FRAME' | 'SECTION' | 'RECTANGLE' | 'ELLIPSE' | 'LINE' | 'POLYGON' | 'STAR' | 'TEXT' | 'PEN' | 'HAND'
|
||||
|
||||
export interface ToolDef {
|
||||
key: Tool
|
||||
|
|
@ -32,7 +32,7 @@ export interface ToolDef {
|
|||
export const TOOLS: ToolDef[] = [
|
||||
{ key: 'SELECT', label: 'Move', shortcut: 'V' },
|
||||
{ key: 'FRAME', label: 'Frame', shortcut: 'F', flyout: ['FRAME', 'SECTION'] },
|
||||
{ key: 'RECTANGLE', label: 'Rectangle', shortcut: 'R', flyout: ['RECTANGLE', 'ELLIPSE', 'LINE'] },
|
||||
{ key: 'RECTANGLE', label: 'Rectangle', shortcut: 'R', flyout: ['RECTANGLE', 'LINE', 'ELLIPSE', 'POLYGON', 'STAR'] },
|
||||
{ key: 'PEN', label: 'Pen', shortcut: 'P' },
|
||||
{ key: 'TEXT', label: 'Text', shortcut: 'T' },
|
||||
{ key: 'HAND', label: 'Hand', shortcut: 'H' }
|
||||
|
|
@ -57,6 +57,8 @@ const DEFAULT_FILLS: Record<string, Fill> = {
|
|||
SECTION: SECTION_DEFAULT_FILL,
|
||||
RECTANGLE: DEFAULT_SHAPE_FILL,
|
||||
ELLIPSE: DEFAULT_SHAPE_FILL,
|
||||
POLYGON: DEFAULT_SHAPE_FILL,
|
||||
STAR: DEFAULT_SHAPE_FILL,
|
||||
LINE: BLACK_FILL,
|
||||
TEXT: BLACK_FILL
|
||||
}
|
||||
|
|
@ -1195,6 +1197,13 @@ export function createEditorStore() {
|
|||
overrides.strokes = [{ ...SECTION_DEFAULT_STROKE }]
|
||||
overrides.cornerRadius = 5
|
||||
}
|
||||
if (type === 'POLYGON') {
|
||||
overrides.pointCount = 3
|
||||
}
|
||||
if (type === 'STAR') {
|
||||
overrides.pointCount = 5
|
||||
overrides.starInnerRadius = 0.38
|
||||
}
|
||||
const node = graph.createNode(type, pid, overrides)
|
||||
const id = node.id
|
||||
const snapshot = { ...node }
|
||||
|
|
|
|||
Loading…
Reference in a new issue