diff --git a/src/core/selection.ts b/src/core/selection.ts index 17a9a723..d1e94f68 100644 --- a/src/core/selection.ts +++ b/src/core/selection.ts @@ -16,6 +16,30 @@ export type EditorSelection = | { kind: "entities"; ids: string[] } | { kind: "modelInstances"; ids: string[] }; +export type EditorSelectionWithIds = Extract; +export type SameKindMultiSelectableEditorSelection = Extract< + EditorSelection, + | { kind: "brushes"; ids: string[] } + | { kind: "entities"; ids: string[] } + | { kind: "modelInstances"; ids: string[] } +>; + +export function isEditorSelectionWithIds( + selection: EditorSelection +): selection is EditorSelectionWithIds { + return "ids" in selection; +} + +export function isSameKindMultiSelectableSelection( + selection: EditorSelection +): selection is SameKindMultiSelectableEditorSelection { + return ( + selection.kind === "brushes" || + selection.kind === "entities" || + selection.kind === "modelInstances" + ); +} + export function cloneEditorSelection(selection: EditorSelection): EditorSelection { if (selection.kind === "none") { return { @@ -85,6 +109,104 @@ export function areEditorSelectionsEqual(left: EditorSelection, right: EditorSel } } +export function getSelectionDefaultActiveId(selection: EditorSelection): string | null { + switch (selection.kind) { + case "none": + return null; + case "brushFace": + case "brushEdge": + case "brushVertex": + return selection.brushId; + case "pathPoint": + return selection.pointId; + case "brushes": + case "paths": + case "entities": + case "modelInstances": + return selection.ids.at(-1) ?? null; + } +} + +export function resolveSelectionActiveId( + selection: EditorSelection, + activeSelectionId: string | null +): string | null { + if (activeSelectionId === null) { + return getSelectionDefaultActiveId(selection); + } + + switch (selection.kind) { + case "none": + return null; + case "brushFace": + case "brushEdge": + case "brushVertex": + return selection.brushId === activeSelectionId + ? activeSelectionId + : selection.brushId; + case "pathPoint": + return selection.pointId === activeSelectionId + ? activeSelectionId + : selection.pointId; + case "brushes": + case "paths": + case "entities": + case "modelInstances": + return selection.ids.includes(activeSelectionId) + ? activeSelectionId + : getSelectionDefaultActiveId(selection); + } +} + +export function isSelectionActiveId( + selection: EditorSelection, + activeSelectionId: string | null, + id: string +): boolean { + return resolveSelectionActiveId(selection, activeSelectionId) === id; +} + +export function applySameKindSelectionClick( + currentSelection: EditorSelection, + clickedSelection: EditorSelection, + shiftKey: boolean +): EditorSelection { + if (!shiftKey || !isSameKindMultiSelectableSelection(clickedSelection)) { + return cloneEditorSelection(clickedSelection); + } + + if ( + !isSameKindMultiSelectableSelection(currentSelection) || + currentSelection.kind !== clickedSelection.kind + ) { + return cloneEditorSelection(clickedSelection); + } + + if (clickedSelection.ids.length !== 1) { + return cloneEditorSelection(clickedSelection); + } + + const clickedId = clickedSelection.ids[0]; + + if (!currentSelection.ids.includes(clickedId)) { + return { + kind: currentSelection.kind, + ids: [...currentSelection.ids, clickedId] + }; + } + + const remainingIds = currentSelection.ids.filter((id) => id !== clickedId); + + return remainingIds.length === 0 + ? { + kind: "none" + } + : { + kind: currentSelection.kind, + ids: remainingIds + }; +} + export function getSingleSelectedBrushId(selection: EditorSelection): string | null { if (selection.kind === "brushFace" || selection.kind === "brushEdge" || selection.kind === "brushVertex") { return selection.brushId;