threepipe
Version:
A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.
117 lines (98 loc) • 4.41 kB
text/typescript
import {filterTopmostAncestors, IObject3D, incrementObjectCloneName} from '../../core'
export type ClipboardMode = 'copy' | 'cut'
export interface ClipboardState {
objects: IObject3D[]
mode: ClipboardMode
sourceParents: Map<IObject3D, IObject3D | null>
}
/**
* Internal object clipboard for copy/cut/paste.
* - copy/cut: store references (no scene mutation)
* - paste: clone or move objects directly to the destination parent (one step, no reparenting)
* - Returns {objects, undo, redo} from paste — caller records undo and handles selection.
*/
export class ObjectClipboard {
private _state: ClipboardState | null = null
get state(): ClipboardState | null { return this._state }
get isEmpty(): boolean { return !this._state }
copy(objects: IObject3D[]): void {
const filtered = filterTopmostAncestors(objects)
if (!filtered.length) return
this._state = {objects: filtered, mode: 'copy', sourceParents: this._parentMap(filtered)}
}
cut(objects: IObject3D[]): void {
const filtered = filterTopmostAncestors(objects)
if (!filtered.length) return
this._state = {objects: filtered, mode: 'cut', sourceParents: this._parentMap(filtered)}
}
/**
* Paste from clipboard. Clones (copy) or moves (cut) objects directly to destination.
* @param destination - target parent, or null to use source parents
* @returns pasted objects + undo/redo, or null if clipboard is empty
*/
paste(destination?: IObject3D | null): {objects: IObject3D[], undo: () => void, redo: () => void} | null {
if (!this._state) return null
return this._state.mode === 'copy' ? this._pasteFromCopy(destination) : this._pasteFromCut(destination)
}
clear(): void { this._state = null }
private _pasteFromCopy(destination?: IObject3D | null): {objects: IObject3D[], undo: () => void, redo: () => void} | null {
const clipboard = this._state
if (!clipboard) return null
const clones: IObject3D[] = []
const cloneParents = new Map<IObject3D, IObject3D>()
for (const obj of clipboard.objects) {
const clone = obj.clone(true) as IObject3D
incrementObjectCloneName(obj, clone)
const parent = destination ?? obj.parent
if (parent) {
parent.add(clone)
cloneParents.set(clone, parent)
}
clones.push(clone)
}
return {
objects: clones,
undo: () => { for (const c of clones) c.removeFromParent() },
redo: () => { for (const c of clones) { const p = cloneParents.get(c); if (p && !c.parent) p.add(c) } },
}
}
private _pasteFromCut(destination?: IObject3D | null): {objects: IObject3D[], undo: () => void, redo: () => void} | null {
const clipboard = this._state
if (!clipboard) return null
const objects = clipboard.objects
const sourceParents = clipboard.sourceParents
// Move directly to destination (or back to source parents)
for (const obj of objects) {
const target = (destination ?? sourceParents.get(obj)) as IObject3D | null
if (target && obj.parent !== target) {
obj.removeFromParent()
target.add(obj)
}
}
// Consume clipboard
this._state = null
return {
objects,
undo: () => {
// Move back to source parents
for (const obj of objects) {
const parent = sourceParents.get(obj)
if (parent && obj.parent !== parent) { obj.removeFromParent(); parent.add(obj) }
}
this._state = clipboard
},
redo: () => {
for (const obj of objects) {
const target = (destination ?? sourceParents.get(obj)) as IObject3D | null
if (target && obj.parent !== target) { obj.removeFromParent(); target.add(obj) }
}
this._state = null
},
}
}
private _parentMap(objects: IObject3D[]): Map<IObject3D, IObject3D | null> {
const map = new Map<IObject3D, IObject3D | null>()
for (const obj of objects) map.set(obj, obj.parent as IObject3D | null)
return map
}
}