392 lines
10 KiB
TypeScript
392 lines
10 KiB
TypeScript
/**
|
|
* Batch Operations Manager
|
|
* Perform operations on multiple objects efficiently
|
|
*/
|
|
|
|
import * as THREE from 'three'
|
|
import { MeshEditor } from './mesh-editing'
|
|
import { MaterialManager } from './materials'
|
|
|
|
export interface BatchOperation {
|
|
type: 'transform' | 'material' | 'property' | 'geometry'
|
|
operation: string
|
|
params: any
|
|
}
|
|
|
|
/**
|
|
* Batch Operations Manager
|
|
*/
|
|
export class BatchOperationsManager {
|
|
/**
|
|
* Apply transformation to multiple objects
|
|
*/
|
|
static batchTransform(
|
|
objects: THREE.Object3D[],
|
|
transform: {
|
|
position?: THREE.Vector3
|
|
rotation?: THREE.Euler
|
|
scale?: THREE.Vector3
|
|
pivot?: THREE.Vector3
|
|
}
|
|
): void {
|
|
const pivot = transform.pivot || new THREE.Vector3()
|
|
|
|
objects.forEach((obj) => {
|
|
if (transform.position) {
|
|
obj.position.add(transform.position)
|
|
}
|
|
|
|
if (transform.rotation) {
|
|
// Rotate around pivot
|
|
const offset = obj.position.clone().sub(pivot)
|
|
offset.applyEuler(transform.rotation)
|
|
obj.position.copy(pivot).add(offset)
|
|
obj.rotation.x += transform.rotation.x
|
|
obj.rotation.y += transform.rotation.y
|
|
obj.rotation.z += transform.rotation.z
|
|
}
|
|
|
|
if (transform.scale) {
|
|
obj.scale.multiply(transform.scale)
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Apply material to multiple objects
|
|
*/
|
|
static batchApplyMaterial(
|
|
objects: THREE.Mesh[],
|
|
material: THREE.Material | ((obj: THREE.Mesh) => THREE.Material)
|
|
): void {
|
|
objects.forEach((mesh) => {
|
|
if (typeof material === 'function') {
|
|
mesh.material = material(mesh)
|
|
} else {
|
|
mesh.material = material
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Set property on multiple objects
|
|
*/
|
|
static batchSetProperty(
|
|
objects: THREE.Object3D[],
|
|
property: string,
|
|
value: any
|
|
): void {
|
|
objects.forEach((obj) => {
|
|
if (property in obj) {
|
|
(obj as any)[property] = value
|
|
} else if (obj.userData) {
|
|
obj.userData[property] = value
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Apply geometry operation to multiple meshes
|
|
*/
|
|
static batchGeometryOperation(
|
|
objects: THREE.Mesh[],
|
|
operation: 'smooth' | 'subdivide' | 'optimize',
|
|
params?: any
|
|
): void {
|
|
objects.forEach((mesh) => {
|
|
switch (operation) {
|
|
case 'smooth':
|
|
mesh.geometry = MeshEditor.smoothGeometry(
|
|
mesh.geometry,
|
|
params?.iterations || 1,
|
|
params?.strength || 0.5
|
|
)
|
|
break
|
|
|
|
case 'subdivide':
|
|
mesh.geometry = MeshEditor.subdivide(
|
|
mesh.geometry,
|
|
params?.iterations || 1
|
|
)
|
|
break
|
|
|
|
case 'optimize':
|
|
mesh.geometry = MeshEditor.optimize(mesh.geometry)
|
|
break
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Randomize positions
|
|
*/
|
|
static randomizePositions(
|
|
objects: THREE.Object3D[],
|
|
range: { x: number; y: number; z: number }
|
|
): void {
|
|
objects.forEach((obj) => {
|
|
obj.position.x += (Math.random() - 0.5) * range.x
|
|
obj.position.y += (Math.random() - 0.5) * range.y
|
|
obj.position.z += (Math.random() - 0.5) * range.z
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Randomize rotations
|
|
*/
|
|
static randomizeRotations(
|
|
objects: THREE.Object3D[],
|
|
axis: 'x' | 'y' | 'z' | 'all' = 'y',
|
|
range: number = Math.PI * 2
|
|
): void {
|
|
objects.forEach((obj) => {
|
|
const randomRotation = (Math.random() - 0.5) * range
|
|
|
|
switch (axis) {
|
|
case 'x':
|
|
obj.rotation.x += randomRotation
|
|
break
|
|
case 'y':
|
|
obj.rotation.y += randomRotation
|
|
break
|
|
case 'z':
|
|
obj.rotation.z += randomRotation
|
|
break
|
|
case 'all':
|
|
obj.rotation.x += (Math.random() - 0.5) * range
|
|
obj.rotation.y += (Math.random() - 0.5) * range
|
|
obj.rotation.z += (Math.random() - 0.5) * range
|
|
break
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Array clone (create grid/circular patterns)
|
|
*/
|
|
static arrayClone(
|
|
object: THREE.Object3D,
|
|
count: number,
|
|
pattern: 'linear' | 'grid' | 'circular' | 'spiral',
|
|
spacing: number | { x: number; y: number; z: number }
|
|
): THREE.Object3D[] {
|
|
const clones: THREE.Object3D[] = []
|
|
|
|
for (let i = 1; i <= count; i++) {
|
|
const clone = object.clone()
|
|
|
|
switch (pattern) {
|
|
case 'linear':
|
|
const offset = typeof spacing === 'number' ? spacing : spacing.x
|
|
clone.position.x = object.position.x + i * offset
|
|
break
|
|
|
|
case 'grid':
|
|
const gridSpacing = typeof spacing === 'number' ? spacing : spacing.x
|
|
const gridSize = Math.ceil(Math.sqrt(count))
|
|
const row = Math.floor(i / gridSize)
|
|
const col = i % gridSize
|
|
clone.position.x = object.position.x + col * gridSpacing
|
|
clone.position.z = object.position.z + row * gridSpacing
|
|
break
|
|
|
|
case 'circular':
|
|
const radius = typeof spacing === 'number' ? spacing : spacing.x
|
|
const angle = (i / count) * Math.PI * 2
|
|
clone.position.x = object.position.x + Math.cos(angle) * radius
|
|
clone.position.z = object.position.z + Math.sin(angle) * radius
|
|
clone.rotation.y = angle + Math.PI / 2
|
|
break
|
|
|
|
case 'spiral':
|
|
const spiralRadius = typeof spacing === 'number' ? spacing : spacing.x
|
|
const spiralAngle = (i / count) * Math.PI * 4 // 2 rotations
|
|
const spiralR = spiralRadius * (i / count)
|
|
clone.position.x = object.position.x + Math.cos(spiralAngle) * spiralR
|
|
clone.position.z = object.position.z + Math.sin(spiralAngle) * spiralR
|
|
break
|
|
}
|
|
|
|
clones.push(clone)
|
|
}
|
|
|
|
return clones
|
|
}
|
|
|
|
/**
|
|
* Merge multiple meshes into one
|
|
*/
|
|
static mergeMeshes(meshes: THREE.Mesh[]): THREE.Mesh | null {
|
|
if (meshes.length === 0) return null
|
|
|
|
const geometries = meshes.map(mesh => {
|
|
const geo = mesh.geometry.clone()
|
|
geo.applyMatrix4(mesh.matrixWorld)
|
|
return geo
|
|
})
|
|
|
|
const mergedGeometry = MeshEditor.mergeGeometries(geometries)
|
|
const mergedMesh = new THREE.Mesh(mergedGeometry, meshes[0].material)
|
|
|
|
// Cleanup
|
|
geometries.forEach(geo => geo.dispose())
|
|
|
|
return mergedMesh
|
|
}
|
|
|
|
/**
|
|
* Separate mesh by material
|
|
*/
|
|
static separateByMaterial(mesh: THREE.Mesh): THREE.Mesh[] {
|
|
if (!Array.isArray(mesh.material)) {
|
|
return [mesh]
|
|
}
|
|
|
|
const separatedMeshes: THREE.Mesh[] = []
|
|
|
|
mesh.material.forEach((mat, index) => {
|
|
const newGeometry = new THREE.BufferGeometry()
|
|
// Extract faces with this material
|
|
// Complex implementation would go here
|
|
|
|
const newMesh = new THREE.Mesh(newGeometry, mat)
|
|
newMesh.position.copy(mesh.position)
|
|
newMesh.rotation.copy(mesh.rotation)
|
|
newMesh.scale.copy(mesh.scale)
|
|
|
|
separatedMeshes.push(newMesh)
|
|
})
|
|
|
|
return separatedMeshes
|
|
}
|
|
|
|
/**
|
|
* Center objects at origin
|
|
*/
|
|
static centerObjects(objects: THREE.Object3D[]): void {
|
|
if (objects.length === 0) return
|
|
|
|
// Calculate bounding box
|
|
const box = new THREE.Box3()
|
|
objects.forEach(obj => {
|
|
box.expandByObject(obj)
|
|
})
|
|
|
|
const center = new THREE.Vector3()
|
|
box.getCenter(center)
|
|
|
|
// Move all objects
|
|
objects.forEach(obj => {
|
|
obj.position.sub(center)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Fit objects in bounds
|
|
*/
|
|
static fitInBounds(
|
|
objects: THREE.Object3D[],
|
|
bounds: THREE.Box3
|
|
): void {
|
|
if (objects.length === 0) return
|
|
|
|
// Calculate current bounds
|
|
const currentBounds = new THREE.Box3()
|
|
objects.forEach(obj => currentBounds.expandByObject(obj))
|
|
|
|
const currentSize = new THREE.Vector3()
|
|
const targetSize = new THREE.Vector3()
|
|
currentBounds.getSize(currentSize)
|
|
bounds.getSize(targetSize)
|
|
|
|
// Calculate scale factor
|
|
const scale = Math.min(
|
|
targetSize.x / currentSize.x,
|
|
targetSize.y / currentSize.y,
|
|
targetSize.z / currentSize.z
|
|
)
|
|
|
|
// Apply scale
|
|
const currentCenter = new THREE.Vector3()
|
|
currentBounds.getCenter(currentCenter)
|
|
|
|
objects.forEach(obj => {
|
|
const offset = obj.position.clone().sub(currentCenter)
|
|
offset.multiplyScalar(scale)
|
|
obj.position.copy(currentCenter).add(offset)
|
|
obj.scale.multiplyScalar(scale)
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Advanced Export System
|
|
*/
|
|
export class AdvancedExporter {
|
|
/**
|
|
* Export scene to GLTF
|
|
*/
|
|
static async exportGLTF(scene: THREE.Scene): Promise<Blob> {
|
|
const { GLTFExporter } = await import('three/examples/jsm/exporters/GLTFExporter.js')
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const exporter = new GLTFExporter()
|
|
|
|
exporter.parse(
|
|
scene,
|
|
(result) => {
|
|
const output = JSON.stringify(result, null, 2)
|
|
const blob = new Blob([output], { type: 'application/json' })
|
|
resolve(blob)
|
|
},
|
|
(error) => reject(error),
|
|
{ binary: false }
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Export scene to GLB (binary)
|
|
*/
|
|
static async exportGLB(scene: THREE.Scene): Promise<Blob> {
|
|
const { GLTFExporter } = await import('three/examples/jsm/exporters/GLTFExporter.js')
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const exporter = new GLTFExporter()
|
|
|
|
exporter.parse(
|
|
scene,
|
|
(result) => {
|
|
const blob = new Blob([result as ArrayBuffer], { type: 'application/octet-stream' })
|
|
resolve(blob)
|
|
},
|
|
(error) => reject(error),
|
|
{ binary: true }
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Export to OBJ
|
|
*/
|
|
static async exportOBJ(scene: THREE.Scene): Promise<string> {
|
|
const { OBJExporter } = await import('three/examples/jsm/exporters/OBJExporter.js')
|
|
|
|
const exporter = new OBJExporter()
|
|
return exporter.parse(scene)
|
|
}
|
|
|
|
/**
|
|
* Export screenshot
|
|
*/
|
|
static exportScreenshot(
|
|
renderer: THREE.WebGLRenderer,
|
|
width: number = 1920,
|
|
height: number = 1080,
|
|
format: 'png' | 'jpg' = 'png'
|
|
): string {
|
|
return renderer.domElement.toDataURL(`image/${format}`)
|
|
}
|
|
}
|
|
|