From 93c55899ecd1d8c6fde21f42ca833616b6428dd8 Mon Sep 17 00:00:00 2001 From: Nettika Date: Fri, 30 Jan 2026 00:03:57 -0800 Subject: [PATCH] Implement translate duplication --- TODO.md | 4 ++-- src/components/Viewport.vue | 21 +++++++++++++++++++++ src/stores/scene.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 5e0ee7805..016ad2fc1 100644 --- a/TODO.md +++ b/TODO.md @@ -145,8 +145,8 @@ A step-by-step checklist for porting MatterControl's design features to a Vue + - [x] Left-click object: center/zoom camera on object ### Shift Modifier Behaviors -- [ ] Click with Shift: add to selection (already in Phase 4) -- [ ] Drag translate handle with Shift: duplicate selection along axis +- [x] Click with Shift: add to selection (already in Phase 4) +- [x] Drag translate handle with Shift: duplicate selection along axis --- diff --git a/src/components/Viewport.vue b/src/components/Viewport.vue index bda3bc1e4..ede8c80ed 100644 --- a/src/components/Viewport.vue +++ b/src/components/Viewport.vue @@ -951,6 +951,27 @@ function onCanvasMouseDown(event: MouseEvent) { dragState.isRightDrag = event.button === 2 || event.button === 1 dragState.startMousePosition.copy(mouse) + // Shift+drag: duplicate selection first, then move duplicates + if (isShiftPressed) { + const currentSelection = [...sceneStore.selectedObjects] + const duplicatedIds: string[] = [] + + for (const obj of currentSelection) { + const duplicate = sceneStore.duplicateObject(obj.id) + if (duplicate) { + duplicatedIds.push(duplicate.id) + } + } + + // Select the duplicates (originals stay in place) + if (duplicatedIds.length > 0) { + sceneStore.clearSelection() + for (const id of duplicatedIds) { + sceneStore.addToSelection(id) + } + } + } + // Store original positions and calculate center const selectedMeshes = sceneStore.selectedObjects.map((o) => o.mesh) if (selectedMeshes.length > 0) { diff --git a/src/stores/scene.ts b/src/stores/scene.ts index 22e94fd24..0b147f5f6 100644 --- a/src/stores/scene.ts +++ b/src/stores/scene.ts @@ -138,6 +138,35 @@ export const useSceneStore = defineStore('scene', () => { selectedIds.value = new Set() } + function duplicateObject(id: string): SceneObject | null { + const original = objects.value.find((o) => o.id === id) + if (!original || !threeScene) return null + + // Clone the mesh with geometry and material + const clonedMesh = original.mesh.clone() + clonedMesh.geometry = original.mesh.geometry.clone() + if (original.mesh.material instanceof THREE.Material) { + clonedMesh.material = original.mesh.material.clone() + } + + // Generate a new name based on the original + const baseName = original.name.replace(/ \d+$/, '') // Remove trailing number if present + const objectName = generateName(baseName) + + const sceneObject: SceneObject = { + id: generateId(), + name: objectName, + mesh: clonedMesh, + visible: original.visible, + locked: false, + } + + threeScene.add(clonedMesh) + objects.value = [...objects.value, sceneObject] + + return sceneObject + } + const objectList = computed(() => objects.value) const objectCount = computed(() => objects.value.length) const selectedObjects = computed(() => objects.value.filter((o) => selectedIds.value.has(o.id))) @@ -164,5 +193,6 @@ export const useSceneStore = defineStore('scene', () => { toggleSelection, selectAll, clearSelection, + duplicateObject, } })