UNPKG

@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
/////////////////////////////////////////////////////////////////////////////// // 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(); } }