diff --git a/packages/chili-core/src/visual/cameraController.ts b/packages/chili-core/src/visual/cameraController.ts index 1f12cfb4..f94168b9 100644 --- a/packages/chili-core/src/visual/cameraController.ts +++ b/packages/chili-core/src/visual/cameraController.ts @@ -3,6 +3,7 @@ import { XYZ } from "../math"; export interface ICameraController { + cameraType: "perspective" | "orthographic"; fitContent(): void; lookAt(eye: XYZ, target: XYZ): void; pan(dx: number, dy: number): void; diff --git a/packages/chili-three/src/cameraController.ts b/packages/chili-three/src/cameraController.ts index ea983673..f09532bc 100644 --- a/packages/chili-three/src/cameraController.ts +++ b/packages/chili-three/src/cameraController.ts @@ -17,39 +17,82 @@ import { ThreeView } from "./threeView"; import { ThreeVisualContext } from "./threeVisualContext"; export class CameraController implements ICameraController { - #minZoom: number; - private readonly initialLength: number; - private _target: Vector3 = new Vector3(); - private get camera(): OrthographicCamera | PerspectiveCamera { - return this.view.camera; - } - private rotateStart: Vector3 | undefined; - - zoomSpeed: number = 0.9; + zoomSpeed: number = 0.1; rotateSpeed: number = 0.01; + #target: Vector3 = new Vector3(); + #rotateStart: Vector3 | undefined; + readonly #perspectiveCamera: PerspectiveCamera; + readonly #orthographic: OrthographicCamera; + + #cameraType: "perspective" | "orthographic" = "perspective"; + get cameraType(): "perspective" | "orthographic" { + return this.#cameraType; + } + set cameraType(value: "perspective" | "orthographic") { + if (this.#cameraType === value) { + return; + } + this.#cameraType = value; + if (value === "perspective") { + this.updateCamera(this.#orthographic.position, this.#target, false); + } else { + this.updateCamera(this.#perspectiveCamera.position, this.#target, false); + } + } get scale() { - return this.camera.position.distanceTo(this._target) / this.initialLength; + return this.#orthographic.zoom; + } + + get camera(): PerspectiveCamera | OrthographicCamera { + return this.cameraType === "perspective" ? this.#perspectiveCamera : this.#orthographic; } constructor(readonly view: ThreeView) { - this.initialLength = this.camera.position.length(); - this.#minZoom = this.initialLength * 0.01; + this.#perspectiveCamera = this.initPerspectiveCamera(view.container); + this.#orthographic = this.initOrthographicCamera(view.container); + } + + private initPerspectiveCamera(container: HTMLElement) { + let k = container.clientWidth / container.clientHeight; + let camera = new PerspectiveCamera(45, k, 0.001, 1e12); + this.initCamera(camera); + return camera; + } + + private initOrthographicCamera(container: HTMLElement) { + let camera = new OrthographicCamera( + -container.clientWidth * 0.5, + container.clientWidth * 0.5, + container.clientHeight * 0.5, + -container.clientHeight * 0.5, + 1, + 1e12, + ); + this.initCamera(camera); + return camera; + } + + private initCamera(camera: PerspectiveCamera | OrthographicCamera) { + camera.position.set(1500, 1500, 1500); + camera.lookAt(new Vector3()); + camera.updateMatrixWorld(true); + return camera; } pan(dx: number, dy: number): void { - let { x, y } = this.view.worldToScreen(ThreeHelper.toXYZ(this._target)); - let plane = this.cameraPlane(this._target); + let { x, y } = this.view.worldToScreen(ThreeHelper.toXYZ(this.#target)); + let plane = this.cameraPlane(this.#target); let p1 = this.planeIntersectCameraLineByMouse(x, y, plane); let p2 = this.planeIntersectCameraLineByMouse(x - dx, y - dy, plane); let vec = p2.sub(p1); - this.updateCamera(this.camera.position.add(vec), this._target.add(vec)); + this.updateCamera(this.camera.position.add(vec), this.#target.add(vec)); } private updateCamera(position: Vector3, target: Vector3, setTarget: boolean = true) { - if (setTarget) this._target.copy(target); + if (setTarget) this.#target.copy(target); this.camera.position.copy(position); - this.camera.lookAt(this._target); + this.camera.lookAt(this.#target); this.camera.updateProjectionMatrix(); } @@ -74,10 +117,8 @@ export class CameraController implements ICameraController { } else { this.camera.getWorldDirection(vec); } - return new Line3( - position.clone().add(vec.multiplyScalar(1e19)), - position.clone().sub(vec.multiplyScalar(1e19)), - ); + vec.multiplyScalar(1e19); + return new Line3(position, position.clone().sub(vec)); } private cameraPlane(position: Vector3) { @@ -88,20 +129,19 @@ export class CameraController implements ICameraController { } startRotate(x: number, y: number): void { - // TODO let shape = this.view.detected(ShapeType.Shape, x, y).at(0)?.owner; if (shape instanceof ThreeShape) { - this.rotateStart = new Vector3(); + this.#rotateStart = new Vector3(); let box = new Box3(); box.setFromObject(shape); - box.getCenter(this.rotateStart); + box.getCenter(this.#rotateStart); } else { - this.rotateStart = undefined; + this.#rotateStart = undefined; } } rotate(dx: number, dy: number): void { - let start = this.rotateStart ?? this._target; + let start = this.#rotateStart ?? this.#target; let vecPos = this.camera.position.clone().sub(start); let xvec = this.camera.up.clone().cross(vecPos).normalize(); let yvec = vecPos.clone().cross(xvec).normalize(); @@ -109,32 +149,33 @@ export class CameraController implements ICameraController { let matrixY = new Matrix4().makeRotationAxis(yvec, -dx * this.rotateSpeed); let matrix = new Matrix4().multiplyMatrices(matrixY, matrixX); let position = ThreeHelper.transformVector(matrix, vecPos).add(start); - if (this.rotateStart) { - let vecTrt = this._target.clone().sub(this.camera.position); - this._target = ThreeHelper.transformVector(matrix, vecTrt).add(position); + if (this.#rotateStart) { + let vecTrt = this.#target.clone().sub(this.camera.position); + this.#target = ThreeHelper.transformVector(matrix, vecTrt).add(position); } this.camera.up.copy(this.camera.up.transformDirection(matrix)); - this.updateCamera(position, this._target, false); + this.updateCamera(position, this.#target, false); } fitContent(): void { let context = this.view.viewer.visual.context as ThreeVisualContext; let vectors = ThreeHelper.cameraVectors(this.camera); let rect = this.cameraRect(context.visualShapes, vectors.right, vectors.up); - if (this.camera instanceof PerspectiveCamera) { - let h = Math.max(rect.width / this.camera.aspect, rect.height); - let distance = (0.5 * h) / Math.tan((this.camera.fov * Math.PI) / 180 / 2.0); - let position = rect.center.clone().sub(vectors.direction.clone().multiplyScalar(distance)); - this.updateCamera(position, rect.center); - } + if (!rect.width || !rect.height) return; + let h = Math.max(rect.width / this.#perspectiveCamera.aspect, rect.height); + let distance = (0.5 * h) / Math.tan((this.#perspectiveCamera.fov * Math.PI) / 180 / 2.0); + let position = rect.center.clone().sub(vectors.direction.clone().multiplyScalar(distance)); + this.#orthographic.zoom = Math.min( + this.view.container.clientWidth / rect.width, + this.view.container.clientHeight / rect.height, + ); + this.updateCamera(position, rect.center); } private cameraRect(object: Object3D, right: Vector3, up: Vector3) { let box = this.getSceneBox(object); - let plane = this.cameraPlane(box.center); - let points = box.points.map((point) => this.planeIntersectCameraLine(plane, point)); let [minV, maxV, minH, maxH] = [Infinity, -Infinity, Infinity, -Infinity]; - for (const point of points) { + for (const point of box.points) { let dotV = point.dot(up); let dotH = point.dot(right); minV = Math.min(minV, dotV); @@ -163,39 +204,28 @@ export class CameraController implements ICameraController { } zoom(x: number, y: number, delta: number): void { - let scale = delta > 0 ? this.zoomSpeed : 1 / this.zoomSpeed; + this.#orthographic.zoom *= delta > 0 ? 1 + this.zoomSpeed : 1 - this.zoomSpeed; let point = this.mouseToWorld(x, y); - if (ThreeHelper.isOrthographicCamera(this.camera)) { - this.zoomOrthographicCamera(point, scale); - } else if (ThreeHelper.isPerspectiveCamera(this.camera)) { - this.zoomPerspectiveCamera(point, scale); - } - } - - private zoomOrthographicCamera(point: Vector3, scale: number) { - let vec = point.clone().sub(this._target); - let xvec = new Vector3().setFromMatrixColumn(this.camera.matrix, 0); - let yvec = new Vector3().setFromMatrixColumn(this.camera.matrix, 1); - let x = vec.clone().dot(xvec); - let y = vec.clone().dot(yvec); - let vx = xvec.clone().multiplyScalar(x / scale - x); - let vy = yvec.clone().multiplyScalar(y / scale - y); - let vector = new Vector3().add(vx).add(vy); - this.camera.zoom /= scale; - this.updateCamera(this.camera.position.add(vector), this._target.add(vector)); - } - - private zoomPerspectiveCamera(point: Vector3, scale: number) { - let direction = this.camera.position.clone().sub(this._target); - let vector = this.camera.position.clone().sub(point).normalize(); - let angle = vector.angleTo(direction); - let length = direction.length() * (scale - 1); - if (Math.abs(length) < this.#minZoom) { - length = length < 0 ? -this.#minZoom : this.#minZoom; + let cameraVectors = ThreeHelper.cameraVectors(this.camera); + let scale = delta > 0 ? this.zoomSpeed : -this.zoomSpeed; + let vec: Vector3; + if (this.#cameraType === "orthographic") { + vec = point.clone().sub(this.#target); + } else { + vec = this.camera.position.clone().sub(point); + let angle = vec.angleTo(cameraVectors.direction); + let length = this.camera.position.distanceTo(this.#target); + vec.setLength(length / Math.cos(angle)); } - let moveVector = vector.clone().multiplyScalar(length / Math.cos(angle)); - this._target.add(moveVector.clone().sub(direction.clone().setLength(length))); - this.updateCamera(this.camera.position.add(moveVector), this._target, false); + let dx = vec.clone().dot(cameraVectors.right) * scale; + let dy = vec.clone().dot(cameraVectors.up) * scale; + let dz = this.camera.position.distanceTo(this.#target) * scale; + let moveVector = new Vector3() + .add(cameraVectors.right.clone().multiplyScalar(dx)) + .add(cameraVectors.up.clone().multiplyScalar(dy)); + this.#target.add(moveVector); + moveVector.add(cameraVectors.direction.clone().multiplyScalar(dz)); + this.updateCamera(this.camera.position.add(moveVector), this.#target, false); } lookAt(eye: XYZ, target: XYZ): void { diff --git a/packages/chili-three/src/threeView.ts b/packages/chili-three/src/threeView.ts index 2a8ef39b..e3413949 100644 --- a/packages/chili-three/src/threeView.ts +++ b/packages/chili-three/src/threeView.ts @@ -49,12 +49,8 @@ export class ThreeView extends Observable implements IView { this._name = name; } - private _camera: OrthographicCamera | PerspectiveCamera; get camera(): PerspectiveCamera | OrthographicCamera { - return this._camera; - } - set camera(camera: PerspectiveCamera | OrthographicCamera) { - this._camera = camera; + return this.cameraController.camera; } constructor( @@ -68,9 +64,8 @@ export class ThreeView extends Observable implements IView { this._name = name; this._scene = content.scene; this._workplane = workplane; - this._camera = this.initCamera(container); - this._renderer = this.initRender(container); this.cameraController = new CameraController(this); + this._renderer = this.initRender(container); this.animate(); } @@ -78,23 +73,6 @@ export class ThreeView extends Observable implements IView { return this._renderer; } - private initCamera(container: HTMLElement) { - let [w, h] = [container.clientWidth, container.clientHeight]; - let camera = new PerspectiveCamera(30, w / h, 0.001, 1e12); - // let camera = new OrthographicCamera( - // -w / 2, - // w / 2, - // h / 2, - // -h / 2, - // 0.01, - // 1e12, - // ); - camera.position.set(1000, 1000, 1000); - camera.lookAt(new Vector3()); - camera.updateMatrixWorld(true); - return camera; - } - protected initRender(container: HTMLElement): Renderer { let renderer = new WebGLRenderer({ antialias: true, @@ -109,7 +87,7 @@ export class ThreeView extends Observable implements IView { } toImage(): string { - this._renderer.render(this._scene, this._camera); + this._renderer.render(this._scene, this.camera); return this.renderer.domElement.toDataURL(); } @@ -143,17 +121,17 @@ export class ThreeView extends Observable implements IView { this.animate(); }); if (this._needsUpdate) { - this._renderer.render(this._scene, this._camera); + this._renderer.render(this._scene, this.camera); this._needsUpdate = false; } } resize(width: number, heigth: number) { - if (this._camera instanceof PerspectiveCamera) { - this._camera.aspect = width / heigth; - this._camera.updateProjectionMatrix(); - } else if (this._camera instanceof OrthographicCamera) { - this._camera.updateProjectionMatrix(); + if (this.camera instanceof PerspectiveCamera) { + this.camera.aspect = width / heigth; + this.camera.updateProjectionMatrix(); + } else if (this.camera instanceof OrthographicCamera) { + this.camera.updateProjectionMatrix(); } this._renderer.setSize(width, heigth); this.update(); @@ -174,12 +152,12 @@ export class ThreeView extends Observable implements IView { rayAt(mx: number, my: number): Ray { let position = this.mouseToWorld(mx, my); let vec = new Vector3(); - if (this._camera instanceof PerspectiveCamera) { - vec = position.clone().sub(this._camera.position).normalize(); - } else if (this._camera instanceof OrthographicCamera) { - this._camera.getWorldDirection(vec); + if (this.camera instanceof PerspectiveCamera) { + vec = position.clone().sub(this.camera.position).normalize(); + } else if (this.camera instanceof OrthographicCamera) { + this.camera.getWorldDirection(vec); } - let offset = position.clone().sub(this._camera.position).dot(vec); + let offset = position.clone().sub(this.camera.position).dot(vec); position = position.clone().sub(vec.clone().multiplyScalar(offset)); return new Ray(ThreeHelper.toXYZ(position), ThreeHelper.toXYZ(vec)); } @@ -192,28 +170,28 @@ export class ThreeView extends Observable implements IView { worldToScreen(point: XYZ): XY { let cx = this.width / 2; let cy = this.heigth / 2; - let vec = new Vector3(point.x, point.y, point.z).project(this._camera); + let vec = new Vector3(point.x, point.y, point.z).project(this.camera); return new XY(Math.round(cx * vec.x + cx), Math.round(-cy * vec.y + cy)); } direction(): XYZ { const vec = new Vector3(); - this._camera.getWorldDirection(vec); + this.camera.getWorldDirection(vec); return ThreeHelper.toXYZ(vec); } up(): XYZ { - return ThreeHelper.toXYZ(this._camera.up); + return ThreeHelper.toXYZ(this.camera.up); } private mouseToWorld(mx: number, my: number) { let { x, y } = this.screenToCameraRect(mx, my); - return new Vector3(x, y, 0).unproject(this._camera); + return new Vector3(x, y, 0).unproject(this.camera); } rectDetected(shapeType: ShapeType, mx1: number, my1: number, mx2: number, my2: number) { let detecteds: VisualShapeData[] = []; - const selectionBox = new SelectionBox(this._camera, this._scene); + const selectionBox = new SelectionBox(this.camera, this._scene); const start = this.screenToCameraRect(mx1, my1); const end = this.screenToCameraRect(mx2, my2); selectionBox.startPoint.set(start.x, start.y, 0.5); @@ -372,7 +350,8 @@ export class ThreeView extends Observable implements IView { private initRaycaster(mx: number, my: number) { let ray = this.rayAt(mx, my); - let threshold = Constants.RaycasterThreshold * this.cameraController.scale; + let threshold = Constants.RaycasterThreshold / this.cameraController.scale; + if (threshold < 5) threshold = 5; let raycaster = new Raycaster(ThreeHelper.fromXYZ(ray.location), ThreeHelper.fromXYZ(ray.direction)); raycaster.params = { Line: { threshold }, Points: { threshold } }; return raycaster;