From 2482a219dfa4b2e72ed3daddbb37e038f1b726b4 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Fri, 26 Dec 2025 18:29:36 -0600 Subject: [PATCH] feat(wip): stencil palette with drag-and-drop functionality for stencils. --- TODO.txt | 8 +- apps/web/.prettierrc | 4 +- apps/web/src/app.css | 107 +++-- apps/web/src/lib/assets/grid-dots.svg | 1 + apps/web/src/lib/assets/search.svg | 1 + apps/web/src/lib/canvas/Canvas.svelte | 35 +- .../web/src/lib/canvas/canvas-store.svelte.ts | 50 +++ apps/web/src/lib/components/Dialog.svelte | 6 +- apps/web/src/lib/components/Icon.svelte | 22 +- apps/web/src/lib/components/Sheet.svelte | 6 +- apps/web/src/lib/components/StatusBar.svelte | 12 +- .../src/lib/components/StencilPalette.svelte | 393 ++++++++++++++++++ apps/web/src/lib/components/Toolbar.svelte | 366 ++++++++-------- apps/web/src/lib/dnd.svelte.ts | 19 + apps/web/src/routes/+layout.svelte | 4 +- apps/web/src/routes/+page.svelte | 14 +- packages/core/src/index.ts | 1 + packages/core/src/model.ts | 10 +- packages/core/src/stencils/definitions.ts | 127 ++++++ packages/core/src/stencils/index.ts | 3 + packages/core/src/stencils/registry.ts | 31 ++ packages/core/src/stencils/types.ts | 17 + packages/core/src/tools/select.ts | 42 +- 23 files changed, 1023 insertions(+), 256 deletions(-) create mode 100644 apps/web/src/lib/assets/grid-dots.svg create mode 100644 apps/web/src/lib/assets/search.svg create mode 100644 apps/web/src/lib/components/StencilPalette.svelte create mode 100644 apps/web/src/lib/dnd.svelte.ts create mode 100644 packages/core/src/stencils/definitions.ts create mode 100644 packages/core/src/stencils/index.ts create mode 100644 packages/core/src/stencils/registry.ts create mode 100644 packages/core/src/stencils/types.ts diff --git a/TODO.txt b/TODO.txt index e85f0e7..41a0707 100644 --- a/TODO.txt +++ b/TODO.txt @@ -141,7 +141,7 @@ S1. Stencil definition format (core) - spawn: function (atPoint, scale) -> ShapeRecords[] (group) (A stencil can insert 1 shape or a grouped set.) -[ ] Create initial categories (v0): +[ ] Create initial categories: - Flowchart: process, decision, terminator, data, document - Diagrams: server, db, queue, user, browser, mobile - UI: button, input, card, modal @@ -162,6 +162,10 @@ S2. Insert UX [ ] Placement rules: - insert into active layer (if layers exist) - snap to grid if enabled +[ ] Toolbar + - Open menu to insert stencils with button to open drawer +[ ] Drawer + - Stencil selection with search, category filter, and thumbnail previews (DoD): Inserting stencils is faster than drawing shapes manually. @@ -182,7 +186,7 @@ S4. Preview rendering -------------------------------------------------------------------------------- [ ] Render stencil previews in the panel: - - v0: small SVG thumbnails (best) OR draw to offscreen canvas + - small SVG thumbnails (best) & draw to offscreen canvas (DoD): Users can recognize stencils instantly. diff --git a/apps/web/.prettierrc b/apps/web/.prettierrc index 0f2b258..f46793f 100644 --- a/apps/web/.prettierrc +++ b/apps/web/.prettierrc @@ -6,6 +6,6 @@ "objectWrap": "collapse", "tabWidth": 2, "bracketSameLine": true, - "plugins": [ "prettier-plugin-svelte" ], - "overrides": [ { "files": "*.svelte", "options": { "parser": "svelte" } } ] + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] } diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 8d2ec74..2ad0a03 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -129,7 +129,7 @@ --border: var(--line-numbers); --accent: var(--accent-blue); --accent-hover: var(--accent-cyan); - color-scheme: dark; + color-scheme: dark; } * { @@ -143,8 +143,8 @@ body { background-color: var(--surface); color: var(--text); line-height: 1.5; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } button { @@ -158,9 +158,9 @@ button { /* Markdown Styling */ .markdown-body { - color: var(--text); - font-size: 1rem; - line-height: 1.6; + color: var(--text); + font-size: 1rem; + line-height: 1.6; } .markdown-body h1, @@ -169,73 +169,92 @@ button { .markdown-body h4, .markdown-body h5, .markdown-body h6 { - margin-top: 1.5em; - margin-bottom: 0.5em; - font-weight: 600; - line-height: 1.25; + margin-top: 1.5em; + margin-bottom: 0.5em; + font-weight: 600; + line-height: 1.25; } -.markdown-body h1 { font-size: 2em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; } -.markdown-body h2 { font-size: 1.5em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; } -.markdown-body h3 { font-size: 1.25em; } -.markdown-body h4 { font-size: 1em; } +.markdown-body h1 { + font-size: 2em; + border-bottom: 1px solid var(--border); + padding-bottom: 0.3em; +} +.markdown-body h2 { + font-size: 1.5em; + border-bottom: 1px solid var(--border); + padding-bottom: 0.3em; +} +.markdown-body h3 { + font-size: 1.25em; +} +.markdown-body h4 { + font-size: 1em; +} .markdown-body p { - margin-bottom: 1em; + margin-bottom: 1em; } .markdown-body ul, .markdown-body ol { - padding-left: 2em; - margin-bottom: 1em; + padding-left: 2em; + margin-bottom: 1em; } .markdown-body blockquote { - padding: 0 1em; - color: var(--text-muted); - border-left: 0.25em solid var(--border); - margin-bottom: 1em; + padding: 0 1em; + color: var(--text-muted); + border-left: 0.25em solid var(--border); + margin-bottom: 1em; } .markdown-body code { - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: var(--bg-secondary); - border-radius: 6px; - font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: var(--bg-secondary); + border-radius: 6px; + font-family: + ui-monospace, + SFMono-Regular, + SF Mono, + Menlo, + Consolas, + Liberation Mono, + monospace; } .markdown-body pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: var(--bg-secondary); - border-radius: 6px; - margin-bottom: 1em; + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: var(--bg-secondary); + border-radius: 6px; + margin-bottom: 1em; } .markdown-body pre code { - background-color: transparent; - padding: 0; + background-color: transparent; + padding: 0; } .markdown-body a { - color: var(--accent); - text-decoration: none; + color: var(--accent); + text-decoration: none; } .markdown-body a:hover { - text-decoration: underline; + text-decoration: underline; } .markdown-body hr { - height: 0.25em; - padding: 0; - margin: 24px 0; - background-color: var(--border); - border: 0; + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: var(--border); + border: 0; } * { diff --git a/apps/web/src/lib/assets/grid-dots.svg b/apps/web/src/lib/assets/grid-dots.svg new file mode 100644 index 0000000..0bae598 --- /dev/null +++ b/apps/web/src/lib/assets/grid-dots.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/lib/assets/search.svg b/apps/web/src/lib/assets/search.svg new file mode 100644 index 0000000..6ec2b78 --- /dev/null +++ b/apps/web/src/lib/assets/search.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/lib/canvas/Canvas.svelte b/apps/web/src/lib/canvas/Canvas.svelte index a3ff238..5f65403 100644 --- a/apps/web/src/lib/canvas/Canvas.svelte +++ b/apps/web/src/lib/canvas/Canvas.svelte @@ -3,7 +3,10 @@ import StatusBar from '$lib/components/StatusBar.svelte'; import Toolbar from '$lib/components/Toolbar.svelte'; import FileBrowser from '$lib/filebrowser/FileBrowser.svelte'; + import StencilPalette from '$lib/components/StencilPalette.svelte'; import { createCanvasController } from './canvas-store.svelte.ts'; + import { draggingStencil, endDrag } from '$lib/dnd.svelte'; + import { Camera } from 'inkfinite-core'; let canvasEl = $state(null); let textEditorEl = $state(null); @@ -43,6 +46,24 @@ c.markdownEditor.setRef(markdownEditorEl); return () => c.markdownEditor.setRef(null); }); + + function handleDrop(e: DragEvent) { + e.preventDefault(); + const stencil = draggingStencil.current; + if (!stencil || !canvasEl) return; + + const rect = canvasEl.getBoundingClientRect(); + const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top }; + const viewport = c.getViewport(); + const world = Camera.screenToWorld(c.store.getState().camera, screen, viewport); + + c.insertStencil(stencil, world); + endDrag(); + } + + function handleStencilsClick() { + c.stencilPaletteOpen = !c.stencilPaletteOpen; + }
@@ -60,11 +81,19 @@ currentTool={c.tools.currentToolId} onToolChange={c.tools.handleChange} onHistoryClick={c.history.handleClick} + onStencilsClick={handleStencilsClick} store={c.store} getViewport={c.getViewport} canvas={canvasEl ?? undefined} brushStore={c.brushStore} /> -
+
{ + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + }} + ondrop={handleDrop} + role="application"> {/if} + (c.stencilPaletteOpen = false)} />
diff --git a/apps/web/src/lib/components/Toolbar.svelte b/apps/web/src/lib/components/Toolbar.svelte index 19ac353..3843789 100644 --- a/apps/web/src/lib/components/Toolbar.svelte +++ b/apps/web/src/lib/components/Toolbar.svelte @@ -10,7 +10,7 @@ } from '$lib/constants'; import type { Platform } from '$lib/platform'; import type { BrushSettings, BrushStore } from '$lib/status'; - import { themeStore } from '$lib/theme.svelte'; + import { themeStore } from '$lib/theme.svelte'; import type { ArrowShape, BoardMeta, @@ -33,7 +33,7 @@ shapeBounds, SnapshotCommand } from 'inkfinite-core'; - import { fade } from 'svelte/transition'; + import { fade } from 'svelte/transition'; import icon from '../assets/favicon.svg'; import ArrowPopover from './ArrowPopover.svelte'; import BrushPopover from './BrushPopover.svelte'; @@ -61,6 +61,7 @@ platform?: Platform; desktop?: DesktopControls; onOpenBrowser?: () => void; + onStencilsClick?: () => void; }; let { @@ -73,7 +74,8 @@ brushStore, platform = 'web', desktop, - onOpenBrowser + onOpenBrowser, + onStencilsClick }: Props = $props(); let editorState = $derived(store.getState()); @@ -130,11 +132,11 @@ } }); - let showColorControls = $derived( - toolSupportsStyles(currentTool) || - toolSupportsFill(currentTool) || - getSelectedShapes(editorState).some(s => shapeSupportsFill(s) || shapeSupportsStroke(s)) - ); + let showColorControls = $derived( + toolSupportsStyles(currentTool) || + toolSupportsFill(currentTool) || + getSelectedShapes(editorState).some((s) => shapeSupportsFill(s) || shapeSupportsStroke(s)) + ); let position = $state({ x: 20, y: 20 }); let isDragging = $state(false); @@ -181,29 +183,22 @@ function handleDragStart(event: PointerEvent) { isDragging = true; - dragOffset = { - x: event.clientX - position.x, - y: event.clientY - position.y - }; + dragOffset = { x: event.clientX - position.x, y: event.clientY - position.y }; - if(typeof document !== 'undefined') document.body.style.userSelect = 'none'; - + if (typeof document !== 'undefined') document.body.style.userSelect = 'none'; - (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); + (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); } function handleDragMove(event: PointerEvent) { if (!isDragging) return; - position = { - x: event.clientX - dragOffset.x, - y: event.clientY - dragOffset.y - }; + position = { x: event.clientX - dragOffset.x, y: event.clientY - dragOffset.y }; } function handleDragEnd(event: PointerEvent) { isDragging = false; - if(typeof document !== 'undefined') document.body.style.userSelect = ''; - (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId); + if (typeof document !== 'undefined') document.body.style.userSelect = ''; + (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId); } function openInfo() { @@ -347,18 +342,13 @@ ); } - function toolSupportsStyles(tool: ToolId): boolean { - return ( - tool === 'rect' || - tool === 'ellipse' || - tool === 'line' || - tool === 'arrow' - ); - } + function toolSupportsStyles(tool: ToolId): boolean { + return tool === 'rect' || tool === 'ellipse' || tool === 'line' || tool === 'arrow'; + } - function toolSupportsFill(tool: ToolId): boolean { - return tool === 'rect' || tool === 'ellipse' || tool === 'text'; - } + function toolSupportsFill(tool: ToolId): boolean { + return tool === 'rect' || tool === 'ellipse' || tool === 'text'; + } function getSharedColor( shapes: T[], @@ -488,26 +478,24 @@ } -