Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions client/src/components/tapestry-elements/item-toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
deleteSelectionItems,
pasteItemSize,
removeFromGroup,
reorderItems,
updateItem,
} from '../../../pages/tapestry/view-model/store-commands/items'
import {
Expand Down Expand Up @@ -250,6 +251,14 @@ export function useEditMoreMenu({
onSelectSubmenu('')
}
const remove = () => dispatch(isMultiselection ? deleteSelectionItems() : deleteItems(dto.id))
const bringTo = (to: 'front' | 'back') => {
dispatch(
reorderItems(
dtoArray.map(({ id }) => id),
to,
),
)
}

useKeyboardShortcuts(
active
Expand Down Expand Up @@ -308,6 +317,14 @@ export function useEditMoreMenu({
>
Drop shadow
</MenuItemToggle>,
<MenuItemButton onClick={() => bringTo('front')} className={styles.menuItemButton}>
Bring to front
<Icon className="button-icon" icon="flip_to_front" />
</MenuItemButton>,
<MenuItemButton onClick={() => bringTo('back')} className={styles.menuItemButton}>
Bring to back
<Icon className="button-icon" icon="flip_to_back" />
</MenuItemButton>,
<MenuItemButton shortcut="meta + C" onClick={copyItem} className={styles.menuItemButton}>
Copy
</MenuItemButton>,
Expand Down
7 changes: 6 additions & 1 deletion client/src/model/data/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
import { FetchContentTypeProxyDto } from 'tapestry-shared/src/data-transfer/resources/dtos/proxy'
import { ItemType, MediaItemType } from 'tapestry-core/src/data-format/schemas/item'
import { viewModelFromTapestry } from 'tapestry-core-client/src/view-model/utils'
import { DEFAULT_LAYER } from '../../pages/tapestry/view-model/utils'

export const EDITABLE_TAPESTRY_PROPS = [
'background',
Expand All @@ -74,6 +75,7 @@ const BASE_EDITABLE_ITEM_PROPS = [
'type',
'groupId',
'notes',
'layer',
] as const satisfies (keyof ItemDto & keyof ItemUpdateDto)[]

export const EDITABLE_TEXT_ITEM_PROPS = [
Expand Down Expand Up @@ -226,6 +228,7 @@ export function createTextItem(text = '', tapestryId: string): TextItemCreateDto
position: ORIGIN,
tapestryId,
backgroundColor: textItemColor,
layer: DEFAULT_LAYER,
}
}

Expand All @@ -239,6 +242,7 @@ export function createActionButtonItem(text = '', tapestryId: string): ActionBut
backgroundColor: userSettings.getTapestrySettings(tapestryId).textItemColor,
tapestryId,
text,
layer: DEFAULT_LAYER,
}
}

Expand Down Expand Up @@ -309,7 +313,8 @@ export async function createMediaItem<T extends MediaItemType>(
dropShadow: true,
position: ORIGIN,
tapestryId,
} as MediaItemCreateDto & { type: T }
layer: DEFAULT_LAYER,
} satisfies MediaItemCreateDto as MediaItemCreateDto & { type: T }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the satisfies operator?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To enforce type constraint on the object. Otherwise a missing layer prop would not result a compile time error.

}

// TODO: Handle the scenario where the source is an S3 object and therefore needs to be cloned.
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/tapestry/tapestry-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export function TapestryEditorCanvas({ className, style }: PropsWithStyle) {

usePresentationShortcuts(isView)

return <TapestryCanvas classes={{ root: className }} style={style} orderByPosition={isView} />
return <TapestryCanvas classes={{ root: className }} style={style} />
}
21 changes: 20 additions & 1 deletion client/src/pages/tapestry/view-model/store-commands/items.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Draft } from 'immer'
import { max, merge, sortBy, sum } from 'lodash-es'
import { max, merge, partition, sortBy, sum } from 'lodash-es'
import { StoreMutationCommand } from 'tapestry-core-client/src/lib/store/index'
import { tween } from 'tapestry-core-client/src/view-model/tweening'
import {
Expand All @@ -26,6 +26,7 @@ import { itemUpload } from '../../../../services/item-upload'
import {
getGridDimensions,
getGridIndices,
getMaxLayer,
getMultiselectRectangle,
GridState,
reassignPresentationStep,
Expand Down Expand Up @@ -81,8 +82,10 @@ export function addAndPositionItems(
? translate(centerAtPoint, mul(-1, translation), scale)
: centerAtPoint
const move = vector(boundingRect.center, center)
let maxLayer = getMaxLayer(store)
items.forEach((item) => {
item.dto.position = translate(item.dto.position, move)
item.dto.layer = ++maxLayer
})

store.dispatch(insertItems(items), selectItems(items.map((i) => i.dto.id)))
Expand Down Expand Up @@ -353,3 +356,19 @@ export function removeFromGroup(itemId: string): StoreMutationCommand<EditableTa
}
}
}

