@inweb/viewer-three
Version:
JavaScript library for rendering CAD and BIM files in a browser using Three.js
212 lines (171 loc) • 7.09 kB
text/typescript
///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
// This application incorporates Open Design Alliance software pursuant to a
// license agreement with Open Design Alliance.
// Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
// All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////
import {
Camera,
EdgesGeometry,
Intersection,
Line3,
MathUtils,
Object3D,
Plane,
Raycaster,
Vector2,
Vector3,
WebGLRenderer,
} from "three";
const DESKTOP_SNAP_DISTANCE = 10;
const MOBILE_SNAP_DISTANCE = 50;
const _vertex = new Vector3();
const _start = new Vector3();
const _end = new Vector3();
const _line = new Line3();
const _center = new Vector3();
const _projection = new Vector3();
export class Snapper {
public camera: Camera;
public renderer: WebGLRenderer;
public canvas: HTMLCanvasElement;
public threshold: number;
private raycaster: Raycaster;
private detectRadiusInPixels: number;
private edgesCache: WeakMap<any, EdgesGeometry>;
constructor(camera: Camera, renderer: WebGLRenderer, canvas: HTMLCanvasElement) {
this.camera = camera;
this.renderer = renderer;
this.canvas = canvas;
this.threshold = 0.0001;
this.raycaster = new Raycaster();
this.detectRadiusInPixels = this.isMobile() ? MOBILE_SNAP_DISTANCE : DESKTOP_SNAP_DISTANCE;
this.edgesCache = new WeakMap();
}
isMobile(): boolean {
if (typeof navigator === "undefined") return false;
// ===================== AI-CODE-START ======================
// Source: Claude Sonnet 4
// Date: 2025-09-09
// Reviewer: roman.mochalov@opendesign.com
// Issue: CLOUD-5799
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
// ===================== AI-CODE-END ======================
}
getMousePosition(event: MouseEvent, target: Vector2): Vector2 {
return target.set(event.clientX, event.clientY);
}
getPointerIntersects(mouse: Vector2, objects: Object3D[]): Array<Intersection<Object3D>> {
const rect = this.canvas.getBoundingClientRect();
const x = ((mouse.x - rect.left) / rect.width) * 2 - 1;
const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1;
const coords = new Vector2(x, y);
this.raycaster.setFromCamera(coords, this.camera);
this.raycaster.params = {
Mesh: {},
Line: { threshold: this.threshold },
Line2: { threshold: this.threshold },
LOD: {},
Points: { threshold: this.threshold },
Sprite: {},
};
let intersects = this.raycaster.intersectObjects(objects, false);
(this.renderer.clippingPlanes || []).forEach((plane: Plane) => {
intersects = intersects.filter((intersect) => plane.distanceToPoint(intersect.point) >= 0);
});
return intersects;
}
getDetectRadius(point: Vector3): number {
const camera: any = this.camera;
// ===================== AI-CODE-START ======================
// Source: Gemini 2.5 Pro
// Date: 2025-08-27
// Reviewer: roman.mochalov@opendesign.com
// Issue: CLOUD-5799
// Notes: Originally AI-generated, modified manually
if (camera.isOrthographicCamera) {
const fieldHeight = (camera.top - camera.bottom) / camera.zoom;
const canvasHeight = this.canvas.height;
const worldUnitsPerPixel = fieldHeight / canvasHeight;
return this.detectRadiusInPixels * worldUnitsPerPixel;
}
if (camera.isPerspectiveCamera) {
const distance = camera.position.distanceTo(point);
const fieldHeight = 2 * Math.tan(MathUtils.degToRad(camera.fov * 0.5)) * distance;
const canvasHeight = this.canvas.height;
const worldUnitsPerPixel = fieldHeight / canvasHeight;
return this.detectRadiusInPixels * worldUnitsPerPixel;
}
// ===================== AI-CODE-END ======================
return 0.1;
}
getSnapPoint(mouse: Vector2, objects: Object3D[]): Vector3 {
const intersections = this.getPointerIntersects(mouse, objects);
if (intersections.length === 0) return undefined;
// ===================== AI-CODE-START ======================
// Source: Gemini 2.5 Pro
// Date: 2025-08-20
// Reviewer: roman.mochalov@opendesign.com
// Issue: CLOUD-5799
// Notes: Originally AI-generated, modified manually
const object: any = intersections[0].object;
const intersectionPoint = intersections[0].point;
const localPoint = object.worldToLocal(intersectionPoint.clone());
let snapPoint: Vector3;
let snapDistance = this.getDetectRadius(intersectionPoint);
const geometry = object.geometry;
const positions = geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
_vertex.set(positions[i], positions[i + 1], positions[i + 2]);
const distance = _vertex.distanceTo(localPoint);
if (distance < snapDistance) {
snapDistance = distance;
snapPoint = _vertex.clone();
}
}
if (snapPoint) return object.localToWorld(snapPoint);
let edges = this.edgesCache.get(geometry);
if (!edges) {
edges = new EdgesGeometry(geometry);
this.edgesCache.set(geometry, edges);
}
const edgePositions = edges.attributes.position.array;
for (let i = 0; i < edgePositions.length; i += 6) {
_start.set(edgePositions[i], edgePositions[i + 1], edgePositions[i + 2]);
_end.set(edgePositions[i + 3], edgePositions[i + 4], edgePositions[i + 5]);
_line.set(_start, _end);
_line.getCenter(_center);
const centerDistance = _center.distanceTo(localPoint);
if (centerDistance < snapDistance) {
snapDistance = centerDistance;
snapPoint = _center.clone();
continue;
}
_line.closestPointToPoint(localPoint, true, _projection);
const lineDistance = _projection.distanceTo(localPoint);
if (lineDistance < snapDistance) {
snapDistance = lineDistance;
snapPoint = _projection.clone();
}
}
if (snapPoint) return object.localToWorld(snapPoint);
// ===================== AI-CODE-END ======================
return intersectionPoint.clone();
}
}