@@ -7,7 +7,13 @@ import {
77 stackSelectedWidgetsInEditor as stackSelectionInEditor ,
88 unstackSelectedWidgetsInEditor as unstackSelectionInEditor ,
99} from '@/ui-desks/stacks/editor-commands' ;
10- import { getStackId } from '@/ui-desks/stacks/utils' ;
10+ import {
11+ getStackAnchorFromMember ,
12+ getStackHome ,
13+ getStackId ,
14+ getStackOrder ,
15+ getStackZIndexFromMember ,
16+ } from '@/ui-desks/stacks/utils' ;
1117import { createDeskWidget } from '@/ui-desks/widgets/create-widget' ;
1218import { getSelectedWidgetToolbarItem } from '@/ui-desks/widgets/toolbar-selection' ;
1319import {
@@ -16,6 +22,10 @@ import {
1622 canvasShapesToDeskStacks ,
1723 deskConfigToCanvasShapes ,
1824 deskWidgetToCanvasShape ,
25+ getDerivedDeskCanvasRecordSourceId ,
26+ hasOnlyDeskCanvasRecordResolutionStateChange ,
27+ isDerivedDeskCanvasRecord ,
28+ isPersistentDeskCanvasShape ,
1929} from '../tldraw-adapter' ;
2030import { DESK_CONFIG_VERSION , type DeskConfig } from '../types' ;
2131import type { SelectedWidgetToolbarItem , AddDeskWidgetOptions } from './context' ;
@@ -27,6 +37,12 @@ interface CanvasStoreChanges {
2737 removed : Record < string , unknown > ;
2838}
2939
40+ interface DerivedWidgetAnchor {
41+ x : number ;
42+ y : number ;
43+ zIndex ?: string ;
44+ }
45+
3046export function hydrateEditorFromDesk ( editor : Editor , desk : DeskConfig ) {
3147 const existingShapes = editor . getCurrentPageShapes ( ) ;
3248 if ( existingShapes . length > 0 ) {
@@ -133,7 +149,7 @@ export function updateSelectedWidgetPropsInEditor(
133149
134150export function removeSelectedWidgetFromEditor ( editor : Editor ) {
135151 const selection = getCurrentSelectedWidgetSelection ( editor ) ;
136- if ( ! selection ) {
152+ if ( ! selection || ! selection . item . canRemove ) {
137153 return false ;
138154 }
139155
@@ -167,6 +183,37 @@ export function hasCameraChange( changes: CanvasStoreChanges ) {
167183 return records . some ( isCameraRecord ) ;
168184}
169185
186+ export function hasPersistentDocumentChange ( changes : CanvasStoreChanges ) {
187+ const addedOrRemovedRecords = [
188+ ...Object . values ( changes . added ) ,
189+ ...Object . values ( changes . removed ) ,
190+ ] ;
191+ if ( addedOrRemovedRecords . some ( isPersistentDocumentRecord ) ) {
192+ return true ;
193+ }
194+
195+ return Object . values ( changes . updated ) . some ( ( [ previousRecord , nextRecord ] ) => {
196+ if ( isShapeRecord ( previousRecord ) || isShapeRecord ( nextRecord ) ) {
197+ if ( hasOnlyDeskCanvasRecordResolutionStateChange ( previousRecord , nextRecord ) ) {
198+ return false ;
199+ }
200+
201+ if (
202+ isDerivedDeskCanvasRecord ( previousRecord ) ||
203+ isDerivedDeskCanvasRecord ( nextRecord )
204+ ) {
205+ return hasDerivedShapePersistenceChange ( previousRecord , nextRecord ) ;
206+ }
207+
208+ return (
209+ isPersistentDocumentRecord ( previousRecord ) || isPersistentDocumentRecord ( nextRecord )
210+ ) ;
211+ }
212+
213+ return true ;
214+ } ) ;
215+ }
216+
170217function getCurrentSelectedWidgetSelection ( editor : Editor ) {
171218 const selectedShapeIds = editor . getSelectedShapeIds ( ) ;
172219 if ( selectedShapeIds . length === 0 ) {
@@ -178,6 +225,11 @@ function getCurrentSelectedWidgetSelection( editor: Editor ) {
178225 return null ;
179226 }
180227
228+ const derivedSourceSelection = getDerivedSourceWidgetSelection ( editor , shapes as TLShape [ ] ) ;
229+ if ( derivedSourceSelection ) {
230+ return derivedSourceSelection ;
231+ }
232+
181233 const widgets = shapes . map ( ( shape ) => canvasShapeToDeskWidget ( shape as TLShape ) ) ;
182234 if ( widgets . some ( ( widget ) => ! widget ) ) {
183235 return null ;
@@ -201,11 +253,18 @@ function getCurrentSelectedWidgetSelection( editor: Editor ) {
201253 } ;
202254}
203255
204- function getCurrentDeskWidgets ( editor : Editor ) {
205- return editor
206- . getCurrentPageShapes ( )
256+ export function getCurrentDeskWidgets ( editor : Editor ) {
257+ const shapes = editor . getCurrentPageShapes ( ) ;
258+ const derivedAnchors = getDerivedWidgetAnchorsBySourceId ( shapes ) ;
259+
260+ return shapes
261+ . filter ( isPersistentDeskCanvasShape )
207262 . map ( canvasShapeToDeskWidget )
208- . filter ( ( widget ) => widget !== null ) ;
263+ . filter ( ( widget ) : widget is DeskWidget => widget !== null )
264+ . map ( ( widget ) => {
265+ const anchor = derivedAnchors . get ( widget . id ) ;
266+ return anchor ? { ...widget , ...anchor } : widget ;
267+ } ) ;
209268}
210269
211270function getCurrentDeskStacks ( editor : Editor ) {
@@ -220,6 +279,155 @@ function isCameraRecord( value: unknown ) {
220279 ) ;
221280}
222281
282+ function isShapeRecord ( value : unknown ) {
283+ return (
284+ Boolean ( value ) &&
285+ typeof value === 'object' &&
286+ ( value as { typeName ?: unknown } ) . typeName === 'shape'
287+ ) ;
288+ }
289+
290+ function isPersistentDocumentRecord ( value : unknown ) {
291+ if ( isShapeRecord ( value ) ) {
292+ return ! isDerivedDeskCanvasRecord ( value ) ;
293+ }
294+
295+ return true ;
296+ }
297+
298+ function hasDerivedShapePersistenceChange ( previousRecord : unknown , nextRecord : unknown ) {
299+ if (
300+ ! isDerivedDeskCanvasRecord ( previousRecord ) ||
301+ ! isDerivedDeskCanvasRecord ( nextRecord )
302+ ) {
303+ return false ;
304+ }
305+
306+ return (
307+ getDerivedShapePersistenceSignature ( previousRecord ) !==
308+ getDerivedShapePersistenceSignature ( nextRecord )
309+ ) ;
310+ }
311+
312+ function getDerivedShapePersistenceSignature ( value : unknown ) {
313+ const shape = value as Partial < TLShape > ;
314+ const meta = ( shape . meta ?? { } ) as Record < string , unknown > ;
315+
316+ return JSON . stringify ( {
317+ x : shape . x ,
318+ y : shape . y ,
319+ rotation : shape . rotation ,
320+ index : shape . index ,
321+ deskStackExpanded : meta . deskStackExpanded ,
322+ deskStackHomeX : meta . deskStackHomeX ,
323+ deskStackHomeY : meta . deskStackHomeY ,
324+ deskStackHomeZIndex : meta . deskStackHomeZIndex ,
325+ } ) ;
326+ }
327+
328+ function getDerivedSourceWidgetSelection ( editor : Editor , shapes : TLShape [ ] ) {
329+ const sourceWidgetId = getDerivedSelectionSourceWidgetId ( shapes ) ;
330+ if ( ! sourceWidgetId ) {
331+ return null ;
332+ }
333+
334+ const sourceShape = editor
335+ . getCurrentPageShapes ( )
336+ . find ( ( shape ) => canvasShapeToDeskWidget ( shape ) ?. id === sourceWidgetId ) ;
337+ if ( ! sourceShape ) {
338+ return null ;
339+ }
340+
341+ const sourceWidget = canvasShapeToDeskWidget ( sourceShape ) ;
342+ if ( ! sourceWidget ) {
343+ return null ;
344+ }
345+
346+ const item = getSelectedWidgetToolbarItem ( [ sourceWidget ] , {
347+ stackIds : [ ] ,
348+ canRemove : false ,
349+ } ) ;
350+ if ( ! item ) {
351+ return null ;
352+ }
353+
354+ return {
355+ item,
356+ shapes : [ sourceShape ] ,
357+ } ;
358+ }
359+
360+ function getDerivedSelectionSourceWidgetId ( shapes : TLShape [ ] ) {
361+ let sourceWidgetId : string | null = null ;
362+ for ( const shape of shapes ) {
363+ const nextSourceWidgetId = getDerivedDeskCanvasRecordSourceId ( shape ) ;
364+ if ( ! nextSourceWidgetId ) {
365+ return null ;
366+ }
367+
368+ if ( sourceWidgetId === null ) {
369+ sourceWidgetId = nextSourceWidgetId ;
370+ } else if ( sourceWidgetId !== nextSourceWidgetId ) {
371+ return null ;
372+ }
373+ }
374+
375+ return sourceWidgetId ;
376+ }
377+
378+ function getDerivedWidgetAnchorsBySourceId ( shapes : TLShape [ ] ) {
379+ const shapesBySourceId = new Map < string , TLShape [ ] > ( ) ;
380+ for ( const shape of shapes ) {
381+ const sourceWidgetId = getDerivedDeskCanvasRecordSourceId ( shape ) ;
382+ if ( ! sourceWidgetId ) {
383+ continue ;
384+ }
385+
386+ shapesBySourceId . set ( sourceWidgetId , [
387+ ...( shapesBySourceId . get ( sourceWidgetId ) ?? [ ] ) ,
388+ shape ,
389+ ] ) ;
390+ }
391+
392+ return new Map (
393+ Array . from ( shapesBySourceId , ( [ sourceWidgetId , sourceShapes ] ) => [
394+ sourceWidgetId ,
395+ getDerivedWidgetAnchor ( sourceShapes ) ,
396+ ] ) . filter ( ( entry ) : entry is [ string , DerivedWidgetAnchor ] => entry [ 1 ] !== null )
397+ ) ;
398+ }
399+
400+ function getDerivedWidgetAnchor ( shapes : TLShape [ ] ) : DerivedWidgetAnchor | null {
401+ const firstShape = [ ...shapes ] . sort ( ( first , second ) => {
402+ const firstStackOrder = getStackOrder ( first ) ;
403+ const secondStackOrder = getStackOrder ( second ) ;
404+ return firstStackOrder - secondStackOrder || sortByIndex ( second , first ) ;
405+ } ) [ 0 ] ;
406+ if ( ! firstShape ) {
407+ return null ;
408+ }
409+
410+ const stackId = getStackId ( firstShape ) ;
411+ if ( ! stackId ) {
412+ return {
413+ x : firstShape . x ,
414+ y : firstShape . y ,
415+ zIndex : firstShape . index ,
416+ } ;
417+ }
418+
419+ const order = getStackOrder ( firstShape ) ;
420+ const home = getStackHome ( firstShape ) ;
421+ if ( home ) {
422+ return home ;
423+ }
424+
425+ return {
426+ ...getStackAnchorFromMember ( firstShape , order ) ,
427+ zIndex : getStackZIndexFromMember ( firstShape . index , order ) ,
428+ } ;
429+ }
430+
223431function ensureContentVisible ( editor : Editor ) {
224432 const shapes = editor . getCurrentPageShapes ( ) ;
225433 if ( shapes . length === 0 ) {
0 commit comments