export function reorderItems(
ids: OneOrMore<string>,
to: 'back' | 'front',
): StoreMutationCommand<EditableTapestryViewModel> {
return (_, { store }) => {
const itemIds = new Set(ensureArray(ids))

const [frontItems, backItems] = partition(idMapToArray(store.get(`items`)), (i) =>
to === 'front' ? itemIds.has(i.dto.id) : !itemIds.has(i.dto.id),
)

const allItems = [...sortBy(backItems, ['layer']), ...sortBy(frontItems, ['layer'])]
store.dispatch(...allItems.map((item, layer) => updateItem(item.dto.id, { dto: { layer } })))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make a single batch-mutation request instead of N updates, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this updateItem command is also used when manipulating a selection - deleting, arranging, grouping....

}
}
12 changes: 11 additions & 1 deletion client/src/pages/tapestry/view-model/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WritableDraft } from 'immer'
import { chunk, zip } from 'lodash-es'
import { chunk, maxBy, zip } from 'lodash-es'
import {
getBoundingRectangle,
MULTISELECT_RECTANGLE_PADDING,
Expand All @@ -22,6 +22,7 @@ import {
EditableItemViewModel,
EditablePresentationStepViewModel,
EditableRelViewModel,
EditableTapestryViewModel,
ItemUIComponent,
MultiselectionUIComponent,
SelectionResizeState,
Expand All @@ -30,6 +31,10 @@ import {
import { DeserializeResult } from '../../../stage/data-transfer-handler'
import { addAndPositionItems } from './store-commands/items'
import { setIAImport, setLargeFiles, setSnackbar } from './store-commands/tapestry'
import { Store } from 'tapestry-core-client/src/lib/store'
import { idMapToArray } from 'tapestry-core/src/utils'

export const DEFAULT_LAYER = 0

export function getMultiselectRectangle(
selectionItems: EditableItemViewModel[],
Expand Down Expand Up @@ -181,3 +186,8 @@ export async function insertDataTransfer(
})
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getMaxLayer(store: Store<EditableTapestryViewModel, any>) {
return maxBy(idMapToArray(store.get('items')), 'dto.layer')?.dto.layer ?? DEFAULT_LAYER
}
31 changes: 3 additions & 28 deletions client/src/stage/controller/editor-item-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from 'tapestry-core-client/src/view-model'
import { getSelectionItems, isItemInSelection } from 'tapestry-core-client/src/view-model/utils'
import { Id } from 'tapestry-core/src/data-format/schemas/common'
import { Point, Rectangle, translate, vector } from 'tapestry-core/src/lib/geometry'
import { translate, vector } from 'tapestry-core/src/lib/geometry'
import { router } from '../../main'
import { InteractionMode, TapestryEditorStore } from '../../pages/tapestry/view-model'
import {
Expand Down Expand Up @@ -61,12 +61,6 @@ const { eventListener, attachListeners, detachListeners } = createEventRegistry<
InteractionMode | 'desktop' | 'mobile'
>()

function isDraggingItems(
event: DragEvent<HoveredDragTarget> | DragEndEvent<HoveredDragTarget>,
): event is DragEvent<HoveredDragTarget> | DragEndEvent<HoveredDragTarget> {
return isHoveredDragTarget(event.detail.dragTarget)
}

function getDraggedItem(event: DragEvent<HoveredDragTarget> | DragStartEvent<HoveredDragTarget>) {
const { dragTarget: draggedElement } = event.detail

Expand Down Expand Up @@ -302,9 +296,7 @@ export class EditorItemController extends ItemController {
protected onDragEnd(e: DragEndEvent<HoveredDragTarget | ResizeTarget>) {
this.stage.gestureDetector.activate()
this.editorStore.dispatch(
isDraggingItems(e)
? setPointerInteraction('hover', e.detail.dragTarget)
: setPointerInteraction(null),
setPointerInteraction('hover', e.detail.dragTarget),
setSelectionRect(null),
updateSelectionItems({ dragState: null }),
(model) => {
Expand All @@ -315,28 +307,11 @@ export class EditorItemController extends ItemController {

@eventListener('dragHandler', 'drag')
protected onDrag(event: DragEvent<HoveredDragTarget>) {
if (this.editorStore.get('interactionMode') === 'edit' && isDraggingItems(event)) {
if (this.editorStore.get('interactionMode') === 'edit') {
this.onDragItems(event)
} else {
this.onDragSelectionRect(event.detail.currentPoint)
}
}

private onDragSelectionRect(cursorLocation: Point) {
const pointerSelection = this.store.get('pointerSelection')
if (!pointerSelection) return

const point = this.stage.pixi.tapestry.app.stage.worldTransform.applyInverse(cursorLocation)
this.editorStore.dispatch(
setSelectionRect(
new Rectangle(pointerSelection.rect.position, {
width: point.x - pointerSelection.rect.position.x,
height: point.y - pointerSelection.rect.position.y,
}),
),
)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this (and isDraggingItems) no longer needed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is being handled in the core-client item controller, and this function here was not being called at all. Also in the editor-item-controller the drag handler is set so that it is impossible to start a drag operation without dragging items.


private onDragItems(event: DragEvent<HoveredDragTarget>) {
const { worldTransform } = this.stage.pixi.tapestry.app.stage
const guidelineSpacing = event.detail.originalEvent?.ctrlKey
Expand Down
2 changes: 2 additions & 0 deletions core-client/src/components/lib/icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const ICONS = [
'fast_rewind',
'feature_search',
'file_copy',
'flip_to_back',
'flip_to_front',
'format_align_center',
'format_align_left',
'format_align_right',
Expand Down
13 changes: 3 additions & 10 deletions core-client/src/components/tapestry/tapestry-canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ import { cssTransformForLocation } from '../../../stage/utils'
import { ItemType } from 'tapestry-core/src/data-format/schemas/item'
import { isMac, isMobile } from '../../../lib/user-agent'

export interface TapestryCanvasProps extends PropsWithStyle<
object,
'root' | 'itemLocator' | 'relLocator'
> {
orderByPosition?: boolean
}
export type TapestryCanvasProps = PropsWithStyle<object, 'root' | 'itemLocator' | 'relLocator'>

interface TapestryElementLocatorProps extends PropsWithStyle {
id: string
Expand Down Expand Up @@ -124,7 +119,7 @@ function getRelBounds(rel: Rel, items: IdMap<ItemViewModel>) {
return getBounds(rel, { [fromItem.dto.id]: fromItem, [toItem.dto.id]: toItem })
}

export function TapestryCanvas({ classes, style, orderByPosition }: TapestryCanvasProps) {
export function TapestryCanvas({ classes, style }: TapestryCanvasProps) {
const { useStoreData, components } = useTapestryConfig()
const transform = useStoreData('viewport.transform', ['translation', 'scale'])
const viewportReady = useStoreData('viewport.ready')
Expand All @@ -140,9 +135,7 @@ export function TapestryCanvas({ classes, style, orderByPosition }: TapestryCanv
}

const itemsArray = idMapToArray(items)
const orderedItems = orderByPosition
? orderBy(itemsArray, ['dto.position.y', 'dto.position.x'])
: itemsArray
const orderedItems = orderBy(itemsArray, 'dto.layer')

function renderItem(item: ItemViewModel) {
let component: TapestryElementComponent
Expand Down
9 changes: 8 additions & 1 deletion core-client/src/stage/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ export abstract class TapestryRenderer<
const interactiveElement = this.store.get('interactiveElement')
this.getGroups().forEach((group) => this.renderViewModel(group, selection, interactiveElement))
this.getRels().forEach((rel) => this.renderViewModel(rel, selection, interactiveElement))
this.getItems().forEach((item) => this.renderViewModel(item, selection, interactiveElement))
this.getItems().forEach((item) =>
this.renderViewModel(item, selection, interactiveElement, item.dto.layer),
)

this.renderSelectionRect()

Expand Down Expand Up @@ -179,6 +181,7 @@ export abstract class TapestryRenderer<
viewModel?: E | null,
selection?: Selection,
interactiveElement?: TapestryElementRef | null,
zIndex?: number,
) {
if (!viewModel) {
return
Expand All @@ -192,6 +195,10 @@ export abstract class TapestryRenderer<
this.tapestryElementRenderers.set(id, renderer)
}

if (zIndex) {
renderer.pixiContainer.zIndex = zIndex
}

const isSelected = this.isSelected(viewModel, selection, interactiveElement)

const container = isSelected ? this.selected : this.world
Expand Down
28 changes: 14 additions & 14 deletions core/src/data-format/export/v4/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,51 +25,51 @@ const ItemNotesSchema = z.object({ notes: z.string().nullish() })
const NullishTitleSchema = z.object({ title: z.string().nullish() })
const CustomThumbnailSchema = z.object({ customThumbnail: z.string().nullish() })

export const TextItemSchema = z.object({
export const TextItemSchemaV4 = z.object({
...TextItemSchemaV3.shape,
...ItemGroupIdSchema.shape,
...ItemNotesSchema.shape,
...NullishTitleSchema.shape,
...CustomThumbnailSchema.shape,
})
export const AudioItemSchema = z.object({
export const AudioItemSchemaV4 = z.object({
...AudioItemSchemaV3.shape,
...ItemGroupIdSchema.shape,
...ItemNotesSchema.shape,
...NullishTitleSchema.shape,
...CustomThumbnailSchema.shape,
})
export const BookItemSchema = z.object({
export const BookItemSchemaV4 = z.object({
...BookItemSchemaV3.shape,
...ItemGroupIdSchema.shape,
...ItemNotesSchema.shape,
...NullishTitleSchema.shape,
...CustomThumbnailSchema.shape,
})
export const ImageItemSchema = z.object({
export const ImageItemSchemaV4 = z.object({
...ImageItemSchemaV3.shape,
...ItemGroupIdSchema.shape,
...ItemNotesSchema.shape,
...NullishTitleSchema.shape,
...CustomThumbnailSchema.shape,
})
export const PDFItemSchema = z.object({
export const PDFItemSchemaV4 = z.object({
...PDFItemSchemaV3.shape,
...ItemGroupIdSchema.shape,
...ItemNotesSchema.shape,
...NullishTitleSchema.shape,
...CustomThumbnailSchema.shape,
defaultPage: z.int().nullish(),
})
export const VideoItemSchema = z.object({
export const VideoItemSchemaV4 = z.object({
...VideoItemSchemaV3.shape,
...ItemGroupIdSchema.shape,
...ItemNotesSchema.shape,
...NullishTitleSchema.shape,
thumbnail: ThumbnailSchema.nullish(),
...CustomThumbnailSchema.shape,
})
export const WebpageItemSchema = z.object({
export const WebpageItemSchemaV4 = z.object({
...WebpageItemSchemaV3.shape,
...ItemGroupIdSchema.shape,
...ItemNotesSchema.shape,
Expand All @@ -80,15 +80,15 @@ export const WebpageItemSchema = z.object({
})

export const MediaItemSchema = z.discriminatedUnion('type', [
AudioItemSchema,
BookItemSchema,
ImageItemSchema,
PDFItemSchema,
VideoItemSchema,
WebpageItemSchema,
AudioItemSchemaV4,
BookItemSchemaV4,
ImageItemSchemaV4,
PDFItemSchemaV4,
VideoItemSchemaV4,
WebpageItemSchemaV4,
])

const ItemSchema = z.discriminatedUnion('type', [...MediaItemSchema.options, TextItemSchema])
const ItemSchema = z.discriminatedUnion('type', [...MediaItemSchema.options, TextItemSchemaV4])

export const RelSchema = z.object({
...RelSchemaV3.shape,
Expand Down
Loading