Pasting / duplicating a freehand drawing places the copy on top of the original #144
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#144
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Bug
Copying a freehand drawing and pasting it (Ctrl+V) — or duplicating it (Ctrl+D) — drops the new drawing at the exact same position as the original. Visually nothing appears to happen, so the user assumes the action did not work. All other object types paste with the standard 20px offset; only freehand drawings have this regression.
Reproduction
Root cause
crates/hero_whiteboard_ui/static/web/js/whiteboard/clipboard.jsline 51 (pasteItems) bumpsdata.x/data.ybyPASTE_OFFSET(20px) for every cloned item.crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.jsline 349 (serializeForServer, drawing branch) writesdata.segmentsas absolute world coordinates by pre-addingnode.x()+node.y()to each local point.crates/hero_whiteboard_ui/static/web/js/whiteboard/app.jsline 483 (renderSingleObjectcase'drawing') only passesdata.segments+ stroke tocreateDrawing; it does NOT passobj.x/obj.y.crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.jsline 2108 (createDrawing) derives the group'sx/yfromminX/minYof the absolute segments, ignoring any caller-supplied position.Net effect: the bumped
data.x/data.yfrompasteItemsare silently discarded for drawings, because the segments still carry the original world coordinates.Other object types are unaffected: their content lives in group-local coords and the group origin is taken from
data.x/data.y.Fix scope
Minimal: inside
pasteItems, whendata.type === 'drawing'anddata.segments(or legacydata.points) is present, translate every point by(offsetX, offsetY)before handing the cloned data torenderSingleObject. Bumpingdata.x/data.yfor drawings can stay (harmless —createDrawingignores it) but the segments themselves are what determine position.Do not change
createDrawingor the drawing serialization — those are touched by many other code paths (server load, collab sync, undo/redo restore) and any change there would regress those paths.Affected files
crates/hero_whiteboard_ui/static/web/js/whiteboard/clipboard.js— the only file in scope. ModifypasteItems.Acceptance criteria
JSON.parse(JSON.stringify(original))already deep-clones, but verify this still holds for the segment translation).Implementation Spec for Issue #144
Objective
Fix freehand-drawing copy/paste/duplicate so the new drawing appears offset by 20px from the original instead of landing exactly on top of it. The bug exists because drawings serialize their stroke data as absolute world coordinates inside
data.data.segments(legacy:data.data.points), so the existingdata.x/data.ybump inpasteItemsis silently discarded by the drawing render path. The fix is to translate the cloned segment points by the paste offset insidepasteItems, drawing-only, leaving the server-load / collab-sync / undo-redo restore paths untouched.Requirements
PASTE_OFFSET = 20) must visibly translate the new drawing on bothpaste()andduplicate().JSON.parse(JSON.stringify(original)), never to the clipboard original (so repeated pastes from the same clipboard entry don't compound the offset).data.data.segmentsisArray<Array<number>>— a list of segments, each a flat[x0, y0, x1, y1, ...].data.data.pointsis a single flatArray<number>.data.x/data.ybump must be preserved (harmless and consistent with other types).data.type === 'drawing'gets the segment translation. Confirmed inserializeForServerthat no other type pre-addsnode.x()/node.y()to itsdatapayload.createDrawing(objects.js),serializeForServer(sync.js), orrenderSingleObject(app.js) — those paths are shared with server load, collab sync, and undo/redo restore.Files to Modify/Create
crates/hero_whiteboard_ui/static/web/js/whiteboard/clipboard.js— modifypasteItemsonly.Implementation Plan
Step 1: Translate drawing segments inside
pasteItemsFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/clipboard.jspasteItems, after theJSON.parse(JSON.stringify(original))deep clone and after the existingdata.x/data.ybump, add a drawing-only branch:data.type === 'drawing'and presence ofdata.data.data.data.segmentsis a non-empty array, iterate each segment and addoffsetXto even indices andoffsetYto odd indices in place on the clone.data.data.pointsis a non-empty array (legacy single segment), apply the same in-place translation to that flat array.delete data.id,nextTempId,renderSingleObject,getObject) exactly as-is.Dependencies: none
Acceptance Criteria
data.data.points(single flat array) still paste with the correct offset.serializeForServer,renderSingleObject, andcreateDrawingwere not regressed.Notes
JSON.parse(JSON.stringify(original))already produces fully independent arrays forsegmentsandpoints, so in-place mutation on the clone is safe and will not contaminate the clipboard original. This is essential for repeated pastes.serializeForServer(sync.js line 349-362) buildsdata.segmentsasArray<Array<number>>, one inner array per pen-down stroke. The translation must loop over the outer array and translate each inner flat array.data.points:app.jsline 487-488 still accepts a single flatdata.pointsarray as a fallback for older saved drawings. The fix handles both shapes.serializeForServerthat no other type pre-addsnode.x()/node.y()to itsdatapayload —kanban,calendar,mindmap,group, etc. all rely on top-leveldata.x/data.y. The new translation must be gated strictly ondata.type === 'drawing'.createDrawingderives the Konva group'sx/yfromminX/minYof the supplied (absolute) points, so translating the points before callingrenderSingleObjectwill naturally cause the group to land at the offset position with the same internal local-point layout.data.databeing missing and against malformed segments (odd length, length < 4); skip rather than throw so a single corrupt segment can't break the entire paste.Test Results
cargo test --workspace --lib— PASScargo fmt --check— PASScargo clippy --workspace --all-targets -- -D warnings— PASSNotes
This fix is JS only (
crates/hero_whiteboard_ui/static/web/js/whiteboard/clipboard.js). The Rust workspace tests do not exercise this code path; they confirm the workspace still builds, formats, and lints cleanly. Manual UI verification is required: Ctrl+C / Ctrl+V on a freehand drawing should produce a duplicate offset 20px down-right; Ctrl+D duplicates the same way; non-drawing types continue to paste with the existing 20px offset.Implementation Summary
Fix for freehand drawings landing on top of the original when copy/pasted or duplicated.
Changes
crates/hero_whiteboard_ui/static/web/js/whiteboard/clipboard.js— extendedpasteItemswith a drawing-only branch that translates the cloned segment points by(offsetX, offsetY). Handles bothdata.data.segments(multi-segment, current format) and legacydata.data.points(single flat array). Defensive: skips malformed segments (odd length or length < 4) so a single corrupt segment cannot break the whole paste. Mutation happens on the deep clone produced byJSON.parse(JSON.stringify(original)), so the clipboard original is never touched and repeated pastes from the same clipboard entry each land at original + 20.No changes to
createDrawing(objects.js),serializeForServer(sync.js), orrenderSingleObject(app.js) — those paths are shared with server load, collab sync, and undo/redo restore and must not regress. The existing top-leveldata.x/data.ybump remains in place (harmless for drawings, required for every other type).Test Results
cargo test --workspace --lib— PASScargo fmt --check— PASScargo clippy --workspace --all-targets -- -D warnings— PASSManual Verification Required
The fix is JS only and the workspace has no JS test harness. Please verify: