@inweb/viewer-three 27.2.1 → 27.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { draggersRegistry, commandsRegistry, Options, componentsRegistry, Info, Loader, loadersRegistry, CANVAS_EVENTS } from '@inweb/viewer-core';
25
25
  export * from '@inweb/viewer-core';
26
- import { Line, Vector3, BufferGeometry, Float32BufferAttribute, LineBasicMaterial, Mesh, MeshBasicMaterial, DoubleSide, EventDispatcher, MOUSE, TOUCH, Spherical, Quaternion, Vector2, Plane, Object3D, Line3, Raycaster, MathUtils, EdgesGeometry, Matrix4, Vector4, Controls, Clock, Box3, Color, PerspectiveCamera, OrthographicCamera, AmbientLight, DirectionalLight, HemisphereLight, REVISION, MeshPhongMaterial, WebGLRenderTarget, UnsignedByteType, RGBAFormat, CylinderGeometry, Sprite, CanvasTexture, SRGBColorSpace, SpriteMaterial, TextureLoader, BufferAttribute, PointsMaterial, Points, TriangleStripDrawMode, TriangleFanDrawMode, LineSegments, LineLoop, Group, NormalBlending, LoadingManager, LoaderUtils, FileLoader, UniformsUtils, ShaderMaterial, AdditiveBlending, HalfFloatType, Scene, WebGLRenderer, LinearSRGBColorSpace } from 'three';
26
+ import { EventDispatcher, Vector3, MOUSE, TOUCH, Spherical, Quaternion, Vector2, Object3D, BufferGeometry, Float32BufferAttribute, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, DoubleSide, Line3, Raycaster, MathUtils, EdgesGeometry, Plane, Matrix4, Vector4, Controls, Box3, Clock, Color, PerspectiveCamera, OrthographicCamera, AmbientLight, DirectionalLight, HemisphereLight, REVISION, MeshPhongMaterial, WebGLRenderTarget, UnsignedByteType, RGBAFormat, CylinderGeometry, Sprite, CanvasTexture, SRGBColorSpace, SpriteMaterial, TextureLoader, BufferAttribute, PointsMaterial, DataTexture, FloatType, NearestFilter, Points, TriangleStripDrawMode, TriangleFanDrawMode, LineSegments, LineLoop, Group, NormalBlending, LoadingManager, LoaderUtils, FileLoader, UniformsUtils, ShaderMaterial, AdditiveBlending, HalfFloatType, Scene, WebGLRenderer, LinearSRGBColorSpace } from 'three';
27
27
  import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
28
28
  import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js';
29
29
  import { Wireframe } from 'three/examples/jsm/lines/Wireframe.js';
@@ -41,47 +41,6 @@ import { EventEmitter2 } from '@inweb/eventemitter2';
41
41
  import { Markup } from '@inweb/markup';
42
42
  export * from '@inweb/markup';
43
43
 
44
- class PlaneHelper extends Line {
45
- constructor(plane, size = 1, color = 0xffff00, offset = new Vector3()) {
46
- const positions = [1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0, 1, 1, 0];
47
- const geometry = new BufferGeometry();
48
- geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
49
- geometry.computeBoundingSphere();
50
- super(geometry, new LineBasicMaterial({ color, toneMapped: false }));
51
- this.type = "PlaneHelper";
52
- this.plane = plane;
53
- this.size = size;
54
- this.offset = offset;
55
- const positions2 = [1, 1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0];
56
- const geometry2 = new BufferGeometry();
57
- geometry2.setAttribute("position", new Float32BufferAttribute(positions2, 3));
58
- geometry2.computeBoundingSphere();
59
- this.helper = new Mesh(geometry2, new MeshBasicMaterial({
60
- color,
61
- opacity: 0.2,
62
- transparent: true,
63
- depthWrite: false,
64
- toneMapped: false,
65
- side: DoubleSide,
66
- }));
67
- this.add(this.helper);
68
- }
69
- dispose() {
70
- this.geometry.dispose();
71
- this.material.dispose();
72
- this.children[0].geometry.dispose();
73
- this.children[0].material.dispose();
74
- }
75
- updateMatrixWorld(force) {
76
- this.position.set(0, 0, 0);
77
- this.lookAt(this.plane.normal);
78
- this.position.copy(this.offset);
79
- this.translateZ(-(this.offset.dot(this.plane.normal) + this.plane.constant));
80
- this.scale.set(0.5 * this.size, 0.5 * this.size, 1);
81
- super.updateMatrixWorld(force);
82
- }
83
- }
84
-
85
44
  const _changeEvent = { type: "change" };
86
45
  const _startEvent = { type: "start" };
87
46
  const _endEvent = { type: "end" };
@@ -740,6 +699,166 @@ class OrbitControls extends EventDispatcher {
740
699
  }
741
700
  }
742
701
 
702
+ class PlaneHelper2 extends Object3D {
703
+ constructor(size = 1, color = 0xf0f0f0, opacity = 0.15) {
704
+ super();
705
+ this.type = "PlaneHelper2";
706
+ this.size = size;
707
+ const positions = [1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0, 1, 1, 0];
708
+ const geometry = new BufferGeometry();
709
+ geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
710
+ geometry.computeBoundingSphere();
711
+ this.outline = new Line(geometry, new LineBasicMaterial({ color, toneMapped: false }));
712
+ const positions2 = [1, 1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0];
713
+ const geometry2 = new BufferGeometry();
714
+ geometry2.setAttribute("position", new Float32BufferAttribute(positions2, 3));
715
+ geometry2.computeBoundingSphere();
716
+ this.mesh = new Mesh(geometry2, new MeshBasicMaterial({
717
+ color,
718
+ opacity,
719
+ transparent: true,
720
+ depthWrite: false,
721
+ toneMapped: false,
722
+ side: DoubleSide,
723
+ }));
724
+ this.add(this.outline);
725
+ this.add(this.mesh);
726
+ }
727
+ dispose() {
728
+ this.outline.geometry.dispose();
729
+ this.outline.material.dispose();
730
+ this.mesh.geometry.dispose();
731
+ this.mesh.material.dispose();
732
+ }
733
+ updateMatrixWorld(force) {
734
+ this.scale.set(0.5 * this.size, 0.5 * this.size, 1);
735
+ super.updateMatrixWorld(force);
736
+ }
737
+ getLineMaterial() {
738
+ return this.outline.material;
739
+ }
740
+ getMeshMaterial() {
741
+ return this.mesh.material;
742
+ }
743
+ }
744
+
745
+ const DESKTOP_SNAP_DISTANCE = 10;
746
+ const MOBILE_SNAP_DISTANCE = 50;
747
+ const _vertex = new Vector3();
748
+ const _start = new Vector3();
749
+ const _end = new Vector3();
750
+ const _line = new Line3();
751
+ const _center = new Vector3();
752
+ const _projection = new Vector3();
753
+ class Snapper {
754
+ constructor(camera, renderer, canvas) {
755
+ this.camera = camera;
756
+ this.renderer = renderer;
757
+ this.canvas = canvas;
758
+ this.threshold = 0.0001;
759
+ this.raycaster = new Raycaster();
760
+ this.detectRadiusInPixels = this.isMobile() ? MOBILE_SNAP_DISTANCE : DESKTOP_SNAP_DISTANCE;
761
+ this.edgesCache = new WeakMap();
762
+ }
763
+ isMobile() {
764
+ if (typeof navigator === "undefined")
765
+ return false;
766
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
767
+ }
768
+ getMousePosition(event, target) {
769
+ return target.set(event.clientX, event.clientY);
770
+ }
771
+ getPointerIntersects(mouse, objects, recursive = false, clip = true) {
772
+ const rect = this.canvas.getBoundingClientRect();
773
+ const x = ((mouse.x - rect.left) / rect.width) * 2 - 1;
774
+ const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1;
775
+ const coords = new Vector2(x, y);
776
+ this.raycaster.setFromCamera(coords, this.camera);
777
+ this.raycaster.params = {
778
+ Mesh: {},
779
+ Line: { threshold: this.threshold },
780
+ Line2: { threshold: this.threshold },
781
+ LOD: {},
782
+ Points: { threshold: this.threshold },
783
+ Sprite: {},
784
+ };
785
+ let intersects = this.raycaster.intersectObjects(objects, recursive);
786
+ if (clip) {
787
+ const clippingPlanes = this.renderer.clippingPlanes || [];
788
+ clippingPlanes.forEach((plane) => {
789
+ intersects = intersects.filter((intersect) => plane.distanceToPoint(intersect.point) >= 0);
790
+ });
791
+ }
792
+ return intersects;
793
+ }
794
+ getDetectRadius(point) {
795
+ const camera = this.camera;
796
+ if (camera.isOrthographicCamera) {
797
+ const fieldHeight = (camera.top - camera.bottom) / camera.zoom;
798
+ const canvasHeight = this.canvas.height;
799
+ const worldUnitsPerPixel = fieldHeight / canvasHeight;
800
+ return this.detectRadiusInPixels * worldUnitsPerPixel;
801
+ }
802
+ if (camera.isPerspectiveCamera) {
803
+ const distance = camera.position.distanceTo(point);
804
+ const fieldHeight = 2 * Math.tan(MathUtils.degToRad(camera.fov * 0.5)) * distance;
805
+ const canvasHeight = this.canvas.height;
806
+ const worldUnitsPerPixel = fieldHeight / canvasHeight;
807
+ return this.detectRadiusInPixels * worldUnitsPerPixel;
808
+ }
809
+ return 0.1;
810
+ }
811
+ getSnapPoint(mouse, objects) {
812
+ const intersections = this.getPointerIntersects(mouse, objects);
813
+ if (intersections.length === 0)
814
+ return undefined;
815
+ const object = intersections[0].object;
816
+ const intersectionPoint = intersections[0].point;
817
+ const localPoint = object.worldToLocal(intersectionPoint.clone());
818
+ let snapPoint;
819
+ let snapDistance = this.getDetectRadius(intersectionPoint);
820
+ const geometry = object.geometry;
821
+ const positions = geometry.attributes.position.array;
822
+ for (let i = 0; i < positions.length; i += 3) {
823
+ _vertex.set(positions[i], positions[i + 1], positions[i + 2]);
824
+ const distance = _vertex.distanceTo(localPoint);
825
+ if (distance < snapDistance) {
826
+ snapDistance = distance;
827
+ snapPoint = _vertex.clone();
828
+ }
829
+ }
830
+ if (snapPoint)
831
+ return object.localToWorld(snapPoint);
832
+ let edges = this.edgesCache.get(geometry);
833
+ if (!edges) {
834
+ edges = new EdgesGeometry(geometry);
835
+ this.edgesCache.set(geometry, edges);
836
+ }
837
+ const edgePositions = edges.attributes.position.array;
838
+ for (let i = 0; i < edgePositions.length; i += 6) {
839
+ _start.set(edgePositions[i], edgePositions[i + 1], edgePositions[i + 2]);
840
+ _end.set(edgePositions[i + 3], edgePositions[i + 4], edgePositions[i + 5]);
841
+ _line.set(_start, _end);
842
+ _line.getCenter(_center);
843
+ const centerDistance = _center.distanceTo(localPoint);
844
+ if (centerDistance < snapDistance) {
845
+ snapDistance = centerDistance;
846
+ snapPoint = _center.clone();
847
+ continue;
848
+ }
849
+ _line.closestPointToPoint(localPoint, true, _projection);
850
+ const lineDistance = _projection.distanceTo(localPoint);
851
+ if (lineDistance < snapDistance) {
852
+ snapDistance = lineDistance;
853
+ snapPoint = _projection.clone();
854
+ }
855
+ }
856
+ if (snapPoint)
857
+ return object.localToWorld(snapPoint);
858
+ return intersectionPoint.clone();
859
+ }
860
+ }
861
+
743
862
  class OrbitDragger {
744
863
  constructor(viewer) {
745
864
  this.updateControls = () => {
@@ -832,52 +951,129 @@ class OrbitDragger {
832
951
  }
833
952
 
834
953
  class CuttingPlaneDragger extends OrbitDragger {
835
- constructor(viewer, normal, color) {
954
+ constructor(viewer) {
836
955
  super(viewer);
956
+ this.helpers = [];
957
+ this.activeHelper = null;
837
958
  this.transformChange = () => {
838
- this.plane.constant = -this.planeCenter.position.dot(this.plane.normal);
959
+ if (!this.activeHelper)
960
+ return;
961
+ const plane = this.activeHelper.plane;
962
+ plane.normal.copy(new Vector3(0, 0, -1)).applyQuaternion(this.activeHelper.quaternion);
963
+ plane.constant = -this.activeHelper.position.dot(plane.normal);
839
964
  this.viewer.update();
965
+ this.changed = true;
966
+ };
967
+ this.translateDrag = (event) => {
968
+ this.orbit.enabled = !event.value;
969
+ this.rotate.enabled = !event.value;
840
970
  };
841
- this.transformDrag = (event) => {
971
+ this.rotateDrag = (event) => {
842
972
  this.orbit.enabled = !event.value;
973
+ this.translate.enabled = !event.value;
843
974
  };
844
975
  this.updatePlaneSize = () => {
845
- this.planeHelper.size = this.viewer.extents.getSize(new Vector3()).length() || 1;
976
+ const extentsSize = this.viewer.extents.getSize(new Vector3()).length() || 1;
977
+ this.helpers.forEach((planeHelper) => (planeHelper.size = extentsSize));
846
978
  this.viewer.update();
847
979
  };
848
980
  this.updateTransformCamera = () => {
849
- this.transform.camera = this.viewer.camera;
981
+ this.translate.camera = this.viewer.camera;
982
+ this.rotate.camera = this.viewer.camera;
983
+ this.snapper.camera = this.viewer.camera;
984
+ };
985
+ this.clearHelpers = () => {
986
+ this.setActiveHelper();
987
+ this.helpers.forEach((helper) => {
988
+ helper.removeFromParent();
989
+ helper.dispose();
990
+ });
991
+ this.helpers = [];
992
+ this.viewer.update();
993
+ };
994
+ this.onKeyDown = (event) => {
995
+ if (event.key === "Shift")
996
+ this.rotate.setRotationSnap(Math.PI / 4);
997
+ if (event.key === "Delete" || event.key === "Backspace")
998
+ this.deleteActivePlane();
999
+ if (event.key === "Escape" && (this.translate.dragging || this.rotate.dragging))
1000
+ this.reset();
1001
+ };
1002
+ this.onKeyUp = (event) => {
1003
+ if (event.key === "Shift")
1004
+ this.rotate.setRotationSnap(null);
1005
+ };
1006
+ this.onPointerDown = (event) => {
1007
+ if (event.button !== 0 || !event.isPrimary)
1008
+ return;
1009
+ this.snapper.getMousePosition(event, this.downPosition);
1010
+ this.saveState();
1011
+ };
1012
+ this.onPointerUp = (event) => {
1013
+ if (event.button !== 0)
1014
+ return;
1015
+ const upPosition = this.snapper.getMousePosition(event, new Vector2());
1016
+ if (upPosition.distanceTo(this.downPosition) !== 0)
1017
+ return;
1018
+ const intersects = this.snapper.getPointerIntersects(upPosition, this.helpers, true, false);
1019
+ if (intersects.length === 0)
1020
+ return;
1021
+ this.setActiveHelper(intersects[0].object.parent);
1022
+ };
1023
+ this.onPointerCancel = (event) => {
1024
+ this.viewer.canvas.dispatchEvent(new PointerEvent("pointerup", event));
850
1025
  };
851
1026
  this.onDoubleClick = (event) => {
1027
+ if (!this.activeHelper)
1028
+ return;
1029
+ const mousePosition = this.snapper.getMousePosition(event, new Vector2());
1030
+ const intersects = this.snapper.getPointerIntersects(mousePosition, [this.activeHelper], true, false);
1031
+ if (intersects.length === 0)
1032
+ return;
1033
+ this.activeHelper.rotateOnAxis(new Vector3(0, 1, 0), Math.PI);
1034
+ this.transformChange();
852
1035
  event.stopPropagation();
853
- this.plane.negate();
854
- this.viewer.update();
855
1036
  };
856
- const extentsSize = viewer.extents.getSize(new Vector3()).length() || 1;
857
- const extentsCenter = viewer.extents.getCenter(new Vector3());
858
- const constant = -extentsCenter.dot(normal);
859
- this.plane = new Plane(normal, constant);
860
1037
  if (!viewer.renderer.clippingPlanes)
861
1038
  viewer.renderer.clippingPlanes = [];
862
- viewer.renderer.clippingPlanes.push(this.plane);
863
- this.planeHelper = new PlaneHelper(this.plane, extentsSize, color, extentsCenter);
864
- this.viewer.helpers.add(this.planeHelper);
865
- this.planeCenter = new Object3D();
866
- this.planeCenter.position.copy(extentsCenter);
867
- this.viewer.helpers.add(this.planeCenter);
868
- this.transform = new TransformControls(viewer.camera, viewer.canvas);
869
- this.transform.showX = !!normal.x;
870
- this.transform.showY = !!normal.y;
871
- this.transform.showZ = !!normal.z;
872
- this.transform.attach(this.planeCenter);
873
- this.transform.addEventListener("change", this.transformChange);
874
- this.transform.addEventListener("dragging-changed", this.transformDrag);
875
- this.viewer.helpers.add(this.transform.getHelper());
1039
+ this.clippingPlanes = viewer.renderer.clippingPlanes;
1040
+ this.clippingPlanes.forEach((plane) => this.addHelper(plane));
1041
+ const extentsSize = viewer.extents.getSize(new Vector3()).length() || 1;
1042
+ this.snapper = new Snapper(viewer.camera, viewer.renderer, viewer.canvas);
1043
+ this.snapper.threshold = extentsSize / 10000;
1044
+ this.downPosition = new Vector2();
1045
+ this.position0 = new Vector3();
1046
+ this.quaternion0 = new Quaternion();
1047
+ this.translate = new TransformControls(viewer.camera, viewer.canvas);
1048
+ this.translate.setMode("translate");
1049
+ this.translate.setSpace("local");
1050
+ this.translate.showX = false;
1051
+ this.translate.showY = false;
1052
+ this.translate.showZ = true;
1053
+ this.translate.addEventListener("change", this.transformChange);
1054
+ this.translate.addEventListener("dragging-changed", this.translateDrag);
1055
+ this.viewer.helpers.add(this.translate.getHelper());
1056
+ this.rotate = new TransformControls(viewer.camera, viewer.canvas);
1057
+ this.rotate.setMode("rotate");
1058
+ this.rotate.setSpace("local");
1059
+ this.rotate.showX = true;
1060
+ this.rotate.showY = true;
1061
+ this.rotate.showZ = false;
1062
+ this.rotate.addEventListener("change", this.transformChange);
1063
+ this.rotate.addEventListener("dragging-changed", this.rotateDrag);
1064
+ this.viewer.helpers.add(this.rotate.getHelper());
1065
+ this.setActiveHelper(this.helpers[this.helpers.length - 1]);
876
1066
  this.viewer.addEventListener("explode", this.updatePlaneSize);
877
1067
  this.viewer.addEventListener("show", this.updatePlaneSize);
878
1068
  this.viewer.addEventListener("showall", this.updatePlaneSize);
879
1069
  this.viewer.addEventListener("changecameramode", this.updateTransformCamera);
1070
+ this.viewer.addEventListener("clearslices", this.clearHelpers);
1071
+ this.viewer.canvas.addEventListener("pointerdown", this.onPointerDown, true);
1072
+ this.viewer.canvas.addEventListener("pointerup", this.onPointerUp, true);
1073
+ this.viewer.canvas.addEventListener("pointercancel", this.onPointerCancel, true);
880
1074
  this.viewer.canvas.addEventListener("dblclick", this.onDoubleClick, true);
1075
+ window.addEventListener("keydown", this.onKeyDown);
1076
+ window.addEventListener("keyup", this.onKeyUp);
881
1077
  this.viewer.update();
882
1078
  }
883
1079
  dispose() {
@@ -885,34 +1081,125 @@ class CuttingPlaneDragger extends OrbitDragger {
885
1081
  this.viewer.removeEventListener("show", this.updatePlaneSize);
886
1082
  this.viewer.removeEventListener("showall", this.updatePlaneSize);
887
1083
  this.viewer.removeEventListener("changecameramode", this.updateTransformCamera);
1084
+ this.viewer.removeEventListener("clearslices", this.clearHelpers);
1085
+ this.viewer.canvas.removeEventListener("pointerdown", this.onPointerDown, true);
1086
+ this.viewer.canvas.removeEventListener("pointerup", this.onPointerUp, true);
1087
+ this.viewer.canvas.removeEventListener("pointercancel", this.onPointerCancel, true);
888
1088
  this.viewer.canvas.removeEventListener("dblclick", this.onDoubleClick, true);
889
- this.transform.removeEventListener("change", this.transformChange);
890
- this.transform.removeEventListener("dragging-changed", this.transformDrag);
891
- this.transform.getHelper().removeFromParent();
892
- this.transform.detach();
893
- this.transform.dispose();
894
- this.planeHelper.removeFromParent();
895
- this.planeHelper.dispose();
896
- this.planeCenter.removeFromParent();
1089
+ window.removeEventListener("keydown", this.onKeyDown);
1090
+ window.removeEventListener("keyup", this.onKeyUp);
1091
+ this.translate.removeEventListener("change", this.transformChange);
1092
+ this.translate.removeEventListener("dragging-changed", this.translateDrag);
1093
+ this.translate.getHelper().removeFromParent();
1094
+ this.translate.detach();
1095
+ this.translate.dispose();
1096
+ this.rotate.removeEventListener("change", this.transformChange);
1097
+ this.rotate.removeEventListener("dragging-changed", this.rotateDrag);
1098
+ this.rotate.getHelper().removeFromParent();
1099
+ this.rotate.detach();
1100
+ this.rotate.dispose();
1101
+ this.helpers.forEach((helper) => {
1102
+ helper.removeFromParent();
1103
+ helper.dispose();
1104
+ });
1105
+ this.helpers = [];
1106
+ this.activeHelper = null;
897
1107
  super.dispose();
898
1108
  }
1109
+ addHelper(plane) {
1110
+ const extentsSize = this.viewer.extents.getSize(new Vector3()).length() || 1;
1111
+ const extentsCenter = this.viewer.extents.getCenter(new Vector3());
1112
+ const helper = new PlaneHelper2(extentsSize);
1113
+ helper.plane = plane;
1114
+ helper.position.copy(plane.projectPoint(extentsCenter, new Vector3()));
1115
+ helper.quaternion.setFromUnitVectors(new Vector3(0, 0, -1), plane.normal);
1116
+ this.helpers.push(helper);
1117
+ this.viewer.helpers.add(helper);
1118
+ return helper;
1119
+ }
1120
+ setActiveHelper(helper) {
1121
+ if (helper === this.activeHelper)
1122
+ return;
1123
+ if (this.activeHelper) {
1124
+ this.activeHelper.getLineMaterial().color.setHex(0xf0f0f0);
1125
+ this.activeHelper.getMeshMaterial().opacity = 0.15;
1126
+ this.translate.detach();
1127
+ this.rotate.detach();
1128
+ }
1129
+ this.activeHelper = helper;
1130
+ if (this.activeHelper) {
1131
+ this.activeHelper.getLineMaterial().color.setHex(0xd0d0d0);
1132
+ this.activeHelper.getMeshMaterial().opacity = 0.3;
1133
+ this.translate.attach(this.activeHelper);
1134
+ this.rotate.attach(this.activeHelper);
1135
+ }
1136
+ this.viewer.update();
1137
+ }
1138
+ saveState() {
1139
+ if (!this.activeHelper)
1140
+ return;
1141
+ this.position0.copy(this.activeHelper.position);
1142
+ this.quaternion0.copy(this.activeHelper.quaternion);
1143
+ }
1144
+ reset() {
1145
+ if (!this.activeHelper)
1146
+ return;
1147
+ this.translate.dragging = false;
1148
+ this.rotate.dragging = false;
1149
+ this.orbit.state = STATE.NONE;
1150
+ this.activeHelper.position.copy(this.position0);
1151
+ this.activeHelper.quaternion.copy(this.quaternion0);
1152
+ this.transformChange();
1153
+ }
1154
+ addPlane(normal) {
1155
+ const extentsCenter = this.viewer.extents.getCenter(new Vector3());
1156
+ const constant = -extentsCenter.dot(normal);
1157
+ const plane = new Plane(normal, constant);
1158
+ this.clippingPlanes.push(plane);
1159
+ const helper = this.addHelper(plane);
1160
+ this.setActiveHelper(helper);
1161
+ }
1162
+ addPlaneX() {
1163
+ this.addPlane(new Vector3(-1, 0, 0));
1164
+ }
1165
+ addPlaneY() {
1166
+ this.addPlane(new Vector3(0, -1, 0));
1167
+ }
1168
+ addPlaneZ() {
1169
+ this.addPlane(new Vector3(0, 0, -1));
1170
+ }
1171
+ deleteActivePlane() {
1172
+ if (!this.activeHelper)
1173
+ return;
1174
+ const helper = this.activeHelper;
1175
+ const index = this.clippingPlanes.indexOf(helper.plane);
1176
+ if (index !== -1)
1177
+ this.clippingPlanes.splice(index, 1);
1178
+ this.helpers = this.helpers.filter((x) => x !== helper);
1179
+ helper.removeFromParent();
1180
+ helper.dispose();
1181
+ this.setActiveHelper(this.helpers[this.helpers.length - 1]);
1182
+ }
899
1183
  }
900
1184
 
901
1185
  class CuttingPlaneXAxisDragger extends CuttingPlaneDragger {
902
1186
  constructor(viewer) {
903
- super(viewer, new Vector3(1, 0, 0), 0xff0000);
1187
+ super(viewer);
1188
+ this.addPlaneX();
904
1189
  }
905
1190
  }
906
1191
 
907
1192
  class CuttingPlaneYAxisDragger extends CuttingPlaneDragger {
908
1193
  constructor(viewer) {
909
- super(viewer, new Vector3(0, 1, 0), 0x00ff00);
1194
+ super(viewer);
1195
+ this.addPlaneY();
910
1196
  }
911
1197
  }
912
1198
 
913
1199
  class CuttingPlaneZAxisDragger extends CuttingPlaneDragger {
914
1200
  constructor(viewer) {
915
- super(viewer, new Vector3(0, 0, 1), 0x0000ff);
1201
+ super(viewer);
1202
+ this.addPlaneZ();
916
1203
  }
917
1204
  }
918
1205
 
@@ -989,120 +1276,6 @@ function formatDistance(distance, units, precision = 2) {
989
1276
  }
990
1277
  }
991
1278
 
992
- const DESKTOP_SNAP_DISTANCE = 10;
993
- const MOBILE_SNAP_DISTANCE = 50;
994
- const _vertex = new Vector3();
995
- const _start = new Vector3();
996
- const _end = new Vector3();
997
- const _line = new Line3();
998
- const _center = new Vector3();
999
- const _projection = new Vector3();
1000
- class Snapper {
1001
- constructor(camera, renderer, canvas) {
1002
- this.camera = camera;
1003
- this.renderer = renderer;
1004
- this.canvas = canvas;
1005
- this.threshold = 0.0001;
1006
- this.raycaster = new Raycaster();
1007
- this.detectRadiusInPixels = this.isMobile() ? MOBILE_SNAP_DISTANCE : DESKTOP_SNAP_DISTANCE;
1008
- this.edgesCache = new WeakMap();
1009
- }
1010
- isMobile() {
1011
- if (typeof navigator === "undefined")
1012
- return false;
1013
- return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
1014
- }
1015
- getMousePosition(event, target) {
1016
- return target.set(event.clientX, event.clientY);
1017
- }
1018
- getPointerIntersects(mouse, objects) {
1019
- const rect = this.canvas.getBoundingClientRect();
1020
- const x = ((mouse.x - rect.left) / rect.width) * 2 - 1;
1021
- const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1;
1022
- const coords = new Vector2(x, y);
1023
- this.raycaster.setFromCamera(coords, this.camera);
1024
- this.raycaster.params = {
1025
- Mesh: {},
1026
- Line: { threshold: this.threshold },
1027
- Line2: { threshold: this.threshold },
1028
- LOD: {},
1029
- Points: { threshold: this.threshold },
1030
- Sprite: {},
1031
- };
1032
- let intersects = this.raycaster.intersectObjects(objects, false);
1033
- (this.renderer.clippingPlanes || []).forEach((plane) => {
1034
- intersects = intersects.filter((intersect) => plane.distanceToPoint(intersect.point) >= 0);
1035
- });
1036
- return intersects;
1037
- }
1038
- getDetectRadius(point) {
1039
- const camera = this.camera;
1040
- if (camera.isOrthographicCamera) {
1041
- const fieldHeight = (camera.top - camera.bottom) / camera.zoom;
1042
- const canvasHeight = this.canvas.height;
1043
- const worldUnitsPerPixel = fieldHeight / canvasHeight;
1044
- return this.detectRadiusInPixels * worldUnitsPerPixel;
1045
- }
1046
- if (camera.isPerspectiveCamera) {
1047
- const distance = camera.position.distanceTo(point);
1048
- const fieldHeight = 2 * Math.tan(MathUtils.degToRad(camera.fov * 0.5)) * distance;
1049
- const canvasHeight = this.canvas.height;
1050
- const worldUnitsPerPixel = fieldHeight / canvasHeight;
1051
- return this.detectRadiusInPixels * worldUnitsPerPixel;
1052
- }
1053
- return 0.1;
1054
- }
1055
- getSnapPoint(mouse, objects) {
1056
- const intersections = this.getPointerIntersects(mouse, objects);
1057
- if (intersections.length === 0)
1058
- return undefined;
1059
- const object = intersections[0].object;
1060
- const intersectionPoint = intersections[0].point;
1061
- const localPoint = object.worldToLocal(intersectionPoint.clone());
1062
- let snapPoint;
1063
- let snapDistance = this.getDetectRadius(intersectionPoint);
1064
- const geometry = object.geometry;
1065
- const positions = geometry.attributes.position.array;
1066
- for (let i = 0; i < positions.length; i += 3) {
1067
- _vertex.set(positions[i], positions[i + 1], positions[i + 2]);
1068
- const distance = _vertex.distanceTo(localPoint);
1069
- if (distance < snapDistance) {
1070
- snapDistance = distance;
1071
- snapPoint = _vertex.clone();
1072
- }
1073
- }
1074
- if (snapPoint)
1075
- return object.localToWorld(snapPoint);
1076
- let edges = this.edgesCache.get(geometry);
1077
- if (!edges) {
1078
- edges = new EdgesGeometry(geometry);
1079
- this.edgesCache.set(geometry, edges);
1080
- }
1081
- const edgePositions = edges.attributes.position.array;
1082
- for (let i = 0; i < edgePositions.length; i += 6) {
1083
- _start.set(edgePositions[i], edgePositions[i + 1], edgePositions[i + 2]);
1084
- _end.set(edgePositions[i + 3], edgePositions[i + 4], edgePositions[i + 5]);
1085
- _line.set(_start, _end);
1086
- _line.getCenter(_center);
1087
- const centerDistance = _center.distanceTo(localPoint);
1088
- if (centerDistance < snapDistance) {
1089
- snapDistance = centerDistance;
1090
- snapPoint = _center.clone();
1091
- continue;
1092
- }
1093
- _line.closestPointToPoint(localPoint, true, _projection);
1094
- const lineDistance = _projection.distanceTo(localPoint);
1095
- if (lineDistance < snapDistance) {
1096
- snapDistance = lineDistance;
1097
- snapPoint = _projection.clone();
1098
- }
1099
- }
1100
- if (snapPoint)
1101
- return object.localToWorld(snapPoint);
1102
- return intersectionPoint.clone();
1103
- }
1104
- }
1105
-
1106
1279
  const _downPoint = new Vector2();
1107
1280
  class MeasureLineDragger extends OrbitDragger {
1108
1281
  constructor(viewer) {
@@ -1514,6 +1687,12 @@ class WalkControls extends Controls {
1514
1687
  this.movementSpeed = 0.1;
1515
1688
  this.multiplier = 3;
1516
1689
  this.groundFollowingSkippedFrames = 0;
1690
+ this.GROUND_BOX_HALF_SIZE = 20;
1691
+ this.GROUND_BOX_REFRESH_THRESHOLD = 0.3;
1692
+ this._groundObjectBoxes = new Map();
1693
+ this._activeGroundObjects = [];
1694
+ this._groundBox = new Box3();
1695
+ this._groundBoxCenter = new Vector3();
1517
1696
  this.moveWheel = 0;
1518
1697
  this.mouseDragOn = false;
1519
1698
  this._up = new Vector3();
@@ -1580,11 +1759,20 @@ class WalkControls extends Controls {
1580
1759
  }
1581
1760
  };
1582
1761
  this.onKeyUp = (event) => {
1583
- if (this.moveKeys.delete(event.code))
1762
+ if (this.moveKeys.delete(event.code)) {
1763
+ if (this.moveKeys.size === 0) {
1764
+ this._rebuildGroundBox(this.object.position);
1765
+ }
1584
1766
  this.update();
1767
+ }
1585
1768
  };
1586
1769
  this.camera = camera;
1587
1770
  this.groundObjects = groundObjects;
1771
+ for (const obj of groundObjects) {
1772
+ this._groundObjectBoxes.set(obj, new Box3().setFromObject(obj));
1773
+ }
1774
+ const pos = this.object.position;
1775
+ this._rebuildGroundBox(pos);
1588
1776
  this.raycaster = new Raycaster();
1589
1777
  this.raycaster.near = 0;
1590
1778
  this.raycaster.far = this.EYE_HEIGHT + this.FAILING_DISTANCE;
@@ -1619,10 +1807,29 @@ class WalkControls extends Controls {
1619
1807
  window.removeEventListener("keyup", this.onKeyUp);
1620
1808
  super.dispose();
1621
1809
  }
1810
+ _rebuildGroundBox(center) {
1811
+ const h = this.GROUND_BOX_HALF_SIZE;
1812
+ this._groundBoxCenter.copy(center);
1813
+ this._groundBox.set(new Vector3(center.x - h, center.y - h * 4, center.z - h), new Vector3(center.x + h, center.y + h * 4, center.z + h));
1814
+ this._activeGroundObjects = this.groundObjects.filter((obj) => {
1815
+ const objectBox = this._groundObjectBoxes.get(obj);
1816
+ return objectBox !== undefined && this._groundBox.intersectsBox(objectBox);
1817
+ });
1818
+ }
1819
+ _needsGroundBoxRebuild(pos) {
1820
+ if (this._activeGroundObjects.length === 0 && this.groundObjects.length > 0)
1821
+ return true;
1822
+ const threshold = this.GROUND_BOX_HALF_SIZE * this.GROUND_BOX_REFRESH_THRESHOLD;
1823
+ return (Math.abs(pos.x - this._groundBoxCenter.x) > threshold || Math.abs(pos.z - this._groundBoxCenter.z) > threshold);
1824
+ }
1622
1825
  updateGroundFollowing() {
1826
+ const pos = this.object.position;
1827
+ if (this._needsGroundBoxRebuild(pos)) {
1828
+ this._rebuildGroundBox(pos);
1829
+ }
1623
1830
  this._up.copy(this.camera.up).negate();
1624
- this.raycaster.set(this.object.position, this._up);
1625
- const intersects = this.raycaster.intersectObjects(this.groundObjects, false);
1831
+ this.raycaster.set(pos, this._up);
1832
+ const intersects = this.raycaster.intersectObjects(this._activeGroundObjects, false);
1626
1833
  if (intersects.length > 0) {
1627
1834
  const groundY = intersects[0].point.y;
1628
1835
  const targetY = groundY + this.EYE_HEIGHT;
@@ -2174,6 +2381,7 @@ draggers.registerDragger("Pan", (viewer) => new PanDragger(viewer));
2174
2381
  draggers.registerDragger("Orbit", (viewer) => new OrbitDragger(viewer));
2175
2382
  draggers.registerDragger("Zoom", (viewer) => new ZoomDragger(viewer));
2176
2383
  draggers.registerDragger("MeasureLine", (viewer) => new MeasureLineDragger(viewer));
2384
+ draggers.registerDragger("CuttingPlane", (viewer) => new CuttingPlaneDragger(viewer));
2177
2385
  draggers.registerDragger("CuttingPlaneXAxis", (viewer) => new CuttingPlaneXAxisDragger(viewer));
2178
2386
  draggers.registerDragger("CuttingPlaneYAxis", (viewer) => new CuttingPlaneYAxisDragger(viewer));
2179
2387
  draggers.registerDragger("CuttingPlaneZAxis", (viewer) => new CuttingPlaneZAxisDragger(viewer));
@@ -3069,7 +3277,7 @@ class SelectionComponent {
3069
3277
  this.getMousePosition(event, this.downPosition);
3070
3278
  };
3071
3279
  this.onPointerUp = (event) => {
3072
- if (!event.isPrimary)
3280
+ if (!event.isPrimary || event.button !== 0)
3073
3281
  return;
3074
3282
  const upPosition = this.getMousePosition(event, new Vector2());
3075
3283
  if (upPosition.distanceTo(this.downPosition) !== 0)
@@ -4383,6 +4591,81 @@ class DynamicGltfLoader {
4383
4591
  this.mergedObjectMap = new Map();
4384
4592
  this.mergedGeometryVisibility = new Map();
4385
4593
  this._webglInfoCache = null;
4594
+ this.transformTextureSize = 1024;
4595
+ this.transformTexture = this.createDummyTexture();
4596
+ this.transformData = null;
4597
+ this.identityTransformData = null;
4598
+ this.visibilityMaterials = new Set();
4599
+ }
4600
+ createDummyTexture() {
4601
+ const data = new Float32Array(16);
4602
+ const identity = new Matrix4();
4603
+ identity.toArray(data);
4604
+ const dummyData = new Float32Array(16);
4605
+ identity.toArray(dummyData);
4606
+ const dummyTexture = new DataTexture(dummyData, 4, 1, RGBAFormat, FloatType);
4607
+ dummyTexture.minFilter = NearestFilter;
4608
+ dummyTexture.magFilter = NearestFilter;
4609
+ dummyTexture.needsUpdate = true;
4610
+ return dummyTexture;
4611
+ }
4612
+ initTransformTexture() {
4613
+ if (this.transformTexture) {
4614
+ this.transformTexture.dispose();
4615
+ }
4616
+ const maxInstanceCount = this.maxObjectId + 1;
4617
+ let size = Math.sqrt(maxInstanceCount * 4);
4618
+ size = Math.ceil(size / 4) * 4;
4619
+ size = Math.max(size, 4);
4620
+ this.transformTextureSize = size;
4621
+ const arraySize = size * size * 4;
4622
+ this.transformData = new Float32Array(arraySize);
4623
+ this.identityTransformData = new Float32Array(arraySize);
4624
+ for (let i = 0; i <= this.maxObjectId; i++) {
4625
+ const base = i * 16;
4626
+ if (base + 15 < arraySize) {
4627
+ this.identityTransformData[base + 0] = 1;
4628
+ this.identityTransformData[base + 5] = 1;
4629
+ this.identityTransformData[base + 10] = 1;
4630
+ this.identityTransformData[base + 15] = 1;
4631
+ }
4632
+ }
4633
+ this._resetTransformData(false);
4634
+ this.transformTexture = new DataTexture(this.transformData, size, size, RGBAFormat, FloatType);
4635
+ this.transformTexture.needsUpdate = true;
4636
+ this.transformTexture.generateMipmaps = false;
4637
+ console.log(`Initialized transform texture: ${size}x${size} for ${maxInstanceCount} objects`);
4638
+ this.updateMaterialUniforms();
4639
+ this.visibilityMaterials.forEach((material) => {
4640
+ material.needsUpdate = true;
4641
+ });
4642
+ }
4643
+ _resetTransformData(updateTexture = true) {
4644
+ if (!this.transformData || !this.identityTransformData) return;
4645
+ this.transformData.set(this.identityTransformData);
4646
+ if (updateTexture) {
4647
+ this.updateTransformTexture();
4648
+ }
4649
+ }
4650
+ updateMaterialUniforms() {
4651
+ if (
4652
+ this._lastTransformTexture === this.transformTexture &&
4653
+ this._lastTransformTextureSize === this.transformTextureSize
4654
+ ) {
4655
+ return;
4656
+ }
4657
+ this._lastTransformTexture = this.transformTexture;
4658
+ this._lastTransformTextureSize = this.transformTextureSize;
4659
+ this.visibilityMaterials.forEach((material) => {
4660
+ if (material.userData && material.userData.visibilityUniforms) {
4661
+ material.userData.visibilityUniforms.transformTexture.value = this.transformTexture;
4662
+ material.userData.visibilityUniforms.transformTextureSize.value = this.transformTextureSize;
4663
+ }
4664
+ });
4665
+ }
4666
+ updateTransformTexture() {
4667
+ if (!this.transformTexture) return;
4668
+ this.transformTexture.needsUpdate = true;
4386
4669
  }
4387
4670
  setVisibleEdges(visible) {
4388
4671
  this.visibleEdges = visible;
@@ -5272,36 +5555,82 @@ class DynamicGltfLoader {
5272
5555
  }
5273
5556
  }
5274
5557
  createVisibilityMaterial(material) {
5558
+ this.visibilityMaterials.add(material);
5559
+ const uniforms = {
5560
+ transformTexture: { value: this.transformTexture },
5561
+ transformTextureSize: { value: this.transformTextureSize },
5562
+ };
5563
+ material.userData.visibilityUniforms = uniforms;
5275
5564
  material.onBeforeCompile = (shader) => {
5565
+ shader.uniforms.transformTexture = uniforms.transformTexture;
5566
+ shader.uniforms.transformTextureSize = uniforms.transformTextureSize;
5276
5567
  shader.vertexShader = shader.vertexShader.replace(
5277
5568
  "#include <common>",
5278
5569
  `
5279
5570
  #include <common>
5571
+
5280
5572
  attribute float visibility;
5573
+ attribute float objectId;
5281
5574
  varying float vVisibility;
5575
+ uniform highp sampler2D transformTexture;
5576
+ uniform float transformTextureSize;
5577
+
5578
+ mat4 getTransformMatrix(float instanceId) {
5579
+ int size = int(transformTextureSize);
5580
+ int index = int(instanceId) * 4;
5581
+
5582
+ int x0 = index % size;
5583
+ int y0 = index / size;
5584
+
5585
+ vec4 row0 = texelFetch(transformTexture, ivec2(x0, y0), 0);
5586
+ vec4 row1 = texelFetch(transformTexture, ivec2(x0 + 1, y0), 0);
5587
+ vec4 row2 = texelFetch(transformTexture, ivec2(x0 + 2, y0), 0);
5588
+ vec4 row3 = texelFetch(transformTexture, ivec2(x0 + 3, y0), 0);
5589
+
5590
+ return mat4(row0, row1, row2, row3);
5591
+ }
5282
5592
  `
5283
5593
  );
5284
- shader.fragmentShader = shader.fragmentShader.replace(
5285
- "#include <common>",
5594
+ shader.vertexShader = shader.vertexShader.replace(
5595
+ "void main() {",
5286
5596
  `
5287
- #include <common>
5288
- varying float vVisibility;
5597
+ void main() {
5598
+ mat4 batchingMatrix = getTransformMatrix(objectId);
5599
+ vVisibility = visibility;
5289
5600
  `
5290
5601
  );
5602
+ if (shader.vertexShader.includes("#include <beginnormal_vertex>")) {
5603
+ shader.vertexShader = shader.vertexShader.replace(
5604
+ "#include <beginnormal_vertex>",
5605
+ `
5606
+ vec3 objectNormal = vec3( normal );
5607
+ mat3 bm = mat3( batchingMatrix );
5608
+ objectNormal = bm * objectNormal;
5609
+ `
5610
+ );
5611
+ }
5291
5612
  shader.vertexShader = shader.vertexShader.replace(
5292
- "void main() {",
5613
+ "#include <begin_vertex>",
5293
5614
  `
5294
- void main() {
5295
- vVisibility = visibility;
5615
+ vec3 transformed = vec3( position );
5616
+ transformed = ( batchingMatrix * vec4( transformed, 1.0 ) ).xyz;
5296
5617
  `
5297
5618
  );
5298
- shader.fragmentShader = shader.fragmentShader.replace(
5299
- "void main() {",
5619
+ shader.fragmentShader = shader.fragmentShader
5620
+ .replace(
5621
+ "#include <common>",
5622
+ `
5623
+ #include <common>
5624
+ varying float vVisibility;
5300
5625
  `
5626
+ )
5627
+ .replace(
5628
+ "void main() {",
5629
+ `
5301
5630
  void main() {
5302
5631
  if (vVisibility < 0.5) discard;
5303
5632
  `
5304
- );
5633
+ );
5305
5634
  };
5306
5635
  material.needsUpdate = true;
5307
5636
  return material;
@@ -5416,6 +5745,8 @@ class DynamicGltfLoader {
5416
5745
  this.objectIdToIndex.clear();
5417
5746
  this.maxObjectId = 0;
5418
5747
  this.objectVisibility = new Float32Array();
5748
+ this.meshToNodeMap = null;
5749
+ this.visibilityMaterials.clear();
5419
5750
  }
5420
5751
  setStructureTransform(structureId, matrix) {
5421
5752
  const rootGroup = this.structureRoots.get(structureId);
@@ -5531,12 +5862,15 @@ class DynamicGltfLoader {
5531
5862
  });
5532
5863
  this.originalObjects.clear();
5533
5864
  this.originalObjectsToSelection.clear();
5865
+ this.objectIdToIndex.clear();
5866
+ this.maxObjectId = 0;
5534
5867
  const structureGroups = new Map();
5535
5868
  this.dispatchEvent("optimizationprogress", {
5536
5869
  phase: "collecting",
5537
5870
  progress: 5,
5538
5871
  message: "Collecting scene objects...",
5539
5872
  });
5873
+ let totalObjectsToMerge = 0;
5540
5874
  this.scene.traverse((object) => {
5541
5875
  if (object.userData.structureId) {
5542
5876
  const structureId = object.userData.structureId;
@@ -5554,17 +5888,32 @@ class DynamicGltfLoader {
5554
5888
  });
5555
5889
  }
5556
5890
  const group = structureGroups.get(structureId);
5891
+ let added = false;
5557
5892
  if (object instanceof Mesh) {
5558
5893
  this.addToMaterialGroup(object, group.mapMeshes, group.meshes);
5894
+ added = true;
5559
5895
  } else if (object instanceof LineSegments) {
5560
5896
  this.addToMaterialGroup(object, group.mapLineSegments, group.lineSegments);
5897
+ added = true;
5561
5898
  } else if (object instanceof Line) {
5562
5899
  this.addToMaterialGroup(object, group.mapLines, group.lines);
5900
+ added = true;
5563
5901
  } else if (object instanceof Points) {
5564
5902
  this.addToMaterialGroup(object, group.mapPoints, group.points);
5903
+ added = true;
5904
+ }
5905
+ if (added) {
5906
+ totalObjectsToMerge++;
5565
5907
  }
5566
5908
  }
5567
5909
  });
5910
+ if (totalObjectsToMerge > 0) {
5911
+ console.log(`Pre-allocating transform texture for ${totalObjectsToMerge} objects`);
5912
+ this.maxObjectId = totalObjectsToMerge;
5913
+ this.initTransformTexture();
5914
+ this.initializeObjectVisibility();
5915
+ this.maxObjectId = 0;
5916
+ }
5568
5917
  let processedGroups = 0;
5569
5918
  const totalGroups = structureGroups.size;
5570
5919
  this.dispatchEvent("optimizationprogress", {
@@ -5609,7 +5958,6 @@ class DynamicGltfLoader {
5609
5958
  this.originalObjectsToSelection.add(obj);
5610
5959
  }
5611
5960
  });
5612
- this.initializeObjectVisibility();
5613
5961
  console.log(`Optimization complete. Total objects: ${this.maxObjectId}`);
5614
5962
  this.dispatchEvent("optimizationprogress", {
5615
5963
  phase: "complete",
@@ -5678,6 +6026,7 @@ class DynamicGltfLoader {
5678
6026
  }
5679
6027
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
5680
6028
  const mergedMesh = new Mesh(mergedGeometry, visibilityMaterial);
6029
+ mergedMesh.frustumCulled = false;
5681
6030
  mergedMesh.userData.isOptimized = true;
5682
6031
  rootGroup.add(mergedMesh);
5683
6032
  this.mergedMesh.add(mergedMesh);
@@ -5794,6 +6143,7 @@ class DynamicGltfLoader {
5794
6143
  geometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));
5795
6144
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
5796
6145
  const mergedLine = new LineSegments(geometry, visibilityMaterial);
6146
+ mergedLine.frustumCulled = false;
5797
6147
  mergedLine.userData.isEdge = isEdge;
5798
6148
  mergedLine.userData.isOptimized = true;
5799
6149
  const mergedObjects = [mergedLine];
@@ -5882,6 +6232,7 @@ class DynamicGltfLoader {
5882
6232
  mergedGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));
5883
6233
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
5884
6234
  const mergedLine = new LineSegments(mergedGeometry, visibilityMaterial);
6235
+ mergedLine.frustumCulled = false;
5885
6236
  mergedLine.userData.isEdge = isEdge;
5886
6237
  mergedLine.userData.isOptimized = true;
5887
6238
  if (this.useVAO) {
@@ -5951,7 +6302,27 @@ class DynamicGltfLoader {
5951
6302
  const mergedObjects = [];
5952
6303
  if (geometries.length > 0) {
5953
6304
  const mergedGeometry = mergeGeometries(geometries, false);
5954
- const mergedPoints = new Points(mergedGeometry, group.material);
6305
+ const totalVertices = mergedGeometry.attributes.position.count;
6306
+ const objectIds = new Float32Array(totalVertices);
6307
+ let vertexOffset = 0;
6308
+ group.objects.forEach((points) => {
6309
+ const handle = points.userData.handle;
6310
+ if (!this.objectIdToIndex.has(handle)) {
6311
+ this.objectIdToIndex.set(handle, this.maxObjectId++);
6312
+ }
6313
+ const objectId = this.objectIdToIndex.get(handle);
6314
+ const count = points.geometry.attributes.position.count;
6315
+ for (let i = 0; i < count; i++) {
6316
+ objectIds[vertexOffset++] = objectId;
6317
+ }
6318
+ });
6319
+ mergedGeometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));
6320
+ const visibilityArray = new Float32Array(totalVertices);
6321
+ visibilityArray.fill(1.0);
6322
+ mergedGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));
6323
+ const visibilityMaterial = this.createVisibilityMaterial(group.material);
6324
+ const mergedPoints = new Points(mergedGeometry, visibilityMaterial);
6325
+ mergedPoints.frustumCulled = false;
5955
6326
  mergedPoints.userData.isOptimized = true;
5956
6327
  if (this.useVAO) {
5957
6328
  this.createVAO(mergedPoints);
@@ -6020,13 +6391,33 @@ class DynamicGltfLoader {
6020
6391
  geometriesWithIndex.push(clonedGeometry);
6021
6392
  });
6022
6393
  const finalGeometry = mergeGeometries(geometriesWithIndex, false);
6394
+ const totalVertices = finalGeometry.attributes.position.count;
6395
+ const objectIds = new Float32Array(totalVertices);
6396
+ let vertexOffset = 0;
6397
+ lineSegmentsArray.forEach((segment) => {
6398
+ const handle = segment.userData.handle;
6399
+ if (!this.objectIdToIndex.has(handle)) {
6400
+ this.objectIdToIndex.set(handle, this.maxObjectId++);
6401
+ }
6402
+ const objectId = this.objectIdToIndex.get(handle);
6403
+ const count = segment.geometry.attributes.position.count;
6404
+ for (let i = 0; i < count; i++) {
6405
+ objectIds[vertexOffset++] = objectId;
6406
+ }
6407
+ });
6408
+ finalGeometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));
6409
+ const visibilityArray = new Float32Array(totalVertices);
6410
+ visibilityArray.fill(1.0);
6411
+ finalGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));
6023
6412
  const material = new LineBasicMaterial({
6024
6413
  vertexColors: true,
6025
6414
  });
6415
+ const visibilityMaterial = this.createVisibilityMaterial(material);
6026
6416
  if (this.useVAO) {
6027
6417
  this.createVAO(finalGeometry);
6028
6418
  }
6029
- const mergedLine = new LineSegments(finalGeometry, material);
6419
+ const mergedLine = new LineSegments(finalGeometry, visibilityMaterial);
6420
+ mergedLine.frustumCulled = false;
6030
6421
  mergedLine.userData.structureId = structureId;
6031
6422
  mergedLine.userData.isOptimized = true;
6032
6423
  rootGroup.add(mergedLine);
@@ -6145,18 +6536,50 @@ class DynamicGltfLoader {
6145
6536
  console.warn("No merged objects to transform");
6146
6537
  return;
6147
6538
  }
6148
- this.objectTransforms = new Map(objectTransformMap);
6149
- for (const mesh of this.mergedMesh) {
6150
- this._applyTransformToMergedObject(mesh);
6151
- }
6152
- for (const line of this.mergedLines) {
6153
- this._applyTransformToMergedObject(line);
6154
- }
6155
- for (const lineSegment of this.mergedLineSegments) {
6156
- this._applyTransformToMergedObject(lineSegment);
6539
+ if (!this.transformData) {
6540
+ console.warn("Transform texture not initialized");
6541
+ return;
6157
6542
  }
6158
- for (const point of this.mergedPoints) {
6159
- this._applyTransformToMergedObject(point);
6543
+ this.objectTransforms = objectTransformMap;
6544
+ this._resetTransformData(false);
6545
+ const transformData = this.transformData;
6546
+ const objectIdToIndex = this.objectIdToIndex;
6547
+ let textureNeedsUpdate = false;
6548
+ if (objectTransformMap instanceof Map) {
6549
+ for (const [object, matrix] of objectTransformMap.entries()) {
6550
+ const userData = object.userData;
6551
+ if (!userData) continue;
6552
+ const handle = userData.handle;
6553
+ if (handle === undefined) continue;
6554
+ const objectId = objectIdToIndex.get(handle);
6555
+ if (objectId !== undefined) {
6556
+ transformData.set(matrix.elements, objectId * 16);
6557
+ textureNeedsUpdate = true;
6558
+ }
6559
+ }
6560
+ } else {
6561
+ const len = objectTransformMap.length;
6562
+ for (let i = 0; i < len; i++) {
6563
+ const pair = objectTransformMap[i];
6564
+ const userData = pair[0].userData;
6565
+ if (!userData) continue;
6566
+ const handle = userData.handle;
6567
+ if (handle === undefined) continue;
6568
+ const objectId = objectIdToIndex.get(handle);
6569
+ if (objectId !== undefined) {
6570
+ transformData.set(pair[1].elements, objectId * 16);
6571
+ textureNeedsUpdate = true;
6572
+ }
6573
+ }
6574
+ }
6575
+ if (textureNeedsUpdate) {
6576
+ this.updateTransformTexture();
6577
+ if (
6578
+ this._lastTransformTexture !== this.transformTexture ||
6579
+ this._lastTransformTextureSize !== this.transformTextureSize
6580
+ ) {
6581
+ this.updateMaterialUniforms();
6582
+ }
6160
6583
  }
6161
6584
  }
6162
6585
  createExplodeTransforms(objects = null, explodeCenter = null, explodeFactor = 1.5) {
@@ -6173,21 +6596,66 @@ class DynamicGltfLoader {
6173
6596
  ? objects
6174
6597
  : Array.from(objects)
6175
6598
  : Array.from(this.originalObjects);
6599
+ const structureInverseMatrices = new Map();
6600
+ if (!this.meshToNodeMap) {
6601
+ this.meshToNodeMap = new Map();
6602
+ for (const node of this.nodes.values()) {
6603
+ if (node.object) {
6604
+ this.meshToNodeMap.set(node.object, node);
6605
+ }
6606
+ }
6607
+ }
6176
6608
  for (const obj of objectsArray) {
6177
6609
  if (!obj.geometry || !obj.geometry.attributes.position) continue;
6178
- const boundingBox = new Box3().setFromBufferAttribute(obj.geometry.attributes.position);
6179
- if (obj.matrixWorld) {
6180
- boundingBox.applyMatrix4(obj.matrixWorld);
6181
- }
6182
- if (boundingBox.isEmpty()) continue;
6183
- const objectCenter = new Vector3();
6184
- boundingBox.getCenter(objectCenter);
6185
- const direction = objectCenter.clone().sub(explodeCenter);
6186
- const distance = direction.length();
6610
+ if (!obj.userData.explodeVector) {
6611
+ let center = null;
6612
+ const node = this.meshToNodeMap.get(obj);
6613
+ if (node && node.geometryExtents) {
6614
+ const box = node.geometryExtents.clone();
6615
+ box.applyMatrix4(obj.matrixWorld);
6616
+ center = new Vector3();
6617
+ box.getCenter(center);
6618
+ }
6619
+ if (!center) {
6620
+ if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
6621
+ const box = obj.geometry.boundingBox.clone();
6622
+ box.applyMatrix4(obj.matrixWorld);
6623
+ center = new Vector3();
6624
+ box.getCenter(center);
6625
+ }
6626
+ const explodeVector = center.sub(explodeCenter);
6627
+ obj.userData.explodeVector = explodeVector;
6628
+ }
6629
+ const explodeVector = obj.userData.explodeVector;
6630
+ const distance = explodeVector.length();
6187
6631
  if (distance > 0) {
6188
- direction.normalize();
6189
- const offset = direction.multiplyScalar(distance * (explodeFactor - 1.0));
6190
- const matrix = new Matrix4().makeTranslation(offset.x, offset.y, offset.z);
6632
+ const offset = explodeVector.clone().multiplyScalar(explodeFactor - 1.0);
6633
+ const localOffset = offset.clone();
6634
+ if (obj.userData.structureId) {
6635
+ const structureId = obj.userData.structureId;
6636
+ let inverseMatrix = structureInverseMatrices.get(structureId);
6637
+ if (!inverseMatrix) {
6638
+ const rootGroup = this.structureRoots.get(structureId);
6639
+ if (rootGroup) {
6640
+ if (!rootGroup.userData.inverseWorldMatrix) {
6641
+ rootGroup.userData.inverseWorldMatrix = new Matrix4().copy(rootGroup.matrixWorld).invert();
6642
+ }
6643
+ inverseMatrix = rootGroup.userData.inverseWorldMatrix;
6644
+ structureInverseMatrices.set(structureId, inverseMatrix);
6645
+ }
6646
+ }
6647
+ if (inverseMatrix) {
6648
+ const zero = new Vector3(0, 0, 0).applyMatrix4(inverseMatrix);
6649
+ const vec = offset.clone().applyMatrix4(inverseMatrix).sub(zero);
6650
+ localOffset.copy(vec);
6651
+ }
6652
+ }
6653
+ let matrix = obj.userData.explodeMatrix;
6654
+ if (!matrix) {
6655
+ matrix = new Matrix4();
6656
+ obj.userData.explodeMatrix = matrix;
6657
+ }
6658
+ matrix.makeTranslation(localOffset.x, localOffset.y, localOffset.z);
6191
6659
  transformMap.set(obj, matrix);
6192
6660
  }
6193
6661
  }
@@ -6195,116 +6663,11 @@ class DynamicGltfLoader {
6195
6663
  }
6196
6664
  clearTransforms() {
6197
6665
  this.objectTransforms.clear();
6198
- for (const mesh of this.mergedMesh) {
6199
- this._restoreOriginalGeometry(mesh);
6200
- }
6201
- for (const line of this.mergedLines) {
6202
- this._restoreOriginalGeometry(line);
6203
- }
6204
- for (const lineSegment of this.mergedLineSegments) {
6205
- this._restoreOriginalGeometry(lineSegment);
6206
- }
6207
- for (const point of this.mergedPoints) {
6208
- this._restoreOriginalGeometry(point);
6209
- }
6666
+ this._resetTransformData(true);
6210
6667
  }
6211
6668
  clearHandleTransforms() {
6212
6669
  this.clearTransforms();
6213
6670
  }
6214
- _applyTransformToMergedObject(mergedObject) {
6215
- const objectData = this.mergedObjectMap.get(mergedObject.uuid);
6216
- if (!objectData || !objectData.objectMapping) return;
6217
- const geometry = mergedObject.geometry;
6218
- if (!geometry || !geometry.attributes.position) return;
6219
- const positionAttr = geometry.attributes.position;
6220
- const positions = positionAttr.array;
6221
- if (!this.transformedGeometries.has(mergedObject.uuid)) {
6222
- this.transformedGeometries.set(mergedObject.uuid, new Float32Array(positions));
6223
- }
6224
- const originalPositions = this.transformedGeometries.get(mergedObject.uuid);
6225
- const tempVector = new Vector3();
6226
- for (const [originalMesh, mappingData] of objectData.objectMapping) {
6227
- const transform = this.objectTransforms.get(originalMesh);
6228
- if (!transform) {
6229
- const startIdx = mappingData.startVertexIndex * 3;
6230
- const endIdx = (mappingData.startVertexIndex + mappingData.vertexCount) * 3;
6231
- for (let i = startIdx; i < endIdx; i++) {
6232
- positions[i] = originalPositions[i];
6233
- }
6234
- continue;
6235
- }
6236
- const startVertex = mappingData.startVertexIndex;
6237
- const vertexCount = mappingData.vertexCount;
6238
- for (let i = 0; i < vertexCount; i++) {
6239
- const idx = (startVertex + i) * 3;
6240
- tempVector.set(originalPositions[idx], originalPositions[idx + 1], originalPositions[idx + 2]);
6241
- tempVector.applyMatrix4(transform);
6242
- positions[idx] = tempVector.x;
6243
- positions[idx + 1] = tempVector.y;
6244
- positions[idx + 2] = tempVector.z;
6245
- }
6246
- }
6247
- if (geometry.attributes.normal) {
6248
- this._updateNormalsForTransform(geometry, objectData, originalPositions);
6249
- }
6250
- positionAttr.needsUpdate = true;
6251
- geometry.computeBoundingSphere();
6252
- geometry.computeBoundingBox();
6253
- }
6254
- _updateNormalsForTransform(geometry, objectData, originalPositions) {
6255
- const normalAttr = geometry.attributes.normal;
6256
- if (!normalAttr) return;
6257
- const normals = normalAttr.array;
6258
- const tempVector = new Vector3();
6259
- const normalMatrix = new Matrix4();
6260
- const normalsKey = `${geometry.uuid}_normals`;
6261
- if (!this.transformedGeometries.has(normalsKey)) {
6262
- this.transformedGeometries.set(normalsKey, new Float32Array(normals));
6263
- }
6264
- const originalNormals = this.transformedGeometries.get(normalsKey);
6265
- for (const [originalMesh, mappingData] of objectData.objectMapping) {
6266
- const transform = this.objectTransforms.get(originalMesh);
6267
- if (!transform) {
6268
- const startIdx = mappingData.startVertexIndex * 3;
6269
- const endIdx = (mappingData.startVertexIndex + mappingData.vertexCount) * 3;
6270
- for (let i = startIdx; i < endIdx; i++) {
6271
- normals[i] = originalNormals[i];
6272
- }
6273
- continue;
6274
- }
6275
- normalMatrix.copy(transform).invert().transpose();
6276
- const startVertex = mappingData.startVertexIndex;
6277
- const vertexCount = mappingData.vertexCount;
6278
- for (let i = 0; i < vertexCount; i++) {
6279
- const idx = (startVertex + i) * 3;
6280
- tempVector.set(originalNormals[idx], originalNormals[idx + 1], originalNormals[idx + 2]);
6281
- tempVector.applyMatrix4(normalMatrix).normalize();
6282
- normals[idx] = tempVector.x;
6283
- normals[idx + 1] = tempVector.y;
6284
- normals[idx + 2] = tempVector.z;
6285
- }
6286
- }
6287
- normalAttr.needsUpdate = true;
6288
- }
6289
- _restoreOriginalGeometry(mergedObject) {
6290
- const geometry = mergedObject.geometry;
6291
- if (!geometry || !geometry.attributes.position) return;
6292
- const originalPositions = this.transformedGeometries.get(mergedObject.uuid);
6293
- if (originalPositions) {
6294
- const positions = geometry.attributes.position.array;
6295
- positions.set(originalPositions);
6296
- geometry.attributes.position.needsUpdate = true;
6297
- }
6298
- const normalsKey = `${geometry.uuid}_normals`;
6299
- const originalNormals = this.transformedGeometries.get(normalsKey);
6300
- if (originalNormals && geometry.attributes.normal) {
6301
- const normals = geometry.attributes.normal.array;
6302
- normals.set(originalNormals);
6303
- geometry.attributes.normal.needsUpdate = true;
6304
- }
6305
- geometry.computeBoundingSphere();
6306
- geometry.computeBoundingBox();
6307
- }
6308
6671
  syncHiddenObjects() {
6309
6672
  if (this.mergedObjectMap.size === 0) {
6310
6673
  console.log("No merged objects to sync");
@@ -7186,12 +7549,14 @@ class Viewer extends EventEmitter2 {
7186
7549
  if (!this.renderer)
7187
7550
  return;
7188
7551
  this._markup.clearOverlay();
7552
+ this.emitEvent({ type: "clearoverlay" });
7189
7553
  this.update();
7190
7554
  }
7191
7555
  clearSlices() {
7192
7556
  if (!this.renderer)
7193
7557
  return;
7194
7558
  this.renderer.clippingPlanes = [];
7559
+ this.emitEvent({ type: "clearslices" });
7195
7560
  this.update();
7196
7561
  }
7197
7562
  getSelected() {