Files
fabrikanabytok/apps/fabrikanabytok/lib/three/batch-operations.ts
2025-11-28 20:48:15 +01:00

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}`)
}
}