@agent-native/core 0.80.0 → 0.80.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +16 -1
- package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +14 -1
- package/corpus/core/docs/content/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/template-design.mdx +14 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/handlers.ts +1 -1
- package/corpus/core/src/brand-kit/index.ts +1 -1
- package/corpus/core/src/cli/skills.ts +18 -0
- package/corpus/core/src/client/AgentPanel.tsx +1 -1
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
- package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
- package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
- package/corpus/core/src/extensions/path.ts +1 -4
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
- package/corpus/core/src/mcp/build-server.ts +2 -2
- package/corpus/core/src/mcp/embed-app.ts +35 -1
- package/corpus/core/src/org/handlers.ts +5 -5
- package/corpus/core/src/org/plugin.ts +2 -2
- package/corpus/core/src/scripts/call-agent.ts +1 -1
- package/corpus/core/src/scripts/db/safety.ts +1 -1
- package/corpus/core/src/server/auth.ts +3 -0
- package/corpus/core/src/server/builder-design-systems.ts +308 -0
- package/corpus/core/src/server/core-routes-plugin.ts +29 -3
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/core/src/styles/agent-native.css +14 -7
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
- package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +37 -0
- package/corpus/templates/analytics/app/i18n-data.ts +404 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +49 -26
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +137 -36
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
- package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +49 -26
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
- package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
- package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +29 -8
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
- package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
- package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
- package/corpus/templates/content/shared/document-export.ts +1 -1
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +17 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +32 -5
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
- package/corpus/templates/design/actions/update-file.ts +9 -2
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +33 -75
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +72 -9
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -39
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1029 -249
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +450 -101
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +978 -154
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -9
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +133 -66
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1234 -344
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +144 -94
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +301 -72
- package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +876 -286
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +30 -7
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +212 -155
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
- package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +487 -107
- package/corpus/templates/design/app/components/design/inspector/index.ts +27 -0
- package/corpus/templates/design/app/global.css +29 -3
- package/corpus/templates/design/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/design/app/i18n-data.ts +239 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +1023 -353
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
- package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
- package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
- package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
- package/corpus/templates/design/shared/code-layer.ts +209 -9
- package/corpus/templates/design/shared/html-content.ts +21 -0
- package/corpus/templates/design/shared/pen-path.ts +207 -0
- package/corpus/templates/mail/app/lib/utils.ts +2 -2
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
- package/corpus/templates/slides/AGENTS.md +3 -2
- package/corpus/templates/slides/actions/import-file.ts +26 -17
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
- package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
- package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
- package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
- package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
- package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
- package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
- package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
- package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
- package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
- package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
- package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
- package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
- package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
- package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
- package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
- package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
- package/dist/application-state/handlers.js +1 -1
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/brand-kit/index.d.ts +1 -1
- package/dist/brand-kit/index.js +1 -1
- package/dist/brand-kit/index.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +18 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/extensions/portable-extension.d.ts +1 -1
- package/dist/client/extensions/portable-extension.d.ts.map +1 -1
- package/dist/client/extensions/portable-extension.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.js +1 -1
- package/dist/code-agents/transcript-normalizer.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/path.d.ts +1 -1
- package/dist/extensions/path.d.ts.map +1 -1
- package/dist/extensions/path.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/a2a-continuation-processor.js +1 -1
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +1 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +35 -1
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +8 -8
- package/dist/org/handlers.js +5 -5
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/plugin.js +2 -2
- package/dist/org/plugin.js.map +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/call-agent.js +1 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/scripts/db/safety.js +1 -1
- package/dist/scripts/db/safety.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +3 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-design-systems.d.ts +28 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -0
- package/dist/server/builder-design-systems.js +217 -0
- package/dist/server/builder-design-systems.js.map +1 -0
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +20 -3
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/styles/agent-native.css +14 -7
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
- package/docs/content/locales/ar-SA/template-design.mdx +14 -1
- package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
- package/docs/content/locales/de-DE/template-design.mdx +15 -1
- package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
- package/docs/content/locales/es-ES/template-design.mdx +15 -1
- package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
- package/docs/content/locales/fr-FR/template-design.mdx +15 -1
- package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
- package/docs/content/locales/hi-IN/template-design.mdx +15 -1
- package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
- package/docs/content/locales/ja-JP/template-design.mdx +16 -1
- package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
- package/docs/content/locales/ko-KR/template-design.mdx +14 -1
- package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
- package/docs/content/locales/pt-BR/template-design.mdx +14 -1
- package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
- package/docs/content/locales/zh-CN/template-design.mdx +14 -1
- package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
- package/docs/content/locales/zh-TW/template-design.mdx +14 -1
- package/docs/content/skills-guide.mdx +5 -0
- package/docs/content/template-design.mdx +14 -1
- package/package.json +1 -1
- package/corpus/templates/design/actions/import-figma-file.ts +0 -166
- package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
|
@@ -13,12 +13,28 @@ import {
|
|
|
13
13
|
screenToCanvasPoint,
|
|
14
14
|
type ArrowNudgeKey,
|
|
15
15
|
} from "@shared/canvas-math";
|
|
16
|
+
import {
|
|
17
|
+
appendPenNode,
|
|
18
|
+
clonePenPath,
|
|
19
|
+
closePenPath,
|
|
20
|
+
constrainPointTo45Degrees,
|
|
21
|
+
createCornerNode,
|
|
22
|
+
createSmoothNode,
|
|
23
|
+
getPenPathGeometry,
|
|
24
|
+
isPenCloseTarget,
|
|
25
|
+
scalePenPathToGeometry,
|
|
26
|
+
serializePenPath,
|
|
27
|
+
translatePenPath,
|
|
28
|
+
type PenNode,
|
|
29
|
+
type PenPath,
|
|
30
|
+
} from "@shared/pen-path";
|
|
16
31
|
import { IconCopy, IconMaximize } from "@tabler/icons-react";
|
|
17
32
|
import {
|
|
18
33
|
useRef,
|
|
19
34
|
useState,
|
|
20
35
|
useCallback,
|
|
21
36
|
useEffect,
|
|
37
|
+
type CSSProperties,
|
|
22
38
|
type ReactNode,
|
|
23
39
|
} from "react";
|
|
24
40
|
|
|
@@ -48,13 +64,17 @@ type MultiScreenCanvasTool =
|
|
|
48
64
|
| "frame"
|
|
49
65
|
| "rect"
|
|
50
66
|
| "rectangle"
|
|
67
|
+
| "line"
|
|
68
|
+
| "arrow"
|
|
69
|
+
| "ellipse"
|
|
70
|
+
| "polygon"
|
|
71
|
+
| "star"
|
|
51
72
|
| "text"
|
|
52
73
|
| "pen"
|
|
53
74
|
| "hand"
|
|
54
75
|
| "comment"
|
|
55
76
|
| "draw"
|
|
56
|
-
| "scale"
|
|
57
|
-
| "overview";
|
|
77
|
+
| "scale";
|
|
58
78
|
|
|
59
79
|
interface CanvasToolProps {
|
|
60
80
|
fill?: string;
|
|
@@ -67,10 +87,12 @@ export interface CanvasPrimitiveInsert {
|
|
|
67
87
|
kind: DraftPrimitiveKind;
|
|
68
88
|
geometry: FrameGeometry;
|
|
69
89
|
points?: Point[];
|
|
90
|
+
pathData?: string;
|
|
70
91
|
text?: string;
|
|
71
92
|
fill?: string;
|
|
72
93
|
stroke?: string;
|
|
73
94
|
strokeWidth?: number;
|
|
95
|
+
autoSize?: boolean;
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
interface ScreenMetadata {
|
|
@@ -115,6 +137,7 @@ interface MultiScreenCanvasProps {
|
|
|
115
137
|
screenId: string,
|
|
116
138
|
primitive: CanvasPrimitiveInsert,
|
|
117
139
|
) => boolean;
|
|
140
|
+
onCreateScreenFrame?: (geometry: FrameGeometry) => void;
|
|
118
141
|
onDeleteSelection?: (ids: string[]) => boolean | void;
|
|
119
142
|
onZoomChange?: (zoom: number) => void;
|
|
120
143
|
onZoomToEdit?: (id: string) => void;
|
|
@@ -147,13 +170,15 @@ const MAX_WHEEL_ZOOM_DELTA = 80;
|
|
|
147
170
|
const MAX_WHEEL_PAN_DELTA = 140;
|
|
148
171
|
const PIXEL_GRID_ZOOM = 800;
|
|
149
172
|
const DRAFT_FRAME_WIDTH = 320;
|
|
150
|
-
const DRAFT_FRAME_HEIGHT =
|
|
173
|
+
const DRAFT_FRAME_HEIGHT = 640;
|
|
151
174
|
const DRAFT_RECT_WIDTH = 160;
|
|
152
175
|
const DRAFT_RECT_HEIGHT = 120;
|
|
153
176
|
const DRAFT_TEXT_WIDTH = 180;
|
|
154
177
|
const DRAFT_TEXT_HEIGHT = 48;
|
|
178
|
+
const DRAFT_LINE_WIDTH = 160;
|
|
155
179
|
const DRAFT_PATH_MIN_SIZE = 12;
|
|
156
180
|
const PEN_SAMPLE_DISTANCE_SCREEN_PX = 5;
|
|
181
|
+
const PEN_CLOSE_HIT_RADIUS_SCREEN_PX = 10;
|
|
157
182
|
|
|
158
183
|
interface ResolvedScreenMetadata {
|
|
159
184
|
source: ScreenSourceType;
|
|
@@ -194,18 +219,39 @@ export interface Point {
|
|
|
194
219
|
y: number;
|
|
195
220
|
}
|
|
196
221
|
|
|
197
|
-
export type DraftPrimitiveKind =
|
|
198
|
-
|
|
222
|
+
export type DraftPrimitiveKind =
|
|
223
|
+
| "frame"
|
|
224
|
+
| "rectangle"
|
|
225
|
+
| "ellipse"
|
|
226
|
+
| "polygon"
|
|
227
|
+
| "star"
|
|
228
|
+
| "line"
|
|
229
|
+
| "arrow"
|
|
230
|
+
| "text"
|
|
231
|
+
| "path";
|
|
232
|
+
type DraftCreationTool =
|
|
233
|
+
| "frame"
|
|
234
|
+
| "rect"
|
|
235
|
+
| "line"
|
|
236
|
+
| "arrow"
|
|
237
|
+
| "ellipse"
|
|
238
|
+
| "polygon"
|
|
239
|
+
| "star"
|
|
240
|
+
| "text"
|
|
241
|
+
| "pen";
|
|
199
242
|
|
|
200
243
|
interface DraftPrimitive {
|
|
201
244
|
id: string;
|
|
202
245
|
kind: DraftPrimitiveKind;
|
|
203
246
|
geometry: FrameGeometry;
|
|
204
247
|
points?: Point[];
|
|
248
|
+
penPath?: PenPath;
|
|
249
|
+
pathData?: string;
|
|
205
250
|
text?: string;
|
|
206
251
|
fill?: string;
|
|
207
252
|
stroke?: string;
|
|
208
253
|
strokeWidth?: number;
|
|
254
|
+
autoSize?: boolean;
|
|
209
255
|
}
|
|
210
256
|
|
|
211
257
|
type DraftPrimitiveById = Record<string, DraftPrimitive>;
|
|
@@ -309,6 +355,14 @@ interface DraftCreateDragState {
|
|
|
309
355
|
hasMoved: boolean;
|
|
310
356
|
}
|
|
311
357
|
|
|
358
|
+
interface PenNodeDragState {
|
|
359
|
+
type: "pen-node";
|
|
360
|
+
originClient: Point;
|
|
361
|
+
anchor: Point;
|
|
362
|
+
pathBefore: PenPath | null;
|
|
363
|
+
hasMoved: boolean;
|
|
364
|
+
}
|
|
365
|
+
|
|
312
366
|
interface DraftCreationPreview {
|
|
313
367
|
tool: DraftCreationTool;
|
|
314
368
|
geometry: FrameGeometry;
|
|
@@ -323,7 +377,8 @@ type DragState =
|
|
|
323
377
|
| PanDragState
|
|
324
378
|
| DraftMoveDragState
|
|
325
379
|
| DraftResizeDragState
|
|
326
|
-
| DraftCreateDragState
|
|
380
|
+
| DraftCreateDragState
|
|
381
|
+
| PenNodeDragState;
|
|
327
382
|
|
|
328
383
|
export function MultiScreenCanvas({
|
|
329
384
|
screens,
|
|
@@ -341,6 +396,7 @@ export function MultiScreenCanvas({
|
|
|
341
396
|
onGeometryChange,
|
|
342
397
|
onGeometryCommit,
|
|
343
398
|
onCreatePrimitive,
|
|
399
|
+
onCreateScreenFrame,
|
|
344
400
|
onDeleteSelection,
|
|
345
401
|
onZoomChange,
|
|
346
402
|
onZoomToEdit,
|
|
@@ -365,6 +421,13 @@ export function MultiScreenCanvas({
|
|
|
365
421
|
const selectedDraftIdsRef = useRef(selectedDraftIds);
|
|
366
422
|
const [creationPreview, setCreationPreview] =
|
|
367
423
|
useState<DraftCreationPreview | null>(null);
|
|
424
|
+
const [activePenPath, setActivePenPath] = useState<PenPath | null>(null);
|
|
425
|
+
const activePenPathRef = useRef<PenPath | null>(activePenPath);
|
|
426
|
+
const [penGesturePreview, setPenGesturePreview] = useState<PenPath | null>(
|
|
427
|
+
null,
|
|
428
|
+
);
|
|
429
|
+
const [penPointer, setPenPointer] = useState<Point | null>(null);
|
|
430
|
+
const [penCloseHover, setPenCloseHover] = useState(false);
|
|
368
431
|
const [localActiveTool, setLocalActiveTool] =
|
|
369
432
|
useState<MultiScreenCanvasTool>("move");
|
|
370
433
|
const [selectedIds, setSelectedIds] = useState<string[]>(
|
|
@@ -397,6 +460,10 @@ export function MultiScreenCanvas({
|
|
|
397
460
|
onGeometryCommitRef.current = onGeometryCommit;
|
|
398
461
|
}, [onGeometryCommit]);
|
|
399
462
|
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
activePenPathRef.current = activePenPath;
|
|
465
|
+
}, [activePenPath]);
|
|
466
|
+
|
|
400
467
|
const updateFrameGeometry = useCallback(
|
|
401
468
|
(updater: (current: FrameGeometryById) => FrameGeometryById) => {
|
|
402
469
|
setFrameGeometry((current) => {
|
|
@@ -692,15 +759,19 @@ export function MultiScreenCanvas({
|
|
|
692
759
|
handleMouseUp: (ev: MouseEvent) => void,
|
|
693
760
|
) => {
|
|
694
761
|
dragCleanup.current?.();
|
|
762
|
+
let lastMouseEvent: MouseEvent | null = null;
|
|
695
763
|
const move = (ev: MouseEvent) => {
|
|
764
|
+
lastMouseEvent = ev;
|
|
696
765
|
ev.preventDefault();
|
|
697
766
|
handleMouseMove(ev);
|
|
698
767
|
};
|
|
699
768
|
const up = (ev: MouseEvent) => {
|
|
769
|
+
lastMouseEvent = ev;
|
|
700
770
|
ev.preventDefault();
|
|
701
771
|
handleMouseUp(ev);
|
|
702
772
|
};
|
|
703
|
-
const cleanupOnBlur = () =>
|
|
773
|
+
const cleanupOnBlur = () =>
|
|
774
|
+
handleMouseUp(lastMouseEvent ?? new MouseEvent("mouseup"));
|
|
704
775
|
dragCleanup.current = () => {
|
|
705
776
|
window.removeEventListener("mousemove", move);
|
|
706
777
|
window.removeEventListener("mouseup", up);
|
|
@@ -860,6 +931,253 @@ export function MultiScreenCanvas({
|
|
|
860
931
|
],
|
|
861
932
|
);
|
|
862
933
|
|
|
934
|
+
const getTargetFrameForDraft = useCallback(
|
|
935
|
+
(draft: DraftPrimitive) =>
|
|
936
|
+
getCurrentFrameEntries()
|
|
937
|
+
.filter(({ geometry }) =>
|
|
938
|
+
rectContainsPoint(
|
|
939
|
+
{
|
|
940
|
+
left: geometry.x,
|
|
941
|
+
top: geometry.y,
|
|
942
|
+
right: geometry.x + geometry.width,
|
|
943
|
+
bottom: geometry.y + geometry.height,
|
|
944
|
+
},
|
|
945
|
+
getFrameCenter(draft.geometry),
|
|
946
|
+
),
|
|
947
|
+
)
|
|
948
|
+
.sort((a, b) => (b.geometry.z ?? 0) - (a.geometry.z ?? 0))[0],
|
|
949
|
+
[getCurrentFrameEntries],
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
const persistDraftPrimitive = useCallback(
|
|
953
|
+
(draft: DraftPrimitive) => {
|
|
954
|
+
const targetFrame = getTargetFrameForDraft(draft);
|
|
955
|
+
if (!targetFrame || !onCreatePrimitive) return null;
|
|
956
|
+
const targetScreen = screens.find(
|
|
957
|
+
(screen) => screen.id === targetFrame.id,
|
|
958
|
+
);
|
|
959
|
+
const targetMetadata = targetScreen
|
|
960
|
+
? resolveScreenMetadata(
|
|
961
|
+
targetScreen,
|
|
962
|
+
metadataById?.[targetScreen.id],
|
|
963
|
+
getScreenMetadata?.(targetScreen),
|
|
964
|
+
)
|
|
965
|
+
: undefined;
|
|
966
|
+
|
|
967
|
+
const localPrimitive = draftPrimitiveToInsert(
|
|
968
|
+
draft,
|
|
969
|
+
targetFrame.geometry,
|
|
970
|
+
targetMetadata,
|
|
971
|
+
);
|
|
972
|
+
const persisted = onCreatePrimitive(targetFrame.id, localPrimitive);
|
|
973
|
+
return persisted ? targetFrame.id : null;
|
|
974
|
+
},
|
|
975
|
+
[
|
|
976
|
+
getScreenMetadata,
|
|
977
|
+
getTargetFrameForDraft,
|
|
978
|
+
metadataById,
|
|
979
|
+
onCreatePrimitive,
|
|
980
|
+
screens,
|
|
981
|
+
],
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
const commitDraftPrimitive = useCallback(
|
|
985
|
+
(nextDraft: DraftPrimitive) => {
|
|
986
|
+
const persistedFrameId = persistDraftPrimitive(nextDraft);
|
|
987
|
+
if (persistedFrameId) {
|
|
988
|
+
updateDraftPrimitives((current) =>
|
|
989
|
+
current.filter((draft) => draft.id !== nextDraft.id),
|
|
990
|
+
);
|
|
991
|
+
updateSelectedDraftIds(() => []);
|
|
992
|
+
updateSelectedIds(() => [persistedFrameId]);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
updateDraftPrimitives((current) => [...current, nextDraft]);
|
|
997
|
+
updateSelectedIds(() => []);
|
|
998
|
+
updateSelectedDraftIds(() => [nextDraft.id]);
|
|
999
|
+
},
|
|
1000
|
+
[
|
|
1001
|
+
persistDraftPrimitive,
|
|
1002
|
+
updateDraftPrimitives,
|
|
1003
|
+
updateSelectedDraftIds,
|
|
1004
|
+
updateSelectedIds,
|
|
1005
|
+
],
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
const clearActivePenPath = useCallback(() => {
|
|
1009
|
+
setActivePenPath(null);
|
|
1010
|
+
setPenGesturePreview(null);
|
|
1011
|
+
setPenPointer(null);
|
|
1012
|
+
setPenCloseHover(false);
|
|
1013
|
+
}, []);
|
|
1014
|
+
|
|
1015
|
+
const finishPenPath = useCallback(
|
|
1016
|
+
(path = activePenPathRef.current) => {
|
|
1017
|
+
if (!path || path.nodes.length < 2) {
|
|
1018
|
+
clearActivePenPath();
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
commitDraftPrimitive(
|
|
1023
|
+
createPenDraftPrimitive(path, {
|
|
1024
|
+
stroke: toolProps?.stroke,
|
|
1025
|
+
strokeWidth: toolProps?.strokeWidth,
|
|
1026
|
+
}),
|
|
1027
|
+
);
|
|
1028
|
+
clearActivePenPath();
|
|
1029
|
+
onActiveToolChange?.("pen");
|
|
1030
|
+
},
|
|
1031
|
+
[clearActivePenPath, commitDraftPrimitive, onActiveToolChange, toolProps],
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
const getPenAnchorPoint = useCallback(
|
|
1035
|
+
(
|
|
1036
|
+
clientX: number,
|
|
1037
|
+
clientY: number,
|
|
1038
|
+
shiftKey: boolean,
|
|
1039
|
+
path: PenPath | null,
|
|
1040
|
+
) => {
|
|
1041
|
+
const rawPoint = getCanvasPoint(clientX, clientY);
|
|
1042
|
+
const lastAnchor = path?.nodes[path.nodes.length - 1]?.point;
|
|
1043
|
+
return shiftKey && lastAnchor
|
|
1044
|
+
? constrainPointTo45Degrees(lastAnchor, rawPoint)
|
|
1045
|
+
: rawPoint;
|
|
1046
|
+
},
|
|
1047
|
+
[getCanvasPoint],
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
const updatePenPointer = useCallback(
|
|
1051
|
+
(clientX: number, clientY: number, shiftKey: boolean) => {
|
|
1052
|
+
const path = activePenPathRef.current;
|
|
1053
|
+
if (!path || path.closed) {
|
|
1054
|
+
setPenPointer(null);
|
|
1055
|
+
setPenCloseHover(false);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const rawPoint = getCanvasPoint(clientX, clientY);
|
|
1060
|
+
const closeHover = isPenCloseTarget(
|
|
1061
|
+
path,
|
|
1062
|
+
rawPoint,
|
|
1063
|
+
PEN_CLOSE_HIT_RADIUS_SCREEN_PX / (zoomRef.current / 100),
|
|
1064
|
+
);
|
|
1065
|
+
setPenCloseHover(closeHover);
|
|
1066
|
+
setPenPointer(
|
|
1067
|
+
closeHover
|
|
1068
|
+
? path.nodes[0].point
|
|
1069
|
+
: getPenAnchorPoint(clientX, clientY, shiftKey, path),
|
|
1070
|
+
);
|
|
1071
|
+
},
|
|
1072
|
+
[getCanvasPoint, getPenAnchorPoint],
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
const beginPenNodeCreation = useCallback(
|
|
1076
|
+
(e: React.MouseEvent) => {
|
|
1077
|
+
if (e.button !== 0) return;
|
|
1078
|
+
e.preventDefault();
|
|
1079
|
+
e.stopPropagation();
|
|
1080
|
+
suppressNextPick.current = true;
|
|
1081
|
+
|
|
1082
|
+
const pathBefore = activePenPathRef.current?.closed
|
|
1083
|
+
? null
|
|
1084
|
+
: activePenPathRef.current;
|
|
1085
|
+
const rawPoint = getCanvasPoint(e.clientX, e.clientY);
|
|
1086
|
+
if (
|
|
1087
|
+
pathBefore &&
|
|
1088
|
+
isPenCloseTarget(
|
|
1089
|
+
pathBefore,
|
|
1090
|
+
rawPoint,
|
|
1091
|
+
PEN_CLOSE_HIT_RADIUS_SCREEN_PX / (zoomRef.current / 100),
|
|
1092
|
+
)
|
|
1093
|
+
) {
|
|
1094
|
+
finishPenPath(closePenPath(pathBefore));
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const anchor = getPenAnchorPoint(
|
|
1099
|
+
e.clientX,
|
|
1100
|
+
e.clientY,
|
|
1101
|
+
e.shiftKey,
|
|
1102
|
+
pathBefore,
|
|
1103
|
+
);
|
|
1104
|
+
const pathSnapshot = pathBefore ? clonePenPath(pathBefore) : null;
|
|
1105
|
+
dragState.current = {
|
|
1106
|
+
type: "pen-node",
|
|
1107
|
+
originClient: { x: e.clientX, y: e.clientY },
|
|
1108
|
+
anchor,
|
|
1109
|
+
pathBefore: pathSnapshot,
|
|
1110
|
+
hasMoved: false,
|
|
1111
|
+
};
|
|
1112
|
+
setPenGesturePreview(
|
|
1113
|
+
appendPenNode(pathSnapshot, createCornerNode(anchor)),
|
|
1114
|
+
);
|
|
1115
|
+
setPenPointer(null);
|
|
1116
|
+
setPenCloseHover(false);
|
|
1117
|
+
setIsDragging(true);
|
|
1118
|
+
setDragCursor("crosshair");
|
|
1119
|
+
|
|
1120
|
+
const handleMouseMove = (ev: MouseEvent) => {
|
|
1121
|
+
const state = dragState.current;
|
|
1122
|
+
if (!state || state.type !== "pen-node") return;
|
|
1123
|
+
if (
|
|
1124
|
+
!state.hasMoved &&
|
|
1125
|
+
Math.hypot(
|
|
1126
|
+
ev.clientX - state.originClient.x,
|
|
1127
|
+
ev.clientY - state.originClient.y,
|
|
1128
|
+
) >= DRAG_THRESHOLD
|
|
1129
|
+
) {
|
|
1130
|
+
state.hasMoved = true;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const handlePoint = getCanvasPoint(ev.clientX, ev.clientY);
|
|
1134
|
+
const node = state.hasMoved
|
|
1135
|
+
? createSmoothNode(
|
|
1136
|
+
state.anchor,
|
|
1137
|
+
ev.shiftKey
|
|
1138
|
+
? constrainPointTo45Degrees(state.anchor, handlePoint)
|
|
1139
|
+
: handlePoint,
|
|
1140
|
+
)
|
|
1141
|
+
: createCornerNode(state.anchor);
|
|
1142
|
+
setPenGesturePreview(appendPenNode(state.pathBefore, node));
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const handleMouseUp = (ev: MouseEvent) => {
|
|
1146
|
+
const state = dragState.current;
|
|
1147
|
+
if (!state || state.type !== "pen-node") {
|
|
1148
|
+
finishDrag();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const handlePoint = getCanvasPoint(ev.clientX, ev.clientY);
|
|
1153
|
+
const node: PenNode = state.hasMoved
|
|
1154
|
+
? createSmoothNode(
|
|
1155
|
+
state.anchor,
|
|
1156
|
+
ev.shiftKey
|
|
1157
|
+
? constrainPointTo45Degrees(state.anchor, handlePoint)
|
|
1158
|
+
: handlePoint,
|
|
1159
|
+
)
|
|
1160
|
+
: createCornerNode(state.anchor);
|
|
1161
|
+
setActivePenPath(appendPenNode(state.pathBefore, node));
|
|
1162
|
+
setPenGesturePreview(null);
|
|
1163
|
+
setPenPointer(null);
|
|
1164
|
+
setPenCloseHover(false);
|
|
1165
|
+
onActiveToolChange?.("pen");
|
|
1166
|
+
finishDrag();
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
installDragListeners(handleMouseMove, handleMouseUp);
|
|
1170
|
+
},
|
|
1171
|
+
[
|
|
1172
|
+
finishDrag,
|
|
1173
|
+
finishPenPath,
|
|
1174
|
+
getCanvasPoint,
|
|
1175
|
+
getPenAnchorPoint,
|
|
1176
|
+
installDragListeners,
|
|
1177
|
+
onActiveToolChange,
|
|
1178
|
+
],
|
|
1179
|
+
);
|
|
1180
|
+
|
|
863
1181
|
const beginDraftCreation = useCallback(
|
|
864
1182
|
(tool: DraftCreationTool, e: React.MouseEvent) => {
|
|
865
1183
|
if (e.button !== 0) return;
|
|
@@ -872,7 +1190,15 @@ export function MultiScreenCanvas({
|
|
|
872
1190
|
originCanvas,
|
|
873
1191
|
originCanvas,
|
|
874
1192
|
);
|
|
875
|
-
const initialPoints =
|
|
1193
|
+
const initialPoints =
|
|
1194
|
+
tool === "pen"
|
|
1195
|
+
? [originCanvas]
|
|
1196
|
+
: tool === "line" || tool === "arrow"
|
|
1197
|
+
? [
|
|
1198
|
+
originCanvas,
|
|
1199
|
+
{ x: originCanvas.x + DRAFT_LINE_WIDTH, y: originCanvas.y },
|
|
1200
|
+
]
|
|
1201
|
+
: undefined;
|
|
876
1202
|
dragState.current = {
|
|
877
1203
|
type: "draft-create",
|
|
878
1204
|
tool,
|
|
@@ -926,6 +1252,10 @@ export function MultiScreenCanvas({
|
|
|
926
1252
|
state.originCanvas,
|
|
927
1253
|
nextCanvas,
|
|
928
1254
|
),
|
|
1255
|
+
points:
|
|
1256
|
+
state.tool === "line" || state.tool === "arrow"
|
|
1257
|
+
? [state.originCanvas, nextCanvas]
|
|
1258
|
+
: undefined,
|
|
929
1259
|
});
|
|
930
1260
|
};
|
|
931
1261
|
|
|
@@ -937,60 +1267,43 @@ export function MultiScreenCanvas({
|
|
|
937
1267
|
}
|
|
938
1268
|
|
|
939
1269
|
let endCanvas = getCanvasPoint(ev.clientX, ev.clientY);
|
|
1270
|
+
const releaseMoved =
|
|
1271
|
+
state.hasMoved ||
|
|
1272
|
+
Math.hypot(
|
|
1273
|
+
ev.clientX - state.originClient.x,
|
|
1274
|
+
ev.clientY - state.originClient.y,
|
|
1275
|
+
) >= DRAG_THRESHOLD;
|
|
1276
|
+
state.hasMoved = releaseMoved;
|
|
940
1277
|
let points = state.tool === "pen" ? state.points : undefined;
|
|
941
1278
|
if (state.tool === "pen") {
|
|
942
1279
|
points = appendPolylinePoint(state.points, endCanvas, 0);
|
|
943
|
-
if (!
|
|
1280
|
+
if (!releaseMoved || points.length < 2) {
|
|
944
1281
|
finishDrag();
|
|
945
1282
|
return;
|
|
946
1283
|
}
|
|
947
1284
|
endCanvas = points[points.length - 1] ?? endCanvas;
|
|
948
1285
|
}
|
|
1286
|
+
if (state.tool === "frame" && onCreateScreenFrame) {
|
|
1287
|
+
onCreateScreenFrame(
|
|
1288
|
+
getDraftGeometryForTool(state.tool, state.originCanvas, endCanvas),
|
|
1289
|
+
);
|
|
1290
|
+
if (activeTool === undefined) {
|
|
1291
|
+
setLocalActiveTool("move");
|
|
1292
|
+
}
|
|
1293
|
+
onActiveToolChange?.("move");
|
|
1294
|
+
finishDrag();
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
949
1297
|
const nextDraft = createDraftPrimitive({
|
|
950
1298
|
tool: state.tool,
|
|
951
1299
|
start: state.originCanvas,
|
|
952
1300
|
end: endCanvas,
|
|
953
1301
|
points,
|
|
954
|
-
moved:
|
|
1302
|
+
moved: releaseMoved,
|
|
955
1303
|
toolProps,
|
|
956
1304
|
fallbackText: t("designEditor.tools.text"),
|
|
957
1305
|
});
|
|
958
|
-
|
|
959
|
-
.filter(({ geometry }) =>
|
|
960
|
-
rectContainsPoint(
|
|
961
|
-
{
|
|
962
|
-
left: geometry.x,
|
|
963
|
-
top: geometry.y,
|
|
964
|
-
right: geometry.x + geometry.width,
|
|
965
|
-
bottom: geometry.y + geometry.height,
|
|
966
|
-
},
|
|
967
|
-
getFrameCenter(nextDraft.geometry),
|
|
968
|
-
),
|
|
969
|
-
)
|
|
970
|
-
.sort((a, b) => (b.geometry.z ?? 0) - (a.geometry.z ?? 0))[0];
|
|
971
|
-
|
|
972
|
-
if (targetFrame && onCreatePrimitive) {
|
|
973
|
-
const localPrimitive = draftPrimitiveToInsert(
|
|
974
|
-
nextDraft,
|
|
975
|
-
targetFrame.geometry,
|
|
976
|
-
);
|
|
977
|
-
const persisted = onCreatePrimitive(targetFrame.id, localPrimitive);
|
|
978
|
-
if (persisted) {
|
|
979
|
-
updateDraftPrimitives((current) =>
|
|
980
|
-
current.filter((draft) => draft.id !== nextDraft.id),
|
|
981
|
-
);
|
|
982
|
-
updateSelectedDraftIds(() => []);
|
|
983
|
-
updateSelectedIds(() => [targetFrame.id]);
|
|
984
|
-
} else {
|
|
985
|
-
updateDraftPrimitives((current) => [...current, nextDraft]);
|
|
986
|
-
updateSelectedIds(() => []);
|
|
987
|
-
updateSelectedDraftIds(() => [nextDraft.id]);
|
|
988
|
-
}
|
|
989
|
-
} else {
|
|
990
|
-
updateDraftPrimitives((current) => [...current, nextDraft]);
|
|
991
|
-
updateSelectedIds(() => []);
|
|
992
|
-
updateSelectedDraftIds(() => [nextDraft.id]);
|
|
993
|
-
}
|
|
1306
|
+
commitDraftPrimitive(nextDraft);
|
|
994
1307
|
if (activeTool === undefined) {
|
|
995
1308
|
setLocalActiveTool("move");
|
|
996
1309
|
}
|
|
@@ -1009,17 +1322,14 @@ export function MultiScreenCanvas({
|
|
|
1009
1322
|
},
|
|
1010
1323
|
[
|
|
1011
1324
|
activeTool,
|
|
1325
|
+
commitDraftPrimitive,
|
|
1012
1326
|
finishDrag,
|
|
1013
1327
|
getCanvasPoint,
|
|
1014
|
-
getCurrentFrameEntries,
|
|
1015
1328
|
installDragListeners,
|
|
1016
1329
|
onActiveToolChange,
|
|
1017
|
-
|
|
1330
|
+
onCreateScreenFrame,
|
|
1018
1331
|
t,
|
|
1019
1332
|
toolProps,
|
|
1020
|
-
updateDraftPrimitives,
|
|
1021
|
-
updateSelectedDraftIds,
|
|
1022
|
-
updateSelectedIds,
|
|
1023
1333
|
],
|
|
1024
1334
|
);
|
|
1025
1335
|
|
|
@@ -1110,6 +1420,29 @@ export function MultiScreenCanvas({
|
|
|
1110
1420
|
};
|
|
1111
1421
|
|
|
1112
1422
|
const handleMouseUp = () => {
|
|
1423
|
+
const state = dragState.current;
|
|
1424
|
+
if (state?.type === "draft-move" && state.hasMoved) {
|
|
1425
|
+
const persisted: Array<{ draftId: string; frameId: string }> = [];
|
|
1426
|
+
draftPrimitivesRef.current.forEach((draft) => {
|
|
1427
|
+
if (!state.targetIds.includes(draft.id)) return;
|
|
1428
|
+
const frameId = persistDraftPrimitive(draft);
|
|
1429
|
+
if (frameId) persisted.push({ draftId: draft.id, frameId });
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
if (persisted.length > 0) {
|
|
1433
|
+
const persistedDraftIds = new Set(
|
|
1434
|
+
persisted.map((entry) => entry.draftId),
|
|
1435
|
+
);
|
|
1436
|
+
const lastFrameId = persisted[persisted.length - 1]?.frameId;
|
|
1437
|
+
updateDraftPrimitives((current) =>
|
|
1438
|
+
current.filter((draft) => !persistedDraftIds.has(draft.id)),
|
|
1439
|
+
);
|
|
1440
|
+
updateSelectedDraftIds((current) =>
|
|
1441
|
+
current.filter((draftId) => !persistedDraftIds.has(draftId)),
|
|
1442
|
+
);
|
|
1443
|
+
if (lastFrameId) updateSelectedIds(() => [lastFrameId]);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1113
1446
|
finishDrag();
|
|
1114
1447
|
};
|
|
1115
1448
|
|
|
@@ -1119,6 +1452,7 @@ export function MultiScreenCanvas({
|
|
|
1119
1452
|
finishDrag,
|
|
1120
1453
|
getCurrentCanvasEntries,
|
|
1121
1454
|
installDragListeners,
|
|
1455
|
+
persistDraftPrimitive,
|
|
1122
1456
|
showTransformFeedback,
|
|
1123
1457
|
updateDraftPrimitives,
|
|
1124
1458
|
updateSelectedDraftIds,
|
|
@@ -1752,16 +2086,36 @@ export function MultiScreenCanvas({
|
|
|
1752
2086
|
beginPan(e);
|
|
1753
2087
|
return;
|
|
1754
2088
|
}
|
|
2089
|
+
if (e.button === 0 && tool === "pen") {
|
|
2090
|
+
beginPenNodeCreation(e);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
const creationTool = getDraftCreationTool(tool);
|
|
2094
|
+
if (e.button === 0 && creationTool) {
|
|
2095
|
+
beginDraftCreation(creationTool, e);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
1755
2098
|
if (e.button === 0 && !onFrame) {
|
|
1756
|
-
const creationTool = getDraftCreationTool(tool);
|
|
1757
|
-
if (creationTool) {
|
|
1758
|
-
beginDraftCreation(creationTool, e);
|
|
1759
|
-
return;
|
|
1760
|
-
}
|
|
1761
2099
|
beginMarquee(e);
|
|
1762
2100
|
}
|
|
1763
2101
|
},
|
|
1764
|
-
[
|
|
2102
|
+
[
|
|
2103
|
+
activeTool,
|
|
2104
|
+
beginDraftCreation,
|
|
2105
|
+
beginMarquee,
|
|
2106
|
+
beginPan,
|
|
2107
|
+
beginPenNodeCreation,
|
|
2108
|
+
localActiveTool,
|
|
2109
|
+
],
|
|
2110
|
+
);
|
|
2111
|
+
|
|
2112
|
+
const handleMouseMove = useCallback(
|
|
2113
|
+
(e: React.MouseEvent) => {
|
|
2114
|
+
const tool = normalizeCanvasTool(activeTool ?? localActiveTool);
|
|
2115
|
+
if (tool !== "pen" || dragState.current?.type === "pen-node") return;
|
|
2116
|
+
updatePenPointer(e.clientX, e.clientY, e.shiftKey);
|
|
2117
|
+
},
|
|
2118
|
+
[activeTool, localActiveTool, updatePenPointer],
|
|
1765
2119
|
);
|
|
1766
2120
|
|
|
1767
2121
|
const handleWheelEvent = useCallback(
|
|
@@ -1846,6 +2200,7 @@ export function MultiScreenCanvas({
|
|
|
1846
2200
|
|
|
1847
2201
|
useEffect(() => {
|
|
1848
2202
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
2203
|
+
if (activePenPathRef.current) return;
|
|
1849
2204
|
if (!isArrowNudgeKey(event.key) || isEditableHotkeyTarget(event.target)) {
|
|
1850
2205
|
return;
|
|
1851
2206
|
}
|
|
@@ -1946,6 +2301,7 @@ export function MultiScreenCanvas({
|
|
|
1946
2301
|
|
|
1947
2302
|
useEffect(() => {
|
|
1948
2303
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
2304
|
+
if (activePenPathRef.current) return;
|
|
1949
2305
|
if (
|
|
1950
2306
|
(event.key !== "Delete" && event.key !== "Backspace") ||
|
|
1951
2307
|
event.metaKey ||
|
|
@@ -1966,9 +2322,65 @@ export function MultiScreenCanvas({
|
|
|
1966
2322
|
};
|
|
1967
2323
|
}, [deleteSelectedItems]);
|
|
1968
2324
|
|
|
2325
|
+
useEffect(() => {
|
|
2326
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
2327
|
+
const path = activePenPathRef.current;
|
|
2328
|
+
if (
|
|
2329
|
+
!path ||
|
|
2330
|
+
event.metaKey ||
|
|
2331
|
+
event.ctrlKey ||
|
|
2332
|
+
isEditableHotkeyTarget(event.target)
|
|
2333
|
+
) {
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
if (event.key === "Enter" || event.key === "Escape") {
|
|
2338
|
+
event.preventDefault();
|
|
2339
|
+
event.stopPropagation();
|
|
2340
|
+
event.stopImmediatePropagation();
|
|
2341
|
+
finishPenPath(path);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
if (event.key === "Delete" || event.key === "Backspace") {
|
|
2346
|
+
event.preventDefault();
|
|
2347
|
+
event.stopPropagation();
|
|
2348
|
+
event.stopImmediatePropagation();
|
|
2349
|
+
const remainingNodes = path.nodes.slice(0, -1);
|
|
2350
|
+
if (remainingNodes.length === 0) {
|
|
2351
|
+
clearActivePenPath();
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
setActivePenPath({ nodes: remainingNodes, closed: false });
|
|
2355
|
+
setPenPointer(null);
|
|
2356
|
+
setPenCloseHover(false);
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
|
|
2360
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
2361
|
+
return () => {
|
|
2362
|
+
window.removeEventListener("keydown", handleKeyDown, true);
|
|
2363
|
+
};
|
|
2364
|
+
}, [clearActivePenPath, finishPenPath]);
|
|
2365
|
+
|
|
2366
|
+
useEffect(() => {
|
|
2367
|
+
const tool = normalizeCanvasTool(activeTool ?? localActiveTool);
|
|
2368
|
+
if (tool !== "pen") clearActivePenPath();
|
|
2369
|
+
}, [activeTool, clearActivePenPath, localActiveTool]);
|
|
2370
|
+
|
|
1969
2371
|
const scale = canvasZoom / 100;
|
|
2372
|
+
const chromeScale = scale > 0 ? 1 / scale : 1;
|
|
1970
2373
|
const showPixelGrid = canvasZoom >= PIXEL_GRID_ZOOM;
|
|
1971
2374
|
const effectiveTool = normalizeCanvasTool(activeTool ?? localActiveTool);
|
|
2375
|
+
const penActive = effectiveTool === "pen";
|
|
2376
|
+
const creationToolActive = Boolean(getDraftCreationTool(effectiveTool));
|
|
2377
|
+
const displayedPenPath = penGesturePreview
|
|
2378
|
+
? penGesturePreview
|
|
2379
|
+
: activePenPath && penPointer && activePenPath.nodes.length > 0
|
|
2380
|
+
? penCloseHover
|
|
2381
|
+
? closePenPath(activePenPath)
|
|
2382
|
+
: appendPenNode(activePenPath, createCornerNode(penPointer))
|
|
2383
|
+
: activePenPath;
|
|
1972
2384
|
const selectedIdSet = new Set(selectedIds);
|
|
1973
2385
|
const selectedDraftIdSet = new Set(selectedDraftIds);
|
|
1974
2386
|
const surfaceCursor = isPanning
|
|
@@ -1977,7 +2389,7 @@ export function MultiScreenCanvas({
|
|
|
1977
2389
|
? dragCursor
|
|
1978
2390
|
: isDragging && marquee
|
|
1979
2391
|
? "crosshair"
|
|
1980
|
-
: getDraftCreationTool(effectiveTool)
|
|
2392
|
+
: penActive || getDraftCreationTool(effectiveTool)
|
|
1981
2393
|
? "crosshair"
|
|
1982
2394
|
: effectiveTool === "hand"
|
|
1983
2395
|
? "grab"
|
|
@@ -2006,6 +2418,7 @@ export function MultiScreenCanvas({
|
|
|
2006
2418
|
ref={surfaceRef}
|
|
2007
2419
|
className="relative h-full w-full select-none overflow-hidden bg-background"
|
|
2008
2420
|
onMouseDown={handleMouseDown}
|
|
2421
|
+
onMouseMove={handleMouseMove}
|
|
2009
2422
|
style={{ cursor: surfaceCursor, touchAction: "none" }}
|
|
2010
2423
|
>
|
|
2011
2424
|
{showPixelGrid ? (
|
|
@@ -2046,6 +2459,9 @@ export function MultiScreenCanvas({
|
|
|
2046
2459
|
isSelected={selectedIdSet.has(screen.id)}
|
|
2047
2460
|
groupSelected={hasGroupSelection}
|
|
2048
2461
|
handlesEnabled={!hasGroupSelection}
|
|
2462
|
+
penActive={penActive}
|
|
2463
|
+
creationToolActive={creationToolActive}
|
|
2464
|
+
chromeScale={chromeScale}
|
|
2049
2465
|
onPick={handleFrameClick}
|
|
2050
2466
|
onEdit={handleFrameDoubleClick}
|
|
2051
2467
|
onStartFrameDrag={beginFrameDrag}
|
|
@@ -2062,6 +2478,8 @@ export function MultiScreenCanvas({
|
|
|
2062
2478
|
draft={draft}
|
|
2063
2479
|
isSelected={selectedDraftIdSet.has(draft.id)}
|
|
2064
2480
|
groupSelected={Boolean(selectedDraftGroupBounds)}
|
|
2481
|
+
penActive={penActive}
|
|
2482
|
+
chromeScale={chromeScale}
|
|
2065
2483
|
onClick={handleDraftClick}
|
|
2066
2484
|
onStartDrag={beginDraftDrag}
|
|
2067
2485
|
onStartResize={beginDraftResize}
|
|
@@ -2074,15 +2492,22 @@ export function MultiScreenCanvas({
|
|
|
2074
2492
|
isSelected
|
|
2075
2493
|
preview
|
|
2076
2494
|
groupSelected={false}
|
|
2495
|
+
penActive={penActive}
|
|
2496
|
+
chromeScale={chromeScale}
|
|
2077
2497
|
onClick={() => {}}
|
|
2078
2498
|
onStartDrag={() => {}}
|
|
2079
2499
|
onStartResize={() => {}}
|
|
2080
2500
|
/>
|
|
2081
2501
|
) : null}
|
|
2082
2502
|
|
|
2503
|
+
{displayedPenPath ? (
|
|
2504
|
+
<PenPathOverlay path={displayedPenPath} closeHover={penCloseHover} />
|
|
2505
|
+
) : null}
|
|
2506
|
+
|
|
2083
2507
|
{selectedGroupBounds ? (
|
|
2084
2508
|
<GroupSelectionBox
|
|
2085
2509
|
bounds={selectedGroupBounds}
|
|
2510
|
+
chromeScale={chromeScale}
|
|
2086
2511
|
onStartResize={beginGroupResize}
|
|
2087
2512
|
/>
|
|
2088
2513
|
) : null}
|
|
@@ -2090,6 +2515,7 @@ export function MultiScreenCanvas({
|
|
|
2090
2515
|
{selectedDraftGroupBounds ? (
|
|
2091
2516
|
<GroupSelectionBox
|
|
2092
2517
|
bounds={selectedDraftGroupBounds}
|
|
2518
|
+
chromeScale={chromeScale}
|
|
2093
2519
|
onStartResize={beginDraftGroupResize}
|
|
2094
2520
|
/>
|
|
2095
2521
|
) : null}
|
|
@@ -2178,6 +2604,8 @@ function DraftPrimitiveLayer({
|
|
|
2178
2604
|
draft,
|
|
2179
2605
|
isSelected,
|
|
2180
2606
|
groupSelected,
|
|
2607
|
+
penActive,
|
|
2608
|
+
chromeScale,
|
|
2181
2609
|
preview = false,
|
|
2182
2610
|
onClick,
|
|
2183
2611
|
onStartDrag,
|
|
@@ -2186,6 +2614,8 @@ function DraftPrimitiveLayer({
|
|
|
2186
2614
|
draft: DraftPrimitive;
|
|
2187
2615
|
isSelected: boolean;
|
|
2188
2616
|
groupSelected: boolean;
|
|
2617
|
+
penActive: boolean;
|
|
2618
|
+
chromeScale: number;
|
|
2189
2619
|
preview?: boolean;
|
|
2190
2620
|
onClick: (id: string, e: React.MouseEvent) => void;
|
|
2191
2621
|
onStartDrag: (id: string, e: React.MouseEvent) => void;
|
|
@@ -2203,7 +2633,7 @@ function DraftPrimitiveLayer({
|
|
|
2203
2633
|
type="button"
|
|
2204
2634
|
className={cn(
|
|
2205
2635
|
"group/artboard pointer-events-auto absolute block overflow-visible text-left outline-none",
|
|
2206
|
-
preview ? "cursor-crosshair" : "cursor-move",
|
|
2636
|
+
preview || penActive ? "cursor-crosshair" : "cursor-move",
|
|
2207
2637
|
)}
|
|
2208
2638
|
style={{
|
|
2209
2639
|
left: SURFACE_PADDING + geometry.x,
|
|
@@ -2216,25 +2646,32 @@ function DraftPrimitiveLayer({
|
|
|
2216
2646
|
: undefined,
|
|
2217
2647
|
}}
|
|
2218
2648
|
onClick={(event) => {
|
|
2649
|
+
if (penActive) return;
|
|
2219
2650
|
if (!preview) onClick(draft.id, event);
|
|
2220
2651
|
}}
|
|
2221
2652
|
onMouseDown={(event) => {
|
|
2653
|
+
if (penActive) return;
|
|
2222
2654
|
if (!preview) onStartDrag(draft.id, event);
|
|
2223
2655
|
}}
|
|
2224
2656
|
>
|
|
2225
2657
|
<DraftPrimitiveContent draft={draft} preview={preview} />
|
|
2226
2658
|
<span
|
|
2227
2659
|
className={cn(
|
|
2228
|
-
"pointer-events-none absolute
|
|
2660
|
+
"pointer-events-none absolute rounded-sm border transition-opacity",
|
|
2229
2661
|
selected
|
|
2230
2662
|
? "border-[var(--design-editor-accent-color)] opacity-100"
|
|
2231
2663
|
: "border-[var(--design-editor-accent-color)] opacity-0 group-hover/artboard:opacity-100",
|
|
2232
2664
|
)}
|
|
2665
|
+
style={{
|
|
2666
|
+
inset: -5 * chromeScale,
|
|
2667
|
+
borderWidth: 1.5 * chromeScale,
|
|
2668
|
+
}}
|
|
2233
2669
|
/>
|
|
2234
2670
|
<ResizeHandles
|
|
2235
2671
|
active={selected || preview}
|
|
2236
|
-
enabled={(isSelected && !groupSelected) || preview}
|
|
2672
|
+
enabled={!penActive && ((isSelected && !groupSelected) || preview)}
|
|
2237
2673
|
showRotate={false}
|
|
2674
|
+
chromeScale={chromeScale}
|
|
2238
2675
|
onStartResize={(handle, event) =>
|
|
2239
2676
|
onStartResize(draft.id, handle, event)
|
|
2240
2677
|
}
|
|
@@ -2252,19 +2689,45 @@ function DraftPrimitiveContent({
|
|
|
2252
2689
|
preview: boolean;
|
|
2253
2690
|
}) {
|
|
2254
2691
|
const muted = preview ? "opacity-70" : "";
|
|
2255
|
-
if (
|
|
2692
|
+
if (
|
|
2693
|
+
draft.kind === "path" ||
|
|
2694
|
+
draft.kind === "line" ||
|
|
2695
|
+
draft.kind === "arrow"
|
|
2696
|
+
) {
|
|
2697
|
+
const markerId = `arrow-${draft.id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
2698
|
+
const pathData =
|
|
2699
|
+
draft.pathData ??
|
|
2700
|
+
(draft.penPath
|
|
2701
|
+
? serializePenPath(draft.penPath)
|
|
2702
|
+
: pointsToPath(draft.points ?? []));
|
|
2256
2703
|
return (
|
|
2257
2704
|
<svg
|
|
2258
2705
|
className={cn("block size-full overflow-visible", muted)}
|
|
2259
2706
|
viewBox={`${draft.geometry.x} ${draft.geometry.y} ${draft.geometry.width} ${draft.geometry.height}`}
|
|
2260
2707
|
>
|
|
2708
|
+
{draft.kind === "arrow" ? (
|
|
2709
|
+
<defs>
|
|
2710
|
+
<marker
|
|
2711
|
+
id={markerId}
|
|
2712
|
+
markerWidth="10"
|
|
2713
|
+
markerHeight="10"
|
|
2714
|
+
refX="8"
|
|
2715
|
+
refY="5"
|
|
2716
|
+
orient="auto"
|
|
2717
|
+
markerUnits="strokeWidth"
|
|
2718
|
+
>
|
|
2719
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
2720
|
+
</marker>
|
|
2721
|
+
</defs>
|
|
2722
|
+
) : null}
|
|
2261
2723
|
<path
|
|
2262
|
-
d={
|
|
2724
|
+
d={pathData}
|
|
2263
2725
|
fill="none"
|
|
2264
2726
|
stroke={draft.stroke ?? "hsl(var(--primary))"}
|
|
2265
2727
|
strokeLinecap="round"
|
|
2266
2728
|
strokeLinejoin="round"
|
|
2267
2729
|
strokeWidth={draft.strokeWidth ?? 3}
|
|
2730
|
+
markerEnd={draft.kind === "arrow" ? `url(#${markerId})` : undefined}
|
|
2268
2731
|
/>
|
|
2269
2732
|
</svg>
|
|
2270
2733
|
);
|
|
@@ -2294,6 +2757,43 @@ function DraftPrimitiveContent({
|
|
|
2294
2757
|
);
|
|
2295
2758
|
}
|
|
2296
2759
|
|
|
2760
|
+
if (draft.kind === "ellipse") {
|
|
2761
|
+
return (
|
|
2762
|
+
<div
|
|
2763
|
+
className={cn("size-full rounded-full border", muted)}
|
|
2764
|
+
style={{
|
|
2765
|
+
background: draft.fill ?? "hsl(var(--primary) / 0.12)",
|
|
2766
|
+
borderColor: draft.stroke ?? "hsl(var(--primary) / 0.7)",
|
|
2767
|
+
borderWidth: draft.strokeWidth ?? 1,
|
|
2768
|
+
}}
|
|
2769
|
+
/>
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
if (draft.kind === "polygon" || draft.kind === "star") {
|
|
2774
|
+
return (
|
|
2775
|
+
<svg
|
|
2776
|
+
className={cn("block size-full overflow-visible", muted)}
|
|
2777
|
+
viewBox={`0 0 ${Math.max(1, draft.geometry.width)} ${Math.max(
|
|
2778
|
+
1,
|
|
2779
|
+
draft.geometry.height,
|
|
2780
|
+
)}`}
|
|
2781
|
+
>
|
|
2782
|
+
<polygon
|
|
2783
|
+
points={polygonPointsForBox(
|
|
2784
|
+
draft.kind,
|
|
2785
|
+
draft.geometry.width,
|
|
2786
|
+
draft.geometry.height,
|
|
2787
|
+
)}
|
|
2788
|
+
fill={draft.fill ?? "hsl(var(--primary) / 0.12)"}
|
|
2789
|
+
stroke={draft.stroke ?? "hsl(var(--primary))"}
|
|
2790
|
+
strokeLinejoin="round"
|
|
2791
|
+
strokeWidth={draft.strokeWidth ?? 1.5}
|
|
2792
|
+
/>
|
|
2793
|
+
</svg>
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2297
2797
|
return (
|
|
2298
2798
|
<div
|
|
2299
2799
|
className={cn("size-full rounded-sm border", muted)}
|
|
@@ -2306,6 +2806,112 @@ function DraftPrimitiveContent({
|
|
|
2306
2806
|
);
|
|
2307
2807
|
}
|
|
2308
2808
|
|
|
2809
|
+
function PenPathOverlay({
|
|
2810
|
+
path,
|
|
2811
|
+
closeHover,
|
|
2812
|
+
}: {
|
|
2813
|
+
path: PenPath;
|
|
2814
|
+
closeHover: boolean;
|
|
2815
|
+
}) {
|
|
2816
|
+
const geometry = getPenPathGeometry(path);
|
|
2817
|
+
const pathData = serializePenPath(path);
|
|
2818
|
+
return (
|
|
2819
|
+
<div
|
|
2820
|
+
data-pen-path-overlay
|
|
2821
|
+
className="pointer-events-none absolute z-[90]"
|
|
2822
|
+
style={{
|
|
2823
|
+
left: SURFACE_PADDING + geometry.x,
|
|
2824
|
+
top: SURFACE_PADDING + geometry.y,
|
|
2825
|
+
width: geometry.width,
|
|
2826
|
+
height: geometry.height,
|
|
2827
|
+
}}
|
|
2828
|
+
>
|
|
2829
|
+
<svg
|
|
2830
|
+
className="absolute inset-0 size-full overflow-visible"
|
|
2831
|
+
viewBox={`${geometry.x} ${geometry.y} ${geometry.width} ${geometry.height}`}
|
|
2832
|
+
>
|
|
2833
|
+
{path.nodes.map((node, index) => (
|
|
2834
|
+
<g key={`handles-${index}`}>
|
|
2835
|
+
{node.handleIn ? (
|
|
2836
|
+
<line
|
|
2837
|
+
x1={node.point.x}
|
|
2838
|
+
y1={node.point.y}
|
|
2839
|
+
x2={node.handleIn.x}
|
|
2840
|
+
y2={node.handleIn.y}
|
|
2841
|
+
stroke="var(--design-editor-accent-color)"
|
|
2842
|
+
strokeDasharray="3 3"
|
|
2843
|
+
strokeWidth={1}
|
|
2844
|
+
/>
|
|
2845
|
+
) : null}
|
|
2846
|
+
{node.handleOut ? (
|
|
2847
|
+
<line
|
|
2848
|
+
x1={node.point.x}
|
|
2849
|
+
y1={node.point.y}
|
|
2850
|
+
x2={node.handleOut.x}
|
|
2851
|
+
y2={node.handleOut.y}
|
|
2852
|
+
stroke="var(--design-editor-accent-color)"
|
|
2853
|
+
strokeDasharray="3 3"
|
|
2854
|
+
strokeWidth={1}
|
|
2855
|
+
/>
|
|
2856
|
+
) : null}
|
|
2857
|
+
</g>
|
|
2858
|
+
))}
|
|
2859
|
+
<path
|
|
2860
|
+
d={pathData}
|
|
2861
|
+
fill="none"
|
|
2862
|
+
stroke="rgba(255,255,255,0.95)"
|
|
2863
|
+
strokeLinecap="round"
|
|
2864
|
+
strokeLinejoin="round"
|
|
2865
|
+
strokeWidth={5}
|
|
2866
|
+
/>
|
|
2867
|
+
<path
|
|
2868
|
+
d={pathData}
|
|
2869
|
+
fill="none"
|
|
2870
|
+
stroke="var(--design-editor-accent-color)"
|
|
2871
|
+
strokeLinecap="round"
|
|
2872
|
+
strokeLinejoin="round"
|
|
2873
|
+
strokeWidth={2}
|
|
2874
|
+
/>
|
|
2875
|
+
</svg>
|
|
2876
|
+
{path.nodes.map((node, index) => (
|
|
2877
|
+
<span
|
|
2878
|
+
key={`anchor-${index}`}
|
|
2879
|
+
data-pen-anchor
|
|
2880
|
+
className={cn(
|
|
2881
|
+
"absolute size-2 rounded-[2px] border shadow-sm",
|
|
2882
|
+
index === 0 && closeHover
|
|
2883
|
+
? "scale-125 border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-color)] ring-4 ring-[var(--design-editor-selection-color)]"
|
|
2884
|
+
: "border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-contrast-color)]",
|
|
2885
|
+
)}
|
|
2886
|
+
style={{
|
|
2887
|
+
left: node.point.x - geometry.x - 4,
|
|
2888
|
+
top: node.point.y - geometry.y - 4,
|
|
2889
|
+
}}
|
|
2890
|
+
/>
|
|
2891
|
+
))}
|
|
2892
|
+
{path.nodes.flatMap((node, index) =>
|
|
2893
|
+
[node.handleIn, node.handleOut]
|
|
2894
|
+
.filter(isPoint)
|
|
2895
|
+
.map((handle, handleIndex) => (
|
|
2896
|
+
<span
|
|
2897
|
+
key={`handle-${index}-${handleIndex}`}
|
|
2898
|
+
data-pen-handle
|
|
2899
|
+
className="absolute size-1.5 rounded-full border border-[var(--design-editor-accent-color)] bg-background shadow-sm"
|
|
2900
|
+
style={{
|
|
2901
|
+
left: handle.x - geometry.x - 3,
|
|
2902
|
+
top: handle.y - geometry.y - 3,
|
|
2903
|
+
}}
|
|
2904
|
+
/>
|
|
2905
|
+
)),
|
|
2906
|
+
)}
|
|
2907
|
+
</div>
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
function isPoint(point: Point | undefined): point is Point {
|
|
2912
|
+
return !!point;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2309
2915
|
function Screen({
|
|
2310
2916
|
screen,
|
|
2311
2917
|
metadata,
|
|
@@ -2314,6 +2920,9 @@ function Screen({
|
|
|
2314
2920
|
isSelected,
|
|
2315
2921
|
groupSelected,
|
|
2316
2922
|
handlesEnabled,
|
|
2923
|
+
penActive,
|
|
2924
|
+
creationToolActive,
|
|
2925
|
+
chromeScale,
|
|
2317
2926
|
onPick,
|
|
2318
2927
|
onEdit,
|
|
2319
2928
|
onStartFrameDrag,
|
|
@@ -2329,6 +2938,9 @@ function Screen({
|
|
|
2329
2938
|
isSelected: boolean;
|
|
2330
2939
|
groupSelected: boolean;
|
|
2331
2940
|
handlesEnabled: boolean;
|
|
2941
|
+
penActive: boolean;
|
|
2942
|
+
creationToolActive: boolean;
|
|
2943
|
+
chromeScale: number;
|
|
2332
2944
|
screenContent?: ReactNode;
|
|
2333
2945
|
onPick: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
2334
2946
|
onEdit: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
@@ -2351,7 +2963,8 @@ function Screen({
|
|
|
2351
2963
|
const suppressNextClick = useRef(false);
|
|
2352
2964
|
const emphasized = isActive || isSelected;
|
|
2353
2965
|
const selectionOutlined = isSelected && !groupSelected;
|
|
2354
|
-
const showHoverOutline =
|
|
2966
|
+
const showHoverOutline =
|
|
2967
|
+
!creationToolActive && (!isSelected || !groupSelected);
|
|
2355
2968
|
|
|
2356
2969
|
return (
|
|
2357
2970
|
<div
|
|
@@ -2372,6 +2985,7 @@ function Screen({
|
|
|
2372
2985
|
className="flex h-7 w-full cursor-default items-center justify-between gap-2 px-1"
|
|
2373
2986
|
onMouseDown={(e) => {
|
|
2374
2987
|
if (e.button !== 0) return;
|
|
2988
|
+
if (penActive || creationToolActive) return;
|
|
2375
2989
|
if (e.shiftKey) {
|
|
2376
2990
|
e.stopPropagation();
|
|
2377
2991
|
return;
|
|
@@ -2399,13 +3013,32 @@ function Screen({
|
|
|
2399
3013
|
{metadata.width} x {metadata.height}
|
|
2400
3014
|
</span>
|
|
2401
3015
|
</div>
|
|
2402
|
-
<
|
|
3016
|
+
<button
|
|
3017
|
+
type="button"
|
|
3018
|
+
className={cn(
|
|
3019
|
+
"flex h-5 max-w-[46%] shrink-0 items-center gap-1 overflow-hidden rounded-md border border-border bg-background/95 px-1.5 text-[10px] font-medium text-foreground opacity-0 shadow-sm transition-opacity",
|
|
3020
|
+
"hover:bg-accent hover:text-accent-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
3021
|
+
"group-hover/frame:opacity-100 group-focus-within/frame:opacity-100",
|
|
3022
|
+
emphasized && "opacity-100",
|
|
3023
|
+
)}
|
|
3024
|
+
aria-label={t("multiScreenCanvas.fullView")}
|
|
3025
|
+
title={t("multiScreenCanvas.fullView")}
|
|
3026
|
+
onClick={(event) => onEdit(screen.id, event)}
|
|
3027
|
+
onMouseDown={(event) => {
|
|
3028
|
+
event.preventDefault();
|
|
3029
|
+
event.stopPropagation();
|
|
3030
|
+
}}
|
|
3031
|
+
>
|
|
3032
|
+
<IconMaximize className="size-3 shrink-0" />
|
|
3033
|
+
<span className="truncate">{t("multiScreenCanvas.fullView")}</span>
|
|
3034
|
+
</button>
|
|
2403
3035
|
</div>
|
|
2404
3036
|
<div
|
|
2405
3037
|
data-screen-card
|
|
2406
3038
|
role="button"
|
|
2407
3039
|
tabIndex={0}
|
|
2408
3040
|
onClick={(e) => {
|
|
3041
|
+
if (isInteractiveScreenContentTarget(e.target)) return;
|
|
2409
3042
|
e.stopPropagation();
|
|
2410
3043
|
if (suppressNextClick.current) {
|
|
2411
3044
|
suppressNextClick.current = false;
|
|
@@ -2415,14 +3048,18 @@ function Screen({
|
|
|
2415
3048
|
onPick(screen.id, e);
|
|
2416
3049
|
}}
|
|
2417
3050
|
onDoubleClick={(e) => {
|
|
3051
|
+
if (isInteractiveScreenContentTarget(e.target)) return;
|
|
2418
3052
|
e.preventDefault();
|
|
2419
3053
|
e.stopPropagation();
|
|
2420
3054
|
}}
|
|
2421
3055
|
onMouseDown={(e) => {
|
|
3056
|
+
if (creationToolActive) return;
|
|
3057
|
+
if (isInteractiveScreenContentTarget(e.target)) return;
|
|
2422
3058
|
if (e.detail > 1) {
|
|
2423
3059
|
e.stopPropagation();
|
|
2424
3060
|
return;
|
|
2425
3061
|
}
|
|
3062
|
+
if (penActive) return;
|
|
2426
3063
|
if (e.altKey && e.button === 0) {
|
|
2427
3064
|
suppressNextClick.current = true;
|
|
2428
3065
|
onStartDuplicateGesture(screen, display, e);
|
|
@@ -2446,25 +3083,36 @@ function Screen({
|
|
|
2446
3083
|
style={{
|
|
2447
3084
|
width: geometry.width,
|
|
2448
3085
|
height: geometry.height,
|
|
2449
|
-
cursor:
|
|
3086
|
+
cursor:
|
|
3087
|
+
penActive || creationToolActive
|
|
3088
|
+
? "crosshair"
|
|
3089
|
+
: isSelected
|
|
3090
|
+
? "move"
|
|
3091
|
+
: "pointer",
|
|
2450
3092
|
touchAction: "none",
|
|
2451
3093
|
}}
|
|
2452
3094
|
>
|
|
2453
3095
|
<span
|
|
2454
3096
|
className={cn(
|
|
2455
|
-
"pointer-events-none absolute
|
|
3097
|
+
"pointer-events-none absolute border transition-opacity",
|
|
2456
3098
|
selectionOutlined
|
|
2457
3099
|
? "border-[var(--design-editor-accent-color)] opacity-100"
|
|
2458
3100
|
: showHoverOutline
|
|
2459
3101
|
? "border-[var(--design-editor-accent-color)] opacity-0 group-hover/artboard:opacity-100"
|
|
2460
3102
|
: "border-transparent opacity-0",
|
|
2461
3103
|
)}
|
|
3104
|
+
style={{
|
|
3105
|
+
inset: -5 * chromeScale,
|
|
3106
|
+
borderRadius: 13 * chromeScale,
|
|
3107
|
+
borderWidth: 1.5 * chromeScale,
|
|
3108
|
+
}}
|
|
2462
3109
|
/>
|
|
2463
3110
|
<span
|
|
2464
3111
|
className={cn(
|
|
2465
3112
|
"relative block h-full w-full overflow-hidden rounded-lg border bg-white shadow-2xl transition-colors",
|
|
2466
3113
|
"border-border group-hover/artboard:border-muted-foreground/60",
|
|
2467
3114
|
)}
|
|
3115
|
+
style={{ pointerEvents: penActive ? "none" : undefined }}
|
|
2468
3116
|
>
|
|
2469
3117
|
{screenContent ?? (
|
|
2470
3118
|
<iframe
|
|
@@ -2484,31 +3132,19 @@ function Screen({
|
|
|
2484
3132
|
title={screen.filename}
|
|
2485
3133
|
/>
|
|
2486
3134
|
)}
|
|
3135
|
+
{creationToolActive ? (
|
|
3136
|
+
<span
|
|
3137
|
+
className="absolute inset-0 z-20 cursor-crosshair"
|
|
3138
|
+
aria-hidden="true"
|
|
3139
|
+
/>
|
|
3140
|
+
) : null}
|
|
2487
3141
|
<span className="pointer-events-none absolute inset-0 rounded-[7px] border border-black/5" />
|
|
2488
|
-
<button
|
|
2489
|
-
type="button"
|
|
2490
|
-
className={cn(
|
|
2491
|
-
"absolute right-2 top-2 z-20 flex h-7 max-w-[calc(100%-1rem)] translate-y-1 items-center gap-1 rounded-md border border-border bg-background/95 px-2 text-[10px] font-medium text-foreground opacity-0 shadow-sm backdrop-blur transition-all",
|
|
2492
|
-
"hover:bg-accent hover:text-accent-foreground focus-visible:translate-y-0 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2493
|
-
"group-hover/artboard:translate-y-0 group-hover/artboard:opacity-100 group-focus-visible/artboard:translate-y-0 group-focus-visible/artboard:opacity-100",
|
|
2494
|
-
emphasized && "translate-y-0 opacity-100",
|
|
2495
|
-
)}
|
|
2496
|
-
aria-label={t("multiScreenCanvas.fullView")}
|
|
2497
|
-
title={t("multiScreenCanvas.fullView")}
|
|
2498
|
-
onClick={(event) => onEdit(screen.id, event)}
|
|
2499
|
-
onMouseDown={(event) => {
|
|
2500
|
-
event.preventDefault();
|
|
2501
|
-
event.stopPropagation();
|
|
2502
|
-
}}
|
|
2503
|
-
>
|
|
2504
|
-
<IconMaximize className="size-3" />
|
|
2505
|
-
<span>{t("multiScreenCanvas.fullView")}</span>
|
|
2506
|
-
</button>
|
|
2507
3142
|
</span>
|
|
2508
3143
|
<ResizeHandles
|
|
2509
3144
|
active={selectionOutlined}
|
|
2510
|
-
enabled={handlesEnabled}
|
|
3145
|
+
enabled={!penActive && !creationToolActive && handlesEnabled}
|
|
2511
3146
|
showRotate={false}
|
|
3147
|
+
chromeScale={chromeScale}
|
|
2512
3148
|
onStartResize={(handle, e) => onStartResize(screen.id, handle, e)}
|
|
2513
3149
|
onStartRotate={(e) => onStartRotate(screen.id, e)}
|
|
2514
3150
|
/>
|
|
@@ -2519,26 +3155,31 @@ function Screen({
|
|
|
2519
3155
|
|
|
2520
3156
|
function GroupSelectionBox({
|
|
2521
3157
|
bounds,
|
|
3158
|
+
chromeScale,
|
|
2522
3159
|
onStartResize,
|
|
2523
3160
|
}: {
|
|
2524
3161
|
bounds: NonNullable<ReturnType<typeof getFrameGroupBounds>>;
|
|
3162
|
+
chromeScale: number;
|
|
2525
3163
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
2526
3164
|
}) {
|
|
2527
3165
|
return (
|
|
2528
3166
|
<div
|
|
2529
3167
|
data-frame-shell
|
|
2530
|
-
className="pointer-events-none absolute z-30
|
|
3168
|
+
className="pointer-events-none absolute z-30 border border-[var(--design-editor-accent-color)]"
|
|
2531
3169
|
style={{
|
|
2532
3170
|
left: SURFACE_PADDING + bounds.left,
|
|
2533
3171
|
top: SURFACE_PADDING + bounds.top,
|
|
2534
3172
|
width: bounds.width,
|
|
2535
3173
|
height: bounds.height,
|
|
3174
|
+
borderRadius: 13 * chromeScale,
|
|
3175
|
+
borderWidth: 1.5 * chromeScale,
|
|
2536
3176
|
}}
|
|
2537
3177
|
>
|
|
2538
3178
|
<ResizeHandles
|
|
2539
3179
|
active
|
|
2540
3180
|
enabled
|
|
2541
3181
|
showRotate={false}
|
|
3182
|
+
chromeScale={chromeScale}
|
|
2542
3183
|
onStartResize={onStartResize}
|
|
2543
3184
|
onStartRotate={() => {}}
|
|
2544
3185
|
/>
|
|
@@ -2550,19 +3191,21 @@ function ResizeHandles({
|
|
|
2550
3191
|
active,
|
|
2551
3192
|
enabled,
|
|
2552
3193
|
showRotate = true,
|
|
3194
|
+
chromeScale = 1,
|
|
2553
3195
|
onStartResize,
|
|
2554
3196
|
onStartRotate,
|
|
2555
3197
|
}: {
|
|
2556
3198
|
active: boolean;
|
|
2557
3199
|
enabled: boolean;
|
|
2558
3200
|
showRotate?: boolean;
|
|
3201
|
+
chromeScale?: number;
|
|
2559
3202
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
2560
3203
|
onStartRotate: (e: React.MouseEvent) => void;
|
|
2561
3204
|
}) {
|
|
2562
3205
|
if (!enabled) return null;
|
|
2563
3206
|
|
|
2564
3207
|
const visibleHandleClass = cn(
|
|
2565
|
-
"pointer-events-auto absolute z-20
|
|
3208
|
+
"pointer-events-auto absolute z-20 rounded-[2px] border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-contrast-color)] shadow transition-opacity",
|
|
2566
3209
|
active
|
|
2567
3210
|
? "opacity-100"
|
|
2568
3211
|
: "opacity-0 group-hover/artboard:opacity-100 group-focus-visible/artboard:opacity-100",
|
|
@@ -2576,8 +3219,8 @@ function ResizeHandles({
|
|
|
2576
3219
|
<span
|
|
2577
3220
|
key={config.handle}
|
|
2578
3221
|
data-resize-handle
|
|
2579
|
-
className={
|
|
2580
|
-
style={
|
|
3222
|
+
className={edgeHandleClass}
|
|
3223
|
+
style={edgeHandleStyle(config.handle, config.cursor, chromeScale)}
|
|
2581
3224
|
onMouseDown={(e) => onStartResize(config.handle, e)}
|
|
2582
3225
|
/>
|
|
2583
3226
|
))}
|
|
@@ -2585,8 +3228,8 @@ function ResizeHandles({
|
|
|
2585
3228
|
<span
|
|
2586
3229
|
key={config.handle}
|
|
2587
3230
|
data-resize-handle
|
|
2588
|
-
className={
|
|
2589
|
-
style={
|
|
3231
|
+
className={visibleHandleClass}
|
|
3232
|
+
style={cornerHandleStyle(config.handle, config.cursor, chromeScale)}
|
|
2590
3233
|
onMouseDown={(e) => onStartResize(config.handle, e)}
|
|
2591
3234
|
/>
|
|
2592
3235
|
))}
|
|
@@ -2600,9 +3243,8 @@ function ResizeHandles({
|
|
|
2600
3243
|
active
|
|
2601
3244
|
? "opacity-100"
|
|
2602
3245
|
: "opacity-0 group-hover/artboard:opacity-100 group-focus-visible/artboard:opacity-100",
|
|
2603
|
-
config.className,
|
|
2604
3246
|
)}
|
|
2605
|
-
style={
|
|
3247
|
+
style={rotateHandleStyle(config.corner, chromeScale)}
|
|
2606
3248
|
onMouseDown={onStartRotate}
|
|
2607
3249
|
/>
|
|
2608
3250
|
))
|
|
@@ -2613,40 +3255,22 @@ function ResizeHandles({
|
|
|
2613
3255
|
|
|
2614
3256
|
const CORNER_RESIZE_HANDLE_CONFIGS: Array<{
|
|
2615
3257
|
handle: ResizeHandle;
|
|
2616
|
-
className: string;
|
|
2617
3258
|
cursor: string;
|
|
2618
3259
|
}> = [
|
|
2619
|
-
{ handle: "nw",
|
|
2620
|
-
{ handle: "ne",
|
|
2621
|
-
{ handle: "se",
|
|
2622
|
-
{ handle: "sw",
|
|
3260
|
+
{ handle: "nw", cursor: "nwse-resize" },
|
|
3261
|
+
{ handle: "ne", cursor: "nesw-resize" },
|
|
3262
|
+
{ handle: "se", cursor: "nwse-resize" },
|
|
3263
|
+
{ handle: "sw", cursor: "nesw-resize" },
|
|
2623
3264
|
];
|
|
2624
3265
|
|
|
2625
3266
|
const EDGE_RESIZE_HANDLE_CONFIGS: Array<{
|
|
2626
3267
|
handle: ResizeHandle;
|
|
2627
|
-
className: string;
|
|
2628
3268
|
cursor: string;
|
|
2629
3269
|
}> = [
|
|
2630
|
-
{
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
},
|
|
2635
|
-
{
|
|
2636
|
-
handle: "e",
|
|
2637
|
-
className: "-right-1 bottom-0 top-0 w-2",
|
|
2638
|
-
cursor: "ew-resize",
|
|
2639
|
-
},
|
|
2640
|
-
{
|
|
2641
|
-
handle: "s",
|
|
2642
|
-
className: "-bottom-1 left-0 right-0 h-2",
|
|
2643
|
-
cursor: "ns-resize",
|
|
2644
|
-
},
|
|
2645
|
-
{
|
|
2646
|
-
handle: "w",
|
|
2647
|
-
className: "-left-1 bottom-0 top-0 w-2",
|
|
2648
|
-
cursor: "ew-resize",
|
|
2649
|
-
},
|
|
3270
|
+
{ handle: "n", cursor: "ns-resize" },
|
|
3271
|
+
{ handle: "e", cursor: "ew-resize" },
|
|
3272
|
+
{ handle: "s", cursor: "ns-resize" },
|
|
3273
|
+
{ handle: "w", cursor: "ew-resize" },
|
|
2650
3274
|
];
|
|
2651
3275
|
|
|
2652
3276
|
const ALL_RESIZE_HANDLE_CONFIGS = [
|
|
@@ -2656,13 +3280,61 @@ const ALL_RESIZE_HANDLE_CONFIGS = [
|
|
|
2656
3280
|
|
|
2657
3281
|
const ROTATE_HANDLE_CONFIGS: Array<{
|
|
2658
3282
|
corner: string;
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
3283
|
+
}> = [{ corner: "nw" }, { corner: "ne" }, { corner: "se" }, { corner: "sw" }];
|
|
3284
|
+
|
|
3285
|
+
function edgeHandleStyle(
|
|
3286
|
+
handle: ResizeHandle,
|
|
3287
|
+
cursor: string,
|
|
3288
|
+
chromeScale: number,
|
|
3289
|
+
): CSSProperties {
|
|
3290
|
+
const size = 14 * chromeScale;
|
|
3291
|
+
const offset = -size / 2;
|
|
3292
|
+
if (handle === "n" || handle === "s") {
|
|
3293
|
+
return {
|
|
3294
|
+
cursor,
|
|
3295
|
+
left: 0,
|
|
3296
|
+
right: 0,
|
|
3297
|
+
height: size,
|
|
3298
|
+
[handle === "n" ? "top" : "bottom"]: offset,
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
return {
|
|
3302
|
+
cursor,
|
|
3303
|
+
top: 0,
|
|
3304
|
+
bottom: 0,
|
|
3305
|
+
width: size,
|
|
3306
|
+
[handle === "w" ? "left" : "right"]: offset,
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
function cornerHandleStyle(
|
|
3311
|
+
handle: ResizeHandle,
|
|
3312
|
+
cursor: string,
|
|
3313
|
+
chromeScale: number,
|
|
3314
|
+
): CSSProperties {
|
|
3315
|
+
const size = 10 * chromeScale;
|
|
3316
|
+
const offset = -size / 2;
|
|
3317
|
+
return {
|
|
3318
|
+
cursor,
|
|
3319
|
+
width: size,
|
|
3320
|
+
height: size,
|
|
3321
|
+
borderWidth: Math.max(1, 1.25 * chromeScale),
|
|
3322
|
+
...(handle.includes("n") ? { top: offset } : { bottom: offset }),
|
|
3323
|
+
...(handle.includes("w") ? { left: offset } : { right: offset }),
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
function rotateHandleStyle(corner: string, chromeScale: number): CSSProperties {
|
|
3328
|
+
const size = 28 * chromeScale;
|
|
3329
|
+
const offset = -34 * chromeScale;
|
|
3330
|
+
return {
|
|
3331
|
+
cursor: "grab",
|
|
3332
|
+
width: size,
|
|
3333
|
+
height: size,
|
|
3334
|
+
...(corner.includes("n") ? { top: offset } : { bottom: offset }),
|
|
3335
|
+
...(corner.includes("w") ? { left: offset } : { right: offset }),
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
2666
3338
|
|
|
2667
3339
|
interface FrameEntry {
|
|
2668
3340
|
id: string;
|
|
@@ -2735,6 +3407,17 @@ function isEditableHotkeyTarget(target: EventTarget | null) {
|
|
|
2735
3407
|
return tagName === "input" || tagName === "textarea" || tagName === "select";
|
|
2736
3408
|
}
|
|
2737
3409
|
|
|
3410
|
+
function isInteractiveScreenContentTarget(target: EventTarget | null) {
|
|
3411
|
+
return (
|
|
3412
|
+
target instanceof Element &&
|
|
3413
|
+
Boolean(
|
|
3414
|
+
target.closest(
|
|
3415
|
+
".design-canvas-iframe-wrapper,[data-design-preview-iframe]",
|
|
3416
|
+
),
|
|
3417
|
+
)
|
|
3418
|
+
);
|
|
3419
|
+
}
|
|
3420
|
+
|
|
2738
3421
|
function frameBoundsToGeometry(bounds: {
|
|
2739
3422
|
left: number;
|
|
2740
3423
|
top: number;
|
|
@@ -2828,7 +3511,9 @@ function getDraftGeometryForTool(
|
|
|
2828
3511
|
start: Point,
|
|
2829
3512
|
end: Point,
|
|
2830
3513
|
): FrameGeometry {
|
|
2831
|
-
if (tool === "pen"
|
|
3514
|
+
if (tool === "pen" || tool === "line" || tool === "arrow") {
|
|
3515
|
+
return getPathGeometry([start, end]);
|
|
3516
|
+
}
|
|
2832
3517
|
const options =
|
|
2833
3518
|
tool === "frame"
|
|
2834
3519
|
? {
|
|
@@ -2885,9 +3570,7 @@ function createDraftPrimitive({
|
|
|
2885
3570
|
toolProps,
|
|
2886
3571
|
fallbackText,
|
|
2887
3572
|
}: DraftPrimitiveInput): DraftPrimitive {
|
|
2888
|
-
const id =
|
|
2889
|
-
.toString(36)
|
|
2890
|
-
.slice(2, 8)}`;
|
|
3573
|
+
const id = createDraftId(tool);
|
|
2891
3574
|
const geometry = moved
|
|
2892
3575
|
? getDraftGeometryForTool(tool, start, end)
|
|
2893
3576
|
: getDraftGeometryForTool(tool, start, start);
|
|
@@ -2900,14 +3583,17 @@ function createDraftPrimitive({
|
|
|
2900
3583
|
{ x: start.x + 64, y: start.y + 24 },
|
|
2901
3584
|
{ x: start.x + 128, y: start.y },
|
|
2902
3585
|
];
|
|
2903
|
-
return
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
3586
|
+
return createPenDraftPrimitive(
|
|
3587
|
+
{
|
|
3588
|
+
nodes: pathPoints.map(createCornerNode),
|
|
3589
|
+
closed: false,
|
|
3590
|
+
},
|
|
3591
|
+
{
|
|
3592
|
+
id,
|
|
3593
|
+
stroke: toolProps?.stroke,
|
|
3594
|
+
strokeWidth: toolProps?.strokeWidth,
|
|
3595
|
+
},
|
|
3596
|
+
);
|
|
2911
3597
|
}
|
|
2912
3598
|
if (tool === "text") {
|
|
2913
3599
|
return {
|
|
@@ -2917,11 +3603,30 @@ function createDraftPrimitive({
|
|
|
2917
3603
|
text: toolProps?.text ?? fallbackText,
|
|
2918
3604
|
fill: toolProps?.fill,
|
|
2919
3605
|
stroke: toolProps?.stroke,
|
|
3606
|
+
autoSize: !moved,
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
if (tool === "line" || tool === "arrow") {
|
|
3610
|
+
const pathPoints = moved
|
|
3611
|
+
? [start, end]
|
|
3612
|
+
: [start, { x: start.x + DRAFT_LINE_WIDTH, y: start.y }];
|
|
3613
|
+
return {
|
|
3614
|
+
id,
|
|
3615
|
+
kind: tool,
|
|
3616
|
+
geometry: getPathGeometry(pathPoints),
|
|
3617
|
+
points: pathPoints,
|
|
3618
|
+
stroke: toolProps?.stroke,
|
|
3619
|
+
strokeWidth: toolProps?.strokeWidth ?? 3,
|
|
2920
3620
|
};
|
|
2921
3621
|
}
|
|
2922
3622
|
return {
|
|
2923
3623
|
id,
|
|
2924
|
-
kind:
|
|
3624
|
+
kind:
|
|
3625
|
+
tool === "frame"
|
|
3626
|
+
? "frame"
|
|
3627
|
+
: tool === "ellipse" || tool === "polygon" || tool === "star"
|
|
3628
|
+
? tool
|
|
3629
|
+
: "rectangle",
|
|
2925
3630
|
geometry,
|
|
2926
3631
|
fill: toolProps?.fill,
|
|
2927
3632
|
stroke: toolProps?.stroke,
|
|
@@ -2929,33 +3634,83 @@ function createDraftPrimitive({
|
|
|
2929
3634
|
};
|
|
2930
3635
|
}
|
|
2931
3636
|
|
|
3637
|
+
function createPenDraftPrimitive(
|
|
3638
|
+
path: PenPath,
|
|
3639
|
+
{
|
|
3640
|
+
id = createDraftId("pen"),
|
|
3641
|
+
stroke,
|
|
3642
|
+
strokeWidth,
|
|
3643
|
+
}: { id?: string; stroke?: string; strokeWidth?: number } = {},
|
|
3644
|
+
): DraftPrimitive {
|
|
3645
|
+
const penPath = clonePenPath(path);
|
|
3646
|
+
return {
|
|
3647
|
+
id,
|
|
3648
|
+
kind: "path",
|
|
3649
|
+
geometry: getPenPathGeometry(penPath),
|
|
3650
|
+
penPath,
|
|
3651
|
+
pathData: serializePenPath(penPath),
|
|
3652
|
+
stroke,
|
|
3653
|
+
strokeWidth: strokeWidth ?? 3,
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
function createDraftId(tool: DraftCreationTool) {
|
|
3658
|
+
return `draft-${tool}-${Date.now()}-${Math.random()
|
|
3659
|
+
.toString(36)
|
|
3660
|
+
.slice(2, 8)}`;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
2932
3663
|
function cloneDraftPrimitive(draft: DraftPrimitive): DraftPrimitive {
|
|
2933
3664
|
return {
|
|
2934
3665
|
...draft,
|
|
2935
3666
|
geometry: { ...draft.geometry },
|
|
2936
3667
|
points: draft.points?.map((point) => ({ ...point })),
|
|
3668
|
+
penPath: draft.penPath ? clonePenPath(draft.penPath) : undefined,
|
|
2937
3669
|
};
|
|
2938
3670
|
}
|
|
2939
3671
|
|
|
2940
3672
|
function draftPrimitiveToInsert(
|
|
2941
3673
|
draft: DraftPrimitive,
|
|
2942
3674
|
frameGeometry: FrameGeometry,
|
|
3675
|
+
metadata?: ResolvedScreenMetadata,
|
|
2943
3676
|
): CanvasPrimitiveInsert {
|
|
3677
|
+
const scaleX =
|
|
3678
|
+
metadata && frameGeometry.width > 0
|
|
3679
|
+
? metadata.width / frameGeometry.width
|
|
3680
|
+
: 1;
|
|
3681
|
+
const scaleY =
|
|
3682
|
+
metadata && frameGeometry.height > 0
|
|
3683
|
+
? metadata.height / frameGeometry.height
|
|
3684
|
+
: 1;
|
|
3685
|
+
const toLocalPoint = (point: Point) => ({
|
|
3686
|
+
x: Math.round((point.x - frameGeometry.x) * scaleX),
|
|
3687
|
+
y: Math.round((point.y - frameGeometry.y) * scaleY),
|
|
3688
|
+
});
|
|
3689
|
+
const localGeometry = {
|
|
3690
|
+
...draft.geometry,
|
|
3691
|
+
x: Math.round((draft.geometry.x - frameGeometry.x) * scaleX),
|
|
3692
|
+
y: Math.round((draft.geometry.y - frameGeometry.y) * scaleY),
|
|
3693
|
+
width: Math.max(1, Math.round(draft.geometry.width * scaleX)),
|
|
3694
|
+
height: Math.max(1, Math.round(draft.geometry.height * scaleY)),
|
|
3695
|
+
};
|
|
3696
|
+
const scaledPenPath = draft.penPath
|
|
3697
|
+
? scalePenPathToGeometry(draft.penPath, frameGeometry, {
|
|
3698
|
+
x: 0,
|
|
3699
|
+
y: 0,
|
|
3700
|
+
width: metadata?.width ?? frameGeometry.width,
|
|
3701
|
+
height: metadata?.height ?? frameGeometry.height,
|
|
3702
|
+
})
|
|
3703
|
+
: undefined;
|
|
2944
3704
|
return {
|
|
2945
3705
|
kind: draft.kind,
|
|
2946
|
-
geometry:
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
y: Math.round(draft.geometry.y - frameGeometry.y),
|
|
2950
|
-
},
|
|
2951
|
-
points: draft.points?.map((point) => ({
|
|
2952
|
-
x: Math.round(point.x - frameGeometry.x),
|
|
2953
|
-
y: Math.round(point.y - frameGeometry.y),
|
|
2954
|
-
})),
|
|
3706
|
+
geometry: localGeometry,
|
|
3707
|
+
points: draft.points?.map(toLocalPoint),
|
|
3708
|
+
pathData: scaledPenPath ? serializePenPath(scaledPenPath) : undefined,
|
|
2955
3709
|
text: draft.text,
|
|
2956
3710
|
fill: draft.fill,
|
|
2957
3711
|
stroke: draft.stroke,
|
|
2958
3712
|
strokeWidth: draft.strokeWidth,
|
|
3713
|
+
autoSize: draft.autoSize,
|
|
2959
3714
|
};
|
|
2960
3715
|
}
|
|
2961
3716
|
|
|
@@ -2964,6 +3719,9 @@ function moveDraftPrimitive(
|
|
|
2964
3719
|
dx: number,
|
|
2965
3720
|
dy: number,
|
|
2966
3721
|
): DraftPrimitive {
|
|
3722
|
+
const movedPenPath = draft.penPath
|
|
3723
|
+
? translatePenPath(draft.penPath, dx, dy)
|
|
3724
|
+
: undefined;
|
|
2967
3725
|
return {
|
|
2968
3726
|
...draft,
|
|
2969
3727
|
geometry: {
|
|
@@ -2975,6 +3733,8 @@ function moveDraftPrimitive(
|
|
|
2975
3733
|
x: point.x + dx,
|
|
2976
3734
|
y: point.y + dy,
|
|
2977
3735
|
})),
|
|
3736
|
+
penPath: movedPenPath,
|
|
3737
|
+
pathData: movedPenPath ? serializePenPath(movedPenPath) : draft.pathData,
|
|
2978
3738
|
};
|
|
2979
3739
|
}
|
|
2980
3740
|
|
|
@@ -2982,17 +3742,21 @@ function applyDraftGeometry(
|
|
|
2982
3742
|
draft: DraftPrimitive,
|
|
2983
3743
|
geometry: FrameGeometry,
|
|
2984
3744
|
): DraftPrimitive {
|
|
2985
|
-
if (!draft.points?.length) return { ...draft, geometry };
|
|
2986
3745
|
const origin = draft.geometry;
|
|
2987
3746
|
const scaleX = geometry.width / Math.max(1, origin.width);
|
|
2988
3747
|
const scaleY = geometry.height / Math.max(1, origin.height);
|
|
3748
|
+
const scaledPenPath = draft.penPath
|
|
3749
|
+
? scalePenPathToGeometry(draft.penPath, origin, geometry)
|
|
3750
|
+
: undefined;
|
|
2989
3751
|
return {
|
|
2990
3752
|
...draft,
|
|
2991
3753
|
geometry,
|
|
2992
|
-
points: draft.points
|
|
3754
|
+
points: draft.points?.map((point) => ({
|
|
2993
3755
|
x: geometry.x + (point.x - origin.x) * scaleX,
|
|
2994
3756
|
y: geometry.y + (point.y - origin.y) * scaleY,
|
|
2995
3757
|
})),
|
|
3758
|
+
penPath: scaledPenPath,
|
|
3759
|
+
pathData: scaledPenPath ? serializePenPath(scaledPenPath) : draft.pathData,
|
|
2996
3760
|
};
|
|
2997
3761
|
}
|
|
2998
3762
|
|
|
@@ -3008,6 +3772,11 @@ function getDraftCreationTool(
|
|
|
3008
3772
|
if (
|
|
3009
3773
|
tool === "frame" ||
|
|
3010
3774
|
tool === "rect" ||
|
|
3775
|
+
tool === "line" ||
|
|
3776
|
+
tool === "arrow" ||
|
|
3777
|
+
tool === "ellipse" ||
|
|
3778
|
+
tool === "polygon" ||
|
|
3779
|
+
tool === "star" ||
|
|
3011
3780
|
tool === "text" ||
|
|
3012
3781
|
tool === "pen"
|
|
3013
3782
|
) {
|
|
@@ -3016,6 +3785,42 @@ function getDraftCreationTool(
|
|
|
3016
3785
|
return null;
|
|
3017
3786
|
}
|
|
3018
3787
|
|
|
3788
|
+
function polygonPointsForBox(
|
|
3789
|
+
kind: "polygon" | "star",
|
|
3790
|
+
width: number,
|
|
3791
|
+
height: number,
|
|
3792
|
+
) {
|
|
3793
|
+
const safeWidth = Math.max(1, width);
|
|
3794
|
+
const safeHeight = Math.max(1, height);
|
|
3795
|
+
const cx = safeWidth / 2;
|
|
3796
|
+
const cy = safeHeight / 2;
|
|
3797
|
+
const radius = Math.max(1, Math.min(safeWidth, safeHeight) / 2);
|
|
3798
|
+
const points: Point[] = [];
|
|
3799
|
+
|
|
3800
|
+
if (kind === "polygon") {
|
|
3801
|
+
for (let index = 0; index < 3; index += 1) {
|
|
3802
|
+
const angle = -Math.PI / 2 + (index * Math.PI * 2) / 3;
|
|
3803
|
+
points.push({
|
|
3804
|
+
x: cx + Math.cos(angle) * radius,
|
|
3805
|
+
y: cy + Math.sin(angle) * radius,
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
} else {
|
|
3809
|
+
for (let index = 0; index < 10; index += 1) {
|
|
3810
|
+
const angle = -Math.PI / 2 + (index * Math.PI) / 5;
|
|
3811
|
+
const r = index % 2 === 0 ? radius : radius * 0.45;
|
|
3812
|
+
points.push({
|
|
3813
|
+
x: cx + Math.cos(angle) * r,
|
|
3814
|
+
y: cy + Math.sin(angle) * r,
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
return points
|
|
3820
|
+
.map((point) => `${roundCoord(point.x)},${roundCoord(point.y)}`)
|
|
3821
|
+
.join(" ");
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3019
3824
|
function pointsToPath(points: readonly Point[]) {
|
|
3020
3825
|
if (points.length === 0) return "";
|
|
3021
3826
|
const [first, ...rest] = points;
|
|
@@ -3039,9 +3844,28 @@ function previewDraftPrimitive(preview: DraftCreationPreview): DraftPrimitive {
|
|
|
3039
3844
|
? "frame"
|
|
3040
3845
|
: preview.tool === "text"
|
|
3041
3846
|
? "text"
|
|
3042
|
-
: "
|
|
3847
|
+
: preview.tool === "line" ||
|
|
3848
|
+
preview.tool === "arrow" ||
|
|
3849
|
+
preview.tool === "ellipse" ||
|
|
3850
|
+
preview.tool === "polygon" ||
|
|
3851
|
+
preview.tool === "star"
|
|
3852
|
+
? preview.tool
|
|
3853
|
+
: "rectangle",
|
|
3043
3854
|
geometry: preview.geometry,
|
|
3044
|
-
points:
|
|
3855
|
+
points:
|
|
3856
|
+
preview.points ??
|
|
3857
|
+
(preview.tool === "line" || preview.tool === "arrow"
|
|
3858
|
+
? [
|
|
3859
|
+
{
|
|
3860
|
+
x: preview.geometry.x,
|
|
3861
|
+
y: preview.geometry.y + preview.geometry.height / 2,
|
|
3862
|
+
},
|
|
3863
|
+
{
|
|
3864
|
+
x: preview.geometry.x + preview.geometry.width,
|
|
3865
|
+
y: preview.geometry.y + preview.geometry.height / 2,
|
|
3866
|
+
},
|
|
3867
|
+
]
|
|
3868
|
+
: undefined),
|
|
3045
3869
|
text: "Text", // i18n-ignore preview-only canvas placeholder
|
|
3046
3870
|
};
|
|
3047
3871
|
}
|