web based infinite canvas
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: stencil enhancements

* ungroup functionality with kb shortcut

* grid snapping for stencil placement

* stencil dnd recovery

* update default stencil definitions.

+83 -75
+2 -72
TODO.txt
··· 131 131 pleasant insertion workflow. No sharing/community libraries yet—just "your own". 132 132 133 133 -------------------------------------------------------------------------------- 134 - S1. Stencil definition format (core) 135 - -------------------------------------------------------------------------------- 136 - 137 - /packages/core/src/stencils: 138 - [x] Define Stencil: 139 - - id, name, category, tags[] 140 - - preview: { kind: 'svg'|'canvas', data } 141 - - spawn: function (atPoint, scale) -> ShapeRecords[] (group) 142 - (A stencil can insert 1 shape or a grouped set.) 143 - 144 - [x] Create initial categories: 145 - - Flowchart: process, decision, terminator, data, document 146 - - Diagrams: server, db, queue, user, browser, mobile 147 - - UI: button, input, card, modal 148 - - Etc: Post-It style/sticky notes, index cards, speech bubble 149 - 150 - (DoD): Stencils load as data and can spawn shapes deterministically. 151 - 152 - -------------------------------------------------------------------------------- 153 134 S2. Insert UX 154 135 -------------------------------------------------------------------------------- 155 136 156 137 /apps/web: 157 - [x] Stencils drawer/palette: 158 - - search (name + tags) 159 - - category filter 160 - - click inserts at viewport center OR 161 - drag ghost preview onto canvas and drop 162 138 [ ] Placement rules: 163 - - insert into active layer (if layers exist) 164 - - snap to grid if enabled 165 - [x] Toolbar 166 - - Open menu to insert stencils with button to open drawer 167 - [x] Drawer 168 - - Stencil selection with search, category filter, and thumbnail previews 139 + - insert into active layer (when layers are added) 140 + - [x] snap to grid if enabled 169 141 170 142 (DoD): Inserting stencils is faster than drawing shapes manually. 171 - 172 - -------------------------------------------------------------------------------- 173 - S3. Grouping behavior 174 - -------------------------------------------------------------------------------- 175 - 176 - [ ] When a stencil spawns multiple shapes: 177 - - create a GroupRecord OR a "groupId" on shapes (your existing grouping 178 - model) 179 - - allow move as one unit 180 - - ungroup command 181 - 182 - (DoD): Multi-shape stencils behave like a single object until ungrouped. 183 - 184 - -------------------------------------------------------------------------------- 185 - S4. Preview rendering 186 - -------------------------------------------------------------------------------- 187 - 188 - [x] Render stencil previews in the panel: 189 - - small SVG thumbnails (best) & draw to offscreen canvas 190 - 191 - (DoD): Users can recognize stencils instantly. 192 - 193 - -------------------------------------------------------------------------------- 194 - S5. Persistence + versioning 195 - -------------------------------------------------------------------------------- 196 - 197 - [ ] Stencils are "code assets": 198 - - version them with the app 199 - - inserted shapes are normal shapes (no dependency on stencil after 200 - insertion) 201 - 202 - (DoD): Old docs do not break if you change stencil definitions later. 203 - 204 - -------------------------------------------------------------------------------- 205 - S6. Tests 206 - -------------------------------------------------------------------------------- 207 - 208 - [ ] spawn() returns valid records with unique ids and correct initial positions 209 - [ ] group insert produces expected selection and undo/redo works 210 - [ ] search indexing returns correct stencils for tag queries 211 - 212 - (DoD): Stencils are reliable and don’t corrupt docs. 213 143 214 144 ================================================================================ 215 145 Parking Lot *wb-pl*
+11 -1
apps/web/src/lib/canvas/Canvas.svelte
··· 54 54 dataTransferTypes: e.dataTransfer?.types 55 55 }); 56 56 e.preventDefault(); 57 - const stencil = draggingStencil.current; 57 + 58 + let stencil = draggingStencil.current; 59 + 60 + if (!stencil && e.dataTransfer) { 61 + const stencilId = e.dataTransfer.getData('application/x-inkfinite-stencil'); 62 + if (stencilId) { 63 + console.log('[Canvas] Recovering stencil from dataTransfer:', stencilId); 64 + stencil = stencils.registry.get(stencilId) ?? null; 65 + } 66 + } 67 + 58 68 console.log('[Canvas] Dragging stencil state:', stencil); 59 69 60 70 if (!stencil || !canvasEl) {
+50 -1
apps/web/src/lib/canvas/canvas-store.svelte.ts
··· 306 306 return { ...state, doc: { ...state.doc, pages: { ...state.doc.pages, [pageId]: { ...page, shapeIds } } } }; 307 307 } 308 308 309 + function ungroupSelection(state: EditorState): EditorState | null { 310 + const selectionIds = state.ui.selectionIds; 311 + if (selectionIds.length === 0) { 312 + return null; 313 + } 314 + 315 + const groupsToDissolve = new Set<string>(); 316 + const shapes = state.doc.shapes; 317 + 318 + for (const id of selectionIds) { 319 + const shape = shapes[id]; 320 + if (shape && shape.groupId) { 321 + groupsToDissolve.add(shape.groupId); 322 + } 323 + } 324 + 325 + if (groupsToDissolve.size === 0) { 326 + return null; 327 + } 328 + 329 + const newShapes = { ...shapes }; 330 + let changed = false; 331 + 332 + for (const id in newShapes) { 333 + const shape = newShapes[id]; 334 + if (shape.groupId && groupsToDissolve.has(shape.groupId)) { 335 + const newShape = { ...shape }; 336 + delete newShape.groupId; 337 + newShapes[id] = newShape; 338 + changed = true; 339 + } 340 + } 341 + 342 + if (!changed) return null; 343 + 344 + return { ...state, doc: { ...state.doc, shapes: newShapes } }; 345 + } 346 + 309 347 function handleKeyboardShortcuts(state: EditorState, action: Action): EditorState | null { 310 348 if (action.type !== "key-down") { 311 349 return null; ··· 370 408 } 371 409 if (primaryModifier && action.key === "[") { 372 410 return reorderSelection(state, "backward"); 411 + } 412 + 413 + if (primaryModifier && action.modifiers.shift && (action.key === "g" || action.key === "G")) { 414 + return ungroupSelection(state); 373 415 } 374 416 375 417 return null; ··· 644 686 }; 645 687 646 688 function insertStencil(stencil: Stencil, worldPos: { x: number; y: number }) { 689 + const snap = snapStore.get(); 690 + let pos = { ...worldPos }; 691 + if (snap.snapEnabled && snap.gridEnabled) { 692 + const gridSize = snap.gridSize; 693 + pos = { x: Math.round(pos.x / gridSize) * gridSize, y: Math.round(pos.y / gridSize) * gridSize }; 694 + } 695 + 647 696 const state = store.getState(); 648 697 const pageId = state.ui.currentPageId; 649 698 if (!pageId) return; 650 699 651 - const shapes = stencil.spawn(worldPos); 700 + const shapes = stencil.spawn(pos); 652 701 const groupId = shapes.length > 1 ? createId("group") : undefined; 653 702 654 703 const newShapes = { ...state.doc.shapes };
+13
apps/web/src/lib/components/Toolbar.svelte
··· 1125 1125 flex-direction: column; 1126 1126 gap: 0.25rem; 1127 1127 } 1128 + 1129 + .toolbar__colors { 1130 + display: flex; 1131 + flex-direction: column; 1132 + gap: 8px; 1133 + } 1134 + 1135 + .toolbar__color-control { 1136 + display: grid; 1137 + grid-template-columns: repeat(2, 1fr); 1138 + gap: 4px; 1139 + text-align: right; 1140 + } 1128 1141 </style>
+7 -1
packages/core/src/stencils/definitions.ts
··· 112 112 w: 300, 113 113 h: 200, 114 114 fill: "#ffffff", 115 - stroke: "#dddddd", 115 + stroke: "#333333", 116 116 radius: 8, 117 + }), 118 + ShapeRecord.createLine("placeholder_page", at.x, at.y + 50, { 119 + a: { x: 0, y: 0 }, 120 + b: { x: 300, y: 0 }, 121 + stroke: "#333333", 122 + width: 1, 117 123 }), 118 124 ], 119 125 };