@selvajs/visualization 1.6.0-beta.10 → 1.6.0-beta.11
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.
- package/dist/render.cjs +409 -128
- package/dist/render.cjs.map +1 -1
- package/dist/render.d.cts +9 -6
- package/dist/render.d.ts +9 -6
- package/dist/render.js +409 -128
- package/dist/render.js.map +1 -1
- package/package.json +3 -2
package/dist/render.js
CHANGED
|
@@ -8,6 +8,7 @@ import { LineMaterial } from "three/addons/lines/LineMaterial.js";
|
|
|
8
8
|
import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
|
|
9
9
|
import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
|
|
10
10
|
import { CSS2DObject, CSS2DRenderer } from "three/addons/renderers/CSS2DRenderer.js";
|
|
11
|
+
import { MeshBVH } from "three-mesh-bvh";
|
|
11
12
|
import { ViewHelper } from "three/addons/helpers/ViewHelper.js";
|
|
12
13
|
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
|
13
14
|
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
|
@@ -1793,10 +1794,143 @@ function createLabelLayer(container, scene) {
|
|
|
1793
1794
|
}
|
|
1794
1795
|
};
|
|
1795
1796
|
}
|
|
1797
|
+
// src/render/scene-pick.ts
|
|
1798
|
+
const bvhCache = /* @__PURE__ */ new WeakMap();
|
|
1799
|
+
function versionOf$1(attribute) {
|
|
1800
|
+
return attribute?.version ?? 0;
|
|
1801
|
+
}
|
|
1802
|
+
/** BVH for a mesh geometry, building it on first use. Null when the geometry can't take one. */
|
|
1803
|
+
function bvhFor(geometry) {
|
|
1804
|
+
const position = geometry.attributes.position;
|
|
1805
|
+
if (!position || position.count === 0) return null;
|
|
1806
|
+
const index = geometry.index;
|
|
1807
|
+
const key = index ?? position;
|
|
1808
|
+
const cached = bvhCache.get(key);
|
|
1809
|
+
if (cached === null) return null;
|
|
1810
|
+
if (cached && cached.position === position && cached.positionVersion === versionOf$1(position) && cached.indexVersion === versionOf$1(index)) return cached.bvh;
|
|
1811
|
+
const proxy = new THREE.BufferGeometry();
|
|
1812
|
+
proxy.setAttribute("position", position);
|
|
1813
|
+
if (index) proxy.setIndex(index);
|
|
1814
|
+
try {
|
|
1815
|
+
const bvh = new MeshBVH(proxy, { indirect: true });
|
|
1816
|
+
bvhCache.set(key, {
|
|
1817
|
+
position,
|
|
1818
|
+
positionVersion: versionOf$1(position),
|
|
1819
|
+
indexVersion: versionOf$1(index),
|
|
1820
|
+
bvh
|
|
1821
|
+
});
|
|
1822
|
+
return bvh;
|
|
1823
|
+
} catch {
|
|
1824
|
+
bvhCache.set(key, null);
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
function isBvhMesh(object) {
|
|
1829
|
+
const o = object;
|
|
1830
|
+
return o.isMesh === true && !o.isInstancedMesh && !o.isSkinnedMesh && !o.isBatchedMesh && !o.isLineSegments2 && !object.morphTargetInfluences?.length;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Visible, pickable objects under `root`: hidden subtrees, viewer aids and edge overlays are
|
|
1834
|
+
* skipped. Overlays are skipped because they double the mesh's own edges and, as fat lines, cost a
|
|
1835
|
+
* linear scan of every segment per ray.
|
|
1836
|
+
*/
|
|
1837
|
+
function collectPickable(root, out = []) {
|
|
1838
|
+
for (const child of root.children) {
|
|
1839
|
+
if (!child.visible || isViewerAidRoot(child) || isEdgeOverlay(child)) continue;
|
|
1840
|
+
out.push(child);
|
|
1841
|
+
collectPickable(child, out);
|
|
1842
|
+
}
|
|
1843
|
+
return out;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Index-buffer ranges a merged mesh currently draws, or null when it draws everything. Hidden
|
|
1847
|
+
* members are the gaps between groups (`shared/merged-draw.ts`).
|
|
1848
|
+
*/
|
|
1849
|
+
function drawnRanges(mesh) {
|
|
1850
|
+
const { groups } = mesh.geometry;
|
|
1851
|
+
if (!Array.isArray(mesh.material) || groups.length === 0) return null;
|
|
1852
|
+
const materials = mesh.material;
|
|
1853
|
+
return groups.filter((g) => materials[g.materialIndex ?? 0]?.visible !== false).map((g) => ({
|
|
1854
|
+
start: g.start,
|
|
1855
|
+
end: g.start + g.count
|
|
1856
|
+
}));
|
|
1857
|
+
}
|
|
1858
|
+
const _inverse = new THREE.Matrix4();
|
|
1859
|
+
const _localRay = new THREE.Ray();
|
|
1860
|
+
const _sphere = new THREE.Sphere();
|
|
1861
|
+
function raycastMeshBvh(mesh, bvh, raycaster) {
|
|
1862
|
+
_inverse.copy(mesh.matrixWorld).invert();
|
|
1863
|
+
_localRay.copy(raycaster.ray).applyMatrix4(_inverse);
|
|
1864
|
+
const ranges = drawnRanges(mesh);
|
|
1865
|
+
let best = null;
|
|
1866
|
+
if (!ranges) best = bvh.raycastFirst(_localRay, THREE.DoubleSide);
|
|
1867
|
+
else for (const hit of bvh.raycast(_localRay, THREE.DoubleSide)) {
|
|
1868
|
+
const at = (hit.faceIndex ?? 0) * 3;
|
|
1869
|
+
if (!ranges.some((r) => at >= r.start && at < r.end)) continue;
|
|
1870
|
+
if (!best || hit.distance < best.distance) best = hit;
|
|
1871
|
+
}
|
|
1872
|
+
if (!best) return null;
|
|
1873
|
+
best.point.applyMatrix4(mesh.matrixWorld);
|
|
1874
|
+
best.distance = best.point.distanceTo(raycaster.ray.origin);
|
|
1875
|
+
best.object = mesh;
|
|
1876
|
+
if (best.distance < raycaster.near || best.distance > raycaster.far) return null;
|
|
1877
|
+
return best;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Every hit along `raycaster`'s ray over the visible content of `scene`, nearest first. Meshes use
|
|
1881
|
+
* the BVH; lines, points and instanced meshes fall back to their own `raycast`.
|
|
1882
|
+
*/
|
|
1883
|
+
function raycastVisible(scene, raycaster) {
|
|
1884
|
+
const hits = [];
|
|
1885
|
+
for (const object of collectPickable(scene)) {
|
|
1886
|
+
if (!object.layers.test(raycaster.layers)) continue;
|
|
1887
|
+
if (isBvhMesh(object)) {
|
|
1888
|
+
const { geometry } = object;
|
|
1889
|
+
if (!geometry.boundingSphere) geometry.computeBoundingSphere();
|
|
1890
|
+
if (!geometry.boundingSphere) continue;
|
|
1891
|
+
_sphere.copy(geometry.boundingSphere).applyMatrix4(object.matrixWorld);
|
|
1892
|
+
if (!raycaster.ray.intersectsSphere(_sphere)) continue;
|
|
1893
|
+
const bvh = bvhFor(geometry);
|
|
1894
|
+
if (bvh) {
|
|
1895
|
+
const hit = raycastMeshBvh(object, bvh, raycaster);
|
|
1896
|
+
if (hit) hits.push(hit);
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
object.raycast(raycaster, hits);
|
|
1901
|
+
}
|
|
1902
|
+
return hits.sort((a, b) => a.distance - b.distance);
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Builds BVHs for the visible meshes under `root` one per idle slice, so the first hover over a
|
|
1906
|
+
* large model doesn't pay for all of them at once. Returns a cancel function.
|
|
1907
|
+
*/
|
|
1908
|
+
function warmPickIndex(root) {
|
|
1909
|
+
const queue = collectPickable(root).filter(isBvhMesh);
|
|
1910
|
+
const idle = typeof requestIdleCallback === "function" ? (fn) => requestIdleCallback(fn, { timeout: 500 }) : (fn) => setTimeout(fn, 16);
|
|
1911
|
+
const cancelIdle = typeof cancelIdleCallback === "function" ? cancelIdleCallback : (id) => clearTimeout(id);
|
|
1912
|
+
let handle = 0;
|
|
1913
|
+
let cancelled = false;
|
|
1914
|
+
const step = () => {
|
|
1915
|
+
if (cancelled) return;
|
|
1916
|
+
const mesh = queue.shift();
|
|
1917
|
+
if (!mesh) return;
|
|
1918
|
+
bvhFor(mesh.geometry);
|
|
1919
|
+
handle = idle(step);
|
|
1920
|
+
};
|
|
1921
|
+
handle = idle(step);
|
|
1922
|
+
return () => {
|
|
1923
|
+
cancelled = true;
|
|
1924
|
+
cancelIdle(handle);
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1796
1927
|
// src/render/measure.ts
|
|
1797
1928
|
const DEFAULT_SNAP_PIXELS = 12;
|
|
1798
1929
|
const DEFAULT_COLOR = 16763904;
|
|
1799
1930
|
const LINE_PICK_FRACTION = .015;
|
|
1931
|
+
const RENDER_ORDER_LINE = 998;
|
|
1932
|
+
const RENDER_ORDER_MARKER = 999;
|
|
1933
|
+
const RENDER_ORDER_HOVER = 1e3;
|
|
1800
1934
|
const UNIT_DISPLAY = {
|
|
1801
1935
|
Millimeters: {
|
|
1802
1936
|
metersPerUnit: 1 / 1e3,
|
|
@@ -1819,10 +1953,33 @@ const UNIT_DISPLAY = {
|
|
|
1819
1953
|
suffix: "ft"
|
|
1820
1954
|
}
|
|
1821
1955
|
};
|
|
1822
|
-
/**
|
|
1956
|
+
/**
|
|
1957
|
+
* Three significant figures below 1000, whole units above. `toPrecision(3)` alone switches to
|
|
1958
|
+
* exponent notation past 999 ("1.23e+4 mm"), which is unreadable on a building-scale model.
|
|
1959
|
+
*
|
|
1960
|
+
* @internal exported for tests
|
|
1961
|
+
*/
|
|
1823
1962
|
function makeFormatter(displayUnit) {
|
|
1824
1963
|
const unit = displayUnit && UNIT_DISPLAY[displayUnit] || UNIT_DISPLAY.Meters;
|
|
1825
|
-
return (meters) =>
|
|
1964
|
+
return (meters) => {
|
|
1965
|
+
const value = meters / unit.metersPerUnit;
|
|
1966
|
+
const abs = Math.abs(value);
|
|
1967
|
+
const decimals = abs === 0 ? 0 : Math.min(6, Math.max(0, 2 - Math.floor(Math.log10(abs))));
|
|
1968
|
+
return `${value.toFixed(decimals)} ${unit.suffix}`;
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
/** @internal exported for tests */
|
|
1972
|
+
function makeDefaultFormat(fmt) {
|
|
1973
|
+
return (distance, delta) => {
|
|
1974
|
+
const eps = distance * 1e-6;
|
|
1975
|
+
const axes = [
|
|
1976
|
+
"x",
|
|
1977
|
+
"y",
|
|
1978
|
+
"z"
|
|
1979
|
+
].filter((axis) => delta[axis] > eps);
|
|
1980
|
+
if (axes.length < 2) return fmt(distance);
|
|
1981
|
+
return `${fmt(distance)}\n${axes.map((axis) => `Δ${axis} ${fmt(delta[axis])}`).join(" ")}`;
|
|
1982
|
+
};
|
|
1826
1983
|
}
|
|
1827
1984
|
/**
|
|
1828
1985
|
* Raycast threshold for picking lines/points, as a fixed fraction of view size so the grab band
|
|
@@ -1840,138 +1997,129 @@ function pickThreshold(camera, viewTarget) {
|
|
|
1840
1997
|
return ((viewTarget ? camera.position.distanceTo(viewTarget) : camera.position.length()) || 1) * LINE_PICK_FRACTION;
|
|
1841
1998
|
}
|
|
1842
1999
|
/**
|
|
1843
|
-
*
|
|
1844
|
-
* Line/LineSegments → struck segment's 2 endpoints; Points →
|
|
1845
|
-
* carries no usable index
|
|
2000
|
+
* Local-space vertices to consider snapping to, by object type: Mesh → struck triangle's 3
|
|
2001
|
+
* vertices; Line/LineSegments and fat Line2/LineSegments2 → struck segment's 2 endpoints; Points →
|
|
2002
|
+
* the struck vertex. Null when the hit carries no usable index, so the caller keeps the raw point.
|
|
1846
2003
|
*/
|
|
1847
|
-
function
|
|
2004
|
+
function snapCandidates(hit) {
|
|
1848
2005
|
const obj = hit.object;
|
|
1849
|
-
|
|
2006
|
+
const geometry = obj.geometry;
|
|
2007
|
+
if (!geometry) return null;
|
|
2008
|
+
if (obj.isLineSegments2) {
|
|
2009
|
+
const start = geometry.attributes.instanceStart;
|
|
2010
|
+
const end = geometry.attributes.instanceEnd;
|
|
2011
|
+
const i = hit.faceIndex;
|
|
2012
|
+
if (!start || !end || i == null || i >= start.count) return null;
|
|
2013
|
+
return [new THREE.Vector3().fromBufferAttribute(start, i), new THREE.Vector3().fromBufferAttribute(end, i)];
|
|
2014
|
+
}
|
|
2015
|
+
const pos = geometry.attributes.position;
|
|
2016
|
+
if (!pos) return null;
|
|
2017
|
+
const at = (indices) => indices.filter((i) => i < pos.count).map((i) => new THREE.Vector3().fromBufferAttribute(pos, i));
|
|
2018
|
+
if (obj instanceof THREE.Mesh) return hit.face ? at([
|
|
1850
2019
|
hit.face.a,
|
|
1851
2020
|
hit.face.b,
|
|
1852
2021
|
hit.face.c
|
|
1853
|
-
] : null;
|
|
1854
|
-
if (obj instanceof THREE.Points) return hit.index != null ? [hit.index] : null;
|
|
2022
|
+
]) : null;
|
|
2023
|
+
if (obj instanceof THREE.Points) return hit.index != null ? at([hit.index]) : null;
|
|
1855
2024
|
if (obj instanceof THREE.Line) {
|
|
1856
2025
|
if (hit.index == null) return null;
|
|
1857
|
-
const index =
|
|
2026
|
+
const index = geometry.index;
|
|
1858
2027
|
if (index) {
|
|
1859
2028
|
if (hit.index + 1 >= index.count) return null;
|
|
1860
|
-
return [index.getX(hit.index), index.getX(hit.index + 1)];
|
|
2029
|
+
return at([index.getX(hit.index), index.getX(hit.index + 1)]);
|
|
1861
2030
|
}
|
|
1862
|
-
return [hit.index, hit.index + 1];
|
|
2031
|
+
return at([hit.index, hit.index + 1]);
|
|
1863
2032
|
}
|
|
1864
2033
|
return null;
|
|
1865
2034
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2035
|
+
function toScreen(world, camera, screenSize) {
|
|
2036
|
+
const ndc = world.clone().project(camera);
|
|
2037
|
+
return new THREE.Vector2((ndc.x + 1) / 2 * screenSize.width, (1 - ndc.y) / 2 * screenSize.height);
|
|
2038
|
+
}
|
|
2039
|
+
/** Where a hit sits on screen: fat lines report the nearest point on the line separately. */
|
|
2040
|
+
function hitPoint(hit) {
|
|
2041
|
+
return hit.pointOnLine ?? hit.point;
|
|
2042
|
+
}
|
|
2043
|
+
function snapHit(hit, camera, screenSize, snapPixels) {
|
|
2044
|
+
const raw = hitPoint(hit).clone();
|
|
2045
|
+
const candidates = snapCandidates(hit);
|
|
2046
|
+
if (!candidates) return {
|
|
2047
|
+
point: raw,
|
|
2048
|
+
snapped: false
|
|
2049
|
+
};
|
|
2050
|
+
const rawScreen = toScreen(raw, camera, screenSize);
|
|
2051
|
+
let best = {
|
|
2052
|
+
point: raw,
|
|
2053
|
+
snapped: false
|
|
2054
|
+
};
|
|
1880
2055
|
let bestPx = snapPixels;
|
|
1881
|
-
for (const
|
|
1882
|
-
|
|
1883
|
-
const
|
|
1884
|
-
const px = toScreen(world).distanceTo(rawScreen);
|
|
2056
|
+
for (const local of candidates) {
|
|
2057
|
+
const world = local.applyMatrix4(hit.object.matrixWorld);
|
|
2058
|
+
const px = toScreen(world, camera, screenSize).distanceTo(rawScreen);
|
|
1885
2059
|
if (px < bestPx) {
|
|
1886
2060
|
bestPx = px;
|
|
1887
|
-
best =
|
|
2061
|
+
best = {
|
|
2062
|
+
point: world,
|
|
2063
|
+
snapped: true
|
|
2064
|
+
};
|
|
1888
2065
|
}
|
|
1889
2066
|
}
|
|
1890
2067
|
return best;
|
|
1891
2068
|
}
|
|
2069
|
+
/** Snap a raycast hit to the nearest geometry vertex within `snapPixels` on screen, else the raw hit point. */
|
|
2070
|
+
function snapToVertex(hit, camera, screenSize, snapPixels) {
|
|
2071
|
+
return snapHit(hit, camera, screenSize, snapPixels).point;
|
|
2072
|
+
}
|
|
1892
2073
|
function createMeasureTool(deps) {
|
|
1893
2074
|
const { canvas, scene, getActiveCamera, getViewTarget, labelLayer, options = {} } = deps;
|
|
2075
|
+
const requestRender = deps.requestRender ?? (() => {});
|
|
1894
2076
|
const snapPixels = options.snapPixels ?? DEFAULT_SNAP_PIXELS;
|
|
1895
2077
|
const color = new THREE.Color(options.color ?? DEFAULT_COLOR);
|
|
1896
|
-
const
|
|
1897
|
-
const defaultFormat = (d, delta) => `${fmt(d)}\nΔx ${fmt(delta.x)} Δy ${fmt(delta.y)} Δz ${fmt(delta.z)}`;
|
|
1898
|
-
const format = options.format ?? defaultFormat;
|
|
2078
|
+
const format = options.format ?? makeDefaultFormat(makeFormatter(options.displayUnit));
|
|
1899
2079
|
const raycaster = new THREE.Raycaster();
|
|
1900
2080
|
const pointer = new THREE.Vector2();
|
|
1901
2081
|
let enabled = false;
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
let line = null;
|
|
1905
|
-
let label = null;
|
|
2082
|
+
let start = null;
|
|
2083
|
+
const measurements = [];
|
|
1906
2084
|
const markerMaterial = new THREE.PointsMaterial({
|
|
1907
2085
|
color,
|
|
1908
2086
|
size: 8,
|
|
1909
2087
|
sizeAttenuation: false,
|
|
1910
2088
|
depthTest: false
|
|
1911
2089
|
});
|
|
1912
|
-
const
|
|
2090
|
+
const hoverSnappedMaterial = new THREE.PointsMaterial({
|
|
1913
2091
|
color,
|
|
1914
|
-
size:
|
|
2092
|
+
size: 12,
|
|
2093
|
+
sizeAttenuation: false,
|
|
2094
|
+
depthTest: false
|
|
2095
|
+
});
|
|
2096
|
+
const hoverFreeMaterial = new THREE.PointsMaterial({
|
|
2097
|
+
color,
|
|
2098
|
+
size: 7,
|
|
1915
2099
|
sizeAttenuation: false,
|
|
1916
2100
|
depthTest: false,
|
|
1917
2101
|
transparent: true,
|
|
1918
|
-
opacity: .
|
|
2102
|
+
opacity: .6
|
|
1919
2103
|
});
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
geometry.setAttribute("position", new THREE.Float32BufferAttribute([
|
|
1929
|
-
0,
|
|
1930
|
-
0,
|
|
1931
|
-
0
|
|
1932
|
-
], 3));
|
|
1933
|
-
hoverMarker = new THREE.Points(geometry, hoverMaterial);
|
|
1934
|
-
hoverMarker.renderOrder = 1e3;
|
|
1935
|
-
hoverMarker.userData.id = "measure";
|
|
1936
|
-
hoverMarker.raycast = () => {};
|
|
1937
|
-
scene.add(hoverMarker);
|
|
1938
|
-
}
|
|
1939
|
-
hoverMarker.position.copy(p);
|
|
1940
|
-
hoverMarker.visible = true;
|
|
1941
|
-
};
|
|
1942
|
-
const makeMarker = (p) => {
|
|
2104
|
+
const tag = (object, renderOrder) => {
|
|
2105
|
+
object.renderOrder = renderOrder;
|
|
2106
|
+
object.userData.id = "measure";
|
|
2107
|
+
object.raycast = () => {};
|
|
2108
|
+
object.frustumCulled = false;
|
|
2109
|
+
return object;
|
|
2110
|
+
};
|
|
2111
|
+
const makePoints = (...ps) => {
|
|
1943
2112
|
const geometry = new THREE.BufferGeometry();
|
|
1944
|
-
geometry.setAttribute("position", new THREE.Float32BufferAttribute([
|
|
2113
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(ps.flatMap((p) => [
|
|
1945
2114
|
p.x,
|
|
1946
2115
|
p.y,
|
|
1947
2116
|
p.z
|
|
1948
|
-
], 3));
|
|
1949
|
-
const
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
marker.raycast = () => {};
|
|
1953
|
-
scene.add(marker);
|
|
1954
|
-
return marker;
|
|
1955
|
-
};
|
|
1956
|
-
const clear = () => {
|
|
1957
|
-
points.length = 0;
|
|
1958
|
-
markers.forEach((m) => {
|
|
1959
|
-
m.geometry.dispose();
|
|
1960
|
-
m.removeFromParent();
|
|
1961
|
-
});
|
|
1962
|
-
markers.length = 0;
|
|
1963
|
-
if (line) {
|
|
1964
|
-
line.geometry.dispose();
|
|
1965
|
-
line.material.dispose();
|
|
1966
|
-
line.removeFromParent();
|
|
1967
|
-
line = null;
|
|
1968
|
-
}
|
|
1969
|
-
label?.remove();
|
|
1970
|
-
label = null;
|
|
2117
|
+
]), 3));
|
|
2118
|
+
const points = tag(new THREE.Points(geometry, markerMaterial), RENDER_ORDER_MARKER);
|
|
2119
|
+
scene.add(points);
|
|
2120
|
+
return points;
|
|
1971
2121
|
};
|
|
1972
|
-
const
|
|
1973
|
-
if (points.length !== 2) return;
|
|
1974
|
-
const [a, b] = points;
|
|
2122
|
+
const makeLine = (a, b, opacity = 1) => {
|
|
1975
2123
|
const geometry = new LineGeometry();
|
|
1976
2124
|
geometry.setPositions([
|
|
1977
2125
|
a.x,
|
|
@@ -1981,33 +2129,102 @@ function createMeasureTool(deps) {
|
|
|
1981
2129
|
b.y,
|
|
1982
2130
|
b.z
|
|
1983
2131
|
]);
|
|
1984
|
-
const material = new LineMaterial({
|
|
2132
|
+
const material = new LineMaterial({
|
|
2133
|
+
color,
|
|
2134
|
+
transparent: opacity < 1,
|
|
2135
|
+
opacity
|
|
2136
|
+
});
|
|
1985
2137
|
material.linewidth = 2;
|
|
1986
2138
|
material.depthTest = false;
|
|
1987
|
-
line = new Line2(geometry, material);
|
|
1988
|
-
line.renderOrder = 998;
|
|
1989
|
-
line.userData.id = "measure";
|
|
1990
|
-
line.raycast = () => {};
|
|
2139
|
+
const line = tag(new Line2(geometry, material), RENDER_ORDER_LINE);
|
|
1991
2140
|
scene.add(line);
|
|
1992
|
-
|
|
2141
|
+
return line;
|
|
2142
|
+
};
|
|
2143
|
+
const moveLine = (line, a, b) => {
|
|
2144
|
+
const buffer = line.geometry.attributes.instanceStart.data;
|
|
2145
|
+
buffer.array.set([
|
|
2146
|
+
a.x,
|
|
2147
|
+
a.y,
|
|
2148
|
+
a.z,
|
|
2149
|
+
b.x,
|
|
2150
|
+
b.y,
|
|
2151
|
+
b.z
|
|
2152
|
+
]);
|
|
2153
|
+
buffer.needsUpdate = true;
|
|
2154
|
+
};
|
|
2155
|
+
const disposeLine = (line) => {
|
|
2156
|
+
line.geometry.dispose();
|
|
2157
|
+
line.material.dispose();
|
|
2158
|
+
line.removeFromParent();
|
|
2159
|
+
};
|
|
2160
|
+
const disposePoints = (points) => {
|
|
2161
|
+
points.geometry.dispose();
|
|
2162
|
+
points.removeFromParent();
|
|
2163
|
+
};
|
|
2164
|
+
const labelText = (a, b) => {
|
|
1993
2165
|
const delta = new THREE.Vector3(Math.abs(b.x - a.x), Math.abs(b.y - a.y), Math.abs(b.z - a.z));
|
|
1994
|
-
|
|
2166
|
+
return format(a.distanceTo(b), delta);
|
|
1995
2167
|
};
|
|
1996
|
-
const
|
|
2168
|
+
const midpoint = (a, b) => a.clone().add(b).multiplyScalar(.5);
|
|
2169
|
+
let hoverMarker = null;
|
|
2170
|
+
let rubberBand = null;
|
|
2171
|
+
let rubberLabel = null;
|
|
2172
|
+
const hidePreview = () => {
|
|
2173
|
+
if (hoverMarker) hoverMarker.visible = false;
|
|
2174
|
+
if (rubberBand) rubberBand.visible = false;
|
|
2175
|
+
rubberLabel?.remove();
|
|
2176
|
+
rubberLabel = null;
|
|
2177
|
+
};
|
|
2178
|
+
const showPreview = (snap) => {
|
|
2179
|
+
if (!snap) {
|
|
2180
|
+
hidePreview();
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (!hoverMarker) {
|
|
2184
|
+
hoverMarker = makePoints(new THREE.Vector3());
|
|
2185
|
+
hoverMarker.renderOrder = RENDER_ORDER_HOVER;
|
|
2186
|
+
}
|
|
2187
|
+
hoverMarker.material = snap.snapped ? hoverSnappedMaterial : hoverFreeMaterial;
|
|
2188
|
+
hoverMarker.position.copy(snap.point);
|
|
2189
|
+
hoverMarker.visible = true;
|
|
2190
|
+
if (!start) {
|
|
2191
|
+
if (rubberBand) rubberBand.visible = false;
|
|
2192
|
+
rubberLabel?.remove();
|
|
2193
|
+
rubberLabel = null;
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (!rubberBand) rubberBand = makeLine(start.point, snap.point, .7);
|
|
2197
|
+
moveLine(rubberBand, start.point, snap.point);
|
|
2198
|
+
rubberBand.visible = true;
|
|
2199
|
+
const text = labelText(start.point, snap.point);
|
|
2200
|
+
const mid = midpoint(start.point, snap.point);
|
|
2201
|
+
if (rubberLabel) {
|
|
2202
|
+
rubberLabel.setText(text);
|
|
2203
|
+
rubberLabel.setPosition(mid);
|
|
2204
|
+
} else rubberLabel = labelLayer.addLabel(text, mid, options.labelClassName);
|
|
2205
|
+
};
|
|
2206
|
+
const pick = (event) => {
|
|
1997
2207
|
const rect = canvas.getBoundingClientRect();
|
|
1998
|
-
|
|
1999
|
-
|
|
2208
|
+
const cursorX = event.clientX - rect.left;
|
|
2209
|
+
const cursorY = event.clientY - rect.top;
|
|
2210
|
+
pointer.x = cursorX / rect.width * 2 - 1;
|
|
2211
|
+
pointer.y = -(cursorY / rect.height) * 2 + 1;
|
|
2000
2212
|
const camera = getActiveCamera();
|
|
2001
2213
|
raycaster.setFromCamera(pointer, camera);
|
|
2002
|
-
const threshold = pickThreshold(camera, getViewTarget?.());
|
|
2214
|
+
const threshold = pickThreshold(camera, getViewTarget?.()) * 2;
|
|
2003
2215
|
raycaster.params.Line.threshold = threshold;
|
|
2004
2216
|
raycaster.params.Points.threshold = threshold;
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
return snapToVertex(hits[0], camera, {
|
|
2217
|
+
raycaster.params.Line2 = { threshold: snapPixels / 2 };
|
|
2218
|
+
const screen = {
|
|
2008
2219
|
width: rect.width,
|
|
2009
2220
|
height: rect.height
|
|
2010
|
-
}
|
|
2221
|
+
};
|
|
2222
|
+
const cursor = new THREE.Vector2(cursorX, cursorY);
|
|
2223
|
+
for (const hit of raycastVisible(scene, raycaster)) {
|
|
2224
|
+
if (!(hit.object.isMesh && hit.face != null) && toScreen(hitPoint(hit), camera, screen).distanceTo(cursor) > snapPixels) continue;
|
|
2225
|
+
return snapHit(hit, camera, screen, snapPixels);
|
|
2226
|
+
}
|
|
2227
|
+
return null;
|
|
2011
2228
|
};
|
|
2012
2229
|
let pendingMove = null;
|
|
2013
2230
|
let moveRaf = 0;
|
|
@@ -2027,42 +2244,104 @@ function createMeasureTool(deps) {
|
|
|
2027
2244
|
const latest = pendingMove;
|
|
2028
2245
|
pendingMove = null;
|
|
2029
2246
|
if (!enabled || !latest) return;
|
|
2030
|
-
|
|
2247
|
+
showPreview(pick(latest));
|
|
2248
|
+
requestRender();
|
|
2031
2249
|
});
|
|
2032
2250
|
};
|
|
2251
|
+
const handleLeave = () => {
|
|
2252
|
+
cancelPendingMove();
|
|
2253
|
+
hidePreview();
|
|
2254
|
+
requestRender();
|
|
2255
|
+
};
|
|
2256
|
+
const dropStart = () => {
|
|
2257
|
+
if (!start) return;
|
|
2258
|
+
disposePoints(start.marker);
|
|
2259
|
+
start = null;
|
|
2260
|
+
};
|
|
2261
|
+
const clear = () => {
|
|
2262
|
+
dropStart();
|
|
2263
|
+
for (const m of measurements) {
|
|
2264
|
+
disposePoints(m.markers);
|
|
2265
|
+
disposeLine(m.line);
|
|
2266
|
+
m.label.remove();
|
|
2267
|
+
}
|
|
2268
|
+
measurements.length = 0;
|
|
2269
|
+
hidePreview();
|
|
2270
|
+
requestRender();
|
|
2271
|
+
};
|
|
2272
|
+
const cancel = () => {
|
|
2273
|
+
if (start) {
|
|
2274
|
+
dropStart();
|
|
2275
|
+
hidePreview();
|
|
2276
|
+
requestRender();
|
|
2277
|
+
} else clear();
|
|
2278
|
+
};
|
|
2033
2279
|
const handleClick = (event) => {
|
|
2034
2280
|
if (!enabled) return false;
|
|
2035
|
-
|
|
2036
|
-
const
|
|
2037
|
-
if (
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2281
|
+
cancelPendingMove();
|
|
2282
|
+
const snap = pick(event);
|
|
2283
|
+
if (!snap) return true;
|
|
2284
|
+
if (!start) start = {
|
|
2285
|
+
point: snap.point,
|
|
2286
|
+
marker: makePoints(snap.point)
|
|
2287
|
+
};
|
|
2288
|
+
else if (!snap.point.equals(start.point)) {
|
|
2289
|
+
const a = start.point;
|
|
2290
|
+
const b = snap.point;
|
|
2291
|
+
dropStart();
|
|
2292
|
+
measurements.push({
|
|
2293
|
+
markers: makePoints(a, b),
|
|
2294
|
+
line: makeLine(a, b),
|
|
2295
|
+
label: labelLayer.addLabel(labelText(a, b), midpoint(a, b), options.labelClassName)
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
showPreview(snap);
|
|
2299
|
+
requestRender();
|
|
2041
2300
|
return true;
|
|
2042
2301
|
};
|
|
2302
|
+
const handleKey = (event) => {
|
|
2303
|
+
if (event.key !== "Escape") return;
|
|
2304
|
+
if (event.target?.closest?.("input, textarea, select, [contenteditable]")) return;
|
|
2305
|
+
cancel();
|
|
2306
|
+
};
|
|
2307
|
+
let previousCursor = "";
|
|
2308
|
+
let cancelWarm = null;
|
|
2309
|
+
const setEnabled = (value) => {
|
|
2310
|
+
if (value === enabled) return;
|
|
2311
|
+
enabled = value;
|
|
2312
|
+
if (value) {
|
|
2313
|
+
previousCursor = canvas.style.cursor;
|
|
2314
|
+
canvas.style.cursor = "crosshair";
|
|
2315
|
+
canvas.addEventListener("mouseleave", handleLeave);
|
|
2316
|
+
window.addEventListener("keydown", handleKey);
|
|
2317
|
+
cancelWarm = warmPickIndex(scene);
|
|
2318
|
+
} else {
|
|
2319
|
+
canvas.style.cursor = previousCursor;
|
|
2320
|
+
canvas.removeEventListener("mouseleave", handleLeave);
|
|
2321
|
+
window.removeEventListener("keydown", handleKey);
|
|
2322
|
+
cancelWarm?.();
|
|
2323
|
+
cancelWarm = null;
|
|
2324
|
+
cancelPendingMove();
|
|
2325
|
+
clear();
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2043
2328
|
return {
|
|
2044
|
-
setEnabled
|
|
2045
|
-
enabled = value;
|
|
2046
|
-
if (!value) {
|
|
2047
|
-
cancelPendingMove();
|
|
2048
|
-
clear();
|
|
2049
|
-
showHover(null);
|
|
2050
|
-
}
|
|
2051
|
-
},
|
|
2329
|
+
setEnabled,
|
|
2052
2330
|
isEnabled: () => enabled,
|
|
2053
2331
|
handleClick,
|
|
2054
2332
|
handleMove,
|
|
2333
|
+
cancel,
|
|
2055
2334
|
clear,
|
|
2056
2335
|
dispose: () => {
|
|
2057
|
-
|
|
2336
|
+
setEnabled(false);
|
|
2058
2337
|
clear();
|
|
2059
|
-
if (hoverMarker)
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
}
|
|
2338
|
+
if (hoverMarker) disposePoints(hoverMarker);
|
|
2339
|
+
if (rubberBand) disposeLine(rubberBand);
|
|
2340
|
+
hoverMarker = null;
|
|
2341
|
+
rubberBand = null;
|
|
2064
2342
|
markerMaterial.dispose();
|
|
2065
|
-
|
|
2343
|
+
hoverSnappedMaterial.dispose();
|
|
2344
|
+
hoverFreeMaterial.dispose();
|
|
2066
2345
|
}
|
|
2067
2346
|
};
|
|
2068
2347
|
}
|
|
@@ -3894,6 +4173,8 @@ const initThree = function(canvas, options) {
|
|
|
3894
4173
|
canvas,
|
|
3895
4174
|
scene,
|
|
3896
4175
|
getActiveCamera,
|
|
4176
|
+
getViewTarget: () => controls.target,
|
|
4177
|
+
requestRender: () => requestRender(),
|
|
3897
4178
|
labelLayer,
|
|
3898
4179
|
options: {
|
|
3899
4180
|
snapPixels: config.measure.snapPixels,
|