Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions example/zoom-implementation/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<head>
<style>
body { margin: 0; font-family: sans-serif; }
#controls {
position: absolute;
top: 15px;
left: 15px;
z-index: 10;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
button {
display: block;
margin-bottom: 10px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
border-radius: 4px;
border: 1px solid #ccc;
background: white;
transition: background 0.2s;
}
button:hover {
background: #f0f0f0;
}
p { margin: 0 0 10px 0; max-width: 300px; font-size: 13px; color: #333; line-height: 1.4; }
canvas{
border: #ccc 1px solid;
}
</style>

<script type="importmap">{ "imports": {
"three": "https://esm.sh/three"
}}</script>
<script type="module">
import * as THREE from 'three';
window.THREE = THREE;
</script>

<script src="../../dist/three-render-objects.js" defer></script>
</head>

<body>
<div id="myscene"></div>
<div id="controls">
<button id="zoom-normal-btn">Zoom to Fit (New Implementation)</button>

<button id="zoom-old-btn">Zoom to Fit (Old Implementation)</button>
</div>

<script type="module">
import * as THREE from 'three';

const N = 200;
const X_RANGE = 250;
const Y_RANGE = 250;
const Z_RANGE = 50;

const objs = [...Array(N)].map(() => new THREE.Mesh(
new THREE.SphereGeometry(15, 16, 16),
new THREE.MeshNormalMaterial({ wireframe: true })
));

objs.forEach(obj => {
// Offset by 600 on the X axis so it is NOT centered at 0,0,0
obj.position.x = 600 + Math.random() * X_RANGE * 2 - X_RANGE;
obj.position.y = Math.random() * Y_RANGE * 2 - Y_RANGE;
obj.position.z = Math.random() * Z_RANGE * 2 - Z_RANGE;
});

// Simple axis helper though this will affect the bounding box
// objs.push(new THREE.AxesHelper(100));

const ObjRender = new ThreeRenderObjects(
document.getElementById('myscene'),
{ controlType: 'trackball' }
)
.objects(objs);

document.getElementById('zoom-normal-btn').addEventListener('click', () => {
ObjRender.zoomToFit(800, 20);
});

document.getElementById('zoom-old-btn').addEventListener('click', () => {
// Replicate the old fitToBbox logic from old-three-render-objects.js
const camera = ObjRender.camera();
const bbox = ObjRender.getBbox();
const transitionDuration = 800;
const padding = 20;

if (bbox) {
const center = new THREE.Vector3(0, 0, 0); // old logic always reset to 0,0,0
const maxBoxSide = Math.max(...Object.entries(bbox)
.map(([coordType, coords]) => Math.max(...coords.map(c => Math.abs(center[coordType] - c))))
) * 2;

const paddedFov = (1 - (padding * 2 / ObjRender.height())) * camera.fov;
const fitHeightDistance = maxBoxSide / Math.atan(paddedFov * Math.PI / 180);
const fitWidthDistance = fitHeightDistance / camera.aspect;
const distance = Math.max(fitHeightDistance, fitWidthDistance);

if (distance > 0) {
const newCameraPosition = center.clone()
.sub(camera.position)
.normalize()
.multiplyScalar(-distance);

ObjRender.cameraPosition(newCameraPosition, center, transitionDuration);
}
}
});


(function animate() {
ObjRender.tick();
requestAnimationFrame(animate);
})();
</script>
</body>
70 changes: 52 additions & 18 deletions src/three-render-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,35 +223,69 @@ export default Kapsule({
return this.fitToBbox(this.getBbox(...bboxArgs), transitionDuration, padding);
},
fitToBbox: function (state, bbox, transitionDuration = 0, padding = 10) {
// based on https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
const camera = state.camera;

if (bbox) {
const center = new three.Vector3(0, 0, 0); // reset camera aim to center
const maxBoxSide = Math.max(...Object.entries(bbox)
.map(([coordType, coords]) => Math.max(...coords.map(c => Math.abs(center[coordType] - c))))
) * 2;
const center = new three.Vector3(
(bbox.x[0] + bbox.x[1]) / 2,
(bbox.y[0] + bbox.y[1]) / 2,
(bbox.z[0] + bbox.z[1]) / 2
);

camera.updateMatrixWorld();
const camRight = new three.Vector3();
const camUp = new three.Vector3();
const camForward = new three.Vector3();
camera.getWorldDirection(camForward);
camRight.crossVectors(camForward, camera.up).normalize();
camUp.crossVectors(camRight, camForward).normalize();

// find distance that fits whole bbox within padded fov
const paddedFov = (1 - (padding * 2 / state.height)) * camera.fov;
const fitHeightDistance = maxBoxSide / Math.atan(paddedFov * Math.PI / 180);
const fitWidthDistance = fitHeightDistance / camera.aspect;
const distance = Math.max(fitHeightDistance, fitWidthDistance);
const vFovHalf = (paddedFov / 2) * (Math.PI / 180);
const hFovHalf = Math.atan(Math.tan(vFovHalf) * camera.aspect);

let maxDistance = 0;
const corners = [
new three.Vector3(bbox.x[0], bbox.y[0], bbox.z[0]),
new three.Vector3(bbox.x[0], bbox.y[0], bbox.z[1]),
new three.Vector3(bbox.x[0], bbox.y[1], bbox.z[0]),
new three.Vector3(bbox.x[0], bbox.y[1], bbox.z[1]),
new three.Vector3(bbox.x[1], bbox.y[0], bbox.z[0]),
new three.Vector3(bbox.x[1], bbox.y[0], bbox.z[1]),
new three.Vector3(bbox.x[1], bbox.y[1], bbox.z[0]),
new three.Vector3(bbox.x[1], bbox.y[1], bbox.z[1])
];

for (const corner of corners) {
const localPos = corner.clone().sub(center);
const h = Math.abs(localPos.dot(camUp));
const w = Math.abs(localPos.dot(camRight));
const depthOffset = localPos.dot(camForward);

const distH = h / Math.tan(vFovHalf) - depthOffset;
const distW = w / Math.tan(hFovHalf) - depthOffset;
maxDistance = Math.max(maxDistance, distH, distW);
}

if (distance > 0) {
const newCameraPosition = center.clone()
.sub(camera.position)
.normalize()
.multiplyScalar(-distance);
const distance = maxDistance;
const dir = camForward.clone().negate();
const newPos = {
x: center.x + dir.x * distance,
y: center.y + dir.y * distance,
z: center.z + dir.z * distance,
};

this.cameraPosition(
newPos,
{ x: center.x, y: center.y, z: center.z },
transitionDuration
);

this.cameraPosition(newCameraPosition, center, transitionDuration);
}
}

return this;
},
getBbox: function (state, objFilter = () => true) {
const box = new three.Box3(new three.Vector3(0, 0, 0), new three.Vector3(0, 0, 0));
const box = new three.Box3();
const objs = state.objects.filter(objFilter);

if (!objs.length) return null;
Expand Down