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