@selvajs/visualization 1.6.0 → 1.6.1-beta.0
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 +126 -37
- package/dist/render.cjs.map +1 -1
- package/dist/render.js +126 -37
- package/dist/render.js.map +1 -1
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -3809,6 +3809,15 @@ const SELECTION_MIN_OPACITY = 1;
|
|
|
3809
3809
|
* color blindness, so what tells hover from selected there is brightness.
|
|
3810
3810
|
*/
|
|
3811
3811
|
const HOVER_COLOR = "#ffb020";
|
|
3812
|
+
/**
|
|
3813
|
+
* A fingertip covers far more than the pixel under its centre, so a touch that lands just beside a
|
|
3814
|
+
* thin curve or a narrow part still picks it. Rings are sampled only when the centre misses.
|
|
3815
|
+
*/
|
|
3816
|
+
const TOUCH_PICK_RADII_PX = [8, 16];
|
|
3817
|
+
const TOUCH_PICK_DIRECTIONS = 8;
|
|
3818
|
+
/** Two taps closer than this in time and space are a double-tap. */
|
|
3819
|
+
const DOUBLE_TAP_MS = 300;
|
|
3820
|
+
const DOUBLE_TAP_SLOP_PX = 30;
|
|
3812
3821
|
/** Linear RGB distance. Closer than this, a tint barely changes the surface. */
|
|
3813
3822
|
const MIN_TINT_DISTANCE = .35;
|
|
3814
3823
|
/** Taken by a surface already close to the tint. Blue stays distinct under every common deficiency. */
|
|
@@ -3848,7 +3857,7 @@ function tintForSelection(material, color, isMesh) {
|
|
|
3848
3857
|
}
|
|
3849
3858
|
if ("color" in material) material.color = selectionColor.clone();
|
|
3850
3859
|
}
|
|
3851
|
-
function setupEventHandlers(canvas, scene, cameraController, config) {
|
|
3860
|
+
function setupEventHandlers(canvas, scene, cameraController, config, taps) {
|
|
3852
3861
|
const selectedObjects = /* @__PURE__ */ new Set();
|
|
3853
3862
|
const originalMaterials = /* @__PURE__ */ new Map();
|
|
3854
3863
|
const memberHighlights = [];
|
|
@@ -3857,16 +3866,7 @@ function setupEventHandlers(canvas, scene, cameraController, config) {
|
|
|
3857
3866
|
let selectedByEntries = false;
|
|
3858
3867
|
const raycaster = new THREE.Raycaster();
|
|
3859
3868
|
const mouse = new THREE.Vector2();
|
|
3860
|
-
const mouseDownPosition = new THREE.Vector2();
|
|
3861
3869
|
const getActiveCamera = () => cameraController.getActiveCamera();
|
|
3862
|
-
const isFullyVisible = (object) => {
|
|
3863
|
-
let current = object;
|
|
3864
|
-
while (current) {
|
|
3865
|
-
if (!current.visible) return false;
|
|
3866
|
-
current = current.parent;
|
|
3867
|
-
}
|
|
3868
|
-
return true;
|
|
3869
|
-
};
|
|
3870
3870
|
const fitToView = () => {
|
|
3871
3871
|
const box = computeContentBounds(scene);
|
|
3872
3872
|
if (box.isEmpty()) {
|
|
@@ -4004,29 +4004,61 @@ function setupEventHandlers(canvas, scene, cameraController, config) {
|
|
|
4004
4004
|
}
|
|
4005
4005
|
raycaster.params.Points.threshold = cachedDiagonal * .01;
|
|
4006
4006
|
};
|
|
4007
|
-
|
|
4008
|
-
* Nearest visible, pickable object under the pointer, or null. Viewer aids are excluded: the grid
|
|
4009
|
-
* is a huge camera-following plane, so double-clicking through a thin curve onto it would frame a
|
|
4010
|
-
* box the size of the fade radius and fling the camera out.
|
|
4011
|
-
*/
|
|
4012
|
-
const pickAt = (clientX, clientY) => {
|
|
4007
|
+
const setPointerNdc = (clientX, clientY) => {
|
|
4013
4008
|
const rect = canvas.getBoundingClientRect();
|
|
4014
4009
|
mouse.x = (clientX - rect.left) / rect.width * 2 - 1;
|
|
4015
4010
|
mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
|
4011
|
+
};
|
|
4012
|
+
/**
|
|
4013
|
+
* Nearest visible, pickable object under the pointer, or null. Goes through the BVH pick, which
|
|
4014
|
+
* also skips viewer aids: the grid is a huge camera-following plane, so double-clicking through
|
|
4015
|
+
* a thin curve onto it would frame a box the size of the fade radius and fling the camera out.
|
|
4016
|
+
*/
|
|
4017
|
+
const pickAt = (clientX, clientY) => {
|
|
4018
|
+
setPointerNdc(clientX, clientY);
|
|
4016
4019
|
updatePickThresholds();
|
|
4017
4020
|
raycaster.setFromCamera(mouse, getActiveCamera());
|
|
4018
|
-
|
|
4021
|
+
return raycastVisible(scene, raycaster)[0] ?? null;
|
|
4022
|
+
};
|
|
4023
|
+
const pickNear = (clientX, clientY, touch) => {
|
|
4024
|
+
const centre = pickAt(clientX, clientY);
|
|
4025
|
+
if (centre || !touch) return centre;
|
|
4026
|
+
for (const radius of TOUCH_PICK_RADII_PX) {
|
|
4027
|
+
let nearest = null;
|
|
4028
|
+
for (let i = 0; i < TOUCH_PICK_DIRECTIONS; i++) {
|
|
4029
|
+
const angle = i / TOUCH_PICK_DIRECTIONS * Math.PI * 2;
|
|
4030
|
+
const hit = pickAt(clientX + Math.cos(angle) * radius, clientY + Math.sin(angle) * radius);
|
|
4031
|
+
if (hit && (!nearest || hit.distance < nearest.distance)) nearest = hit;
|
|
4032
|
+
}
|
|
4033
|
+
if (nearest) return nearest;
|
|
4034
|
+
}
|
|
4019
4035
|
return null;
|
|
4020
4036
|
};
|
|
4021
|
-
|
|
4022
|
-
|
|
4037
|
+
let lastTap = null;
|
|
4038
|
+
/** iOS fires no `dblclick` for a double-tap, so touch pairs its own taps. */
|
|
4039
|
+
const isSecondTap = (event) => {
|
|
4040
|
+
const previous = lastTap;
|
|
4041
|
+
lastTap = {
|
|
4042
|
+
time: event.timeStamp,
|
|
4043
|
+
x: event.clientX,
|
|
4044
|
+
y: event.clientY
|
|
4045
|
+
};
|
|
4046
|
+
if (!previous) return false;
|
|
4047
|
+
const isDouble = event.timeStamp - previous.time <= DOUBLE_TAP_MS && Math.hypot(event.clientX - previous.x, event.clientY - previous.y) <= DOUBLE_TAP_SLOP_PX;
|
|
4048
|
+
if (isDouble) lastTap = null;
|
|
4049
|
+
return isDouble;
|
|
4023
4050
|
};
|
|
4024
4051
|
const handleCanvasClick = (event) => {
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4052
|
+
if (!taps.wasTap()) return;
|
|
4053
|
+
const touch = taps.pointerType() !== "mouse";
|
|
4054
|
+
if (touch && isSecondTap(event)) {
|
|
4055
|
+
handleDoubleTap(event.clientX, event.clientY, true);
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
const hit = pickNear(event.clientX, event.clientY, touch);
|
|
4028
4059
|
if (!hit) {
|
|
4029
4060
|
clearSelection();
|
|
4061
|
+
setPointerNdc(event.clientX, event.clientY);
|
|
4030
4062
|
config.events?.onBackgroundClicked?.({
|
|
4031
4063
|
x: mouse.x,
|
|
4032
4064
|
y: mouse.y
|
|
@@ -4053,7 +4085,11 @@ function setupEventHandlers(canvas, scene, cameraController, config) {
|
|
|
4053
4085
|
else if (clickedObject instanceof THREE.Mesh && Object.keys(clickedObject.userData).length > 0) config.events?.onMeshMetadataClicked?.(clickedObject.userData);
|
|
4054
4086
|
};
|
|
4055
4087
|
const handleDoubleClick = (event) => {
|
|
4056
|
-
|
|
4088
|
+
if (!taps.wasTap() || taps.pointerType() !== "mouse") return;
|
|
4089
|
+
handleDoubleTap(event.clientX, event.clientY, false);
|
|
4090
|
+
};
|
|
4091
|
+
const handleDoubleTap = (clientX, clientY, touch) => {
|
|
4092
|
+
const hit = pickNear(clientX, clientY, touch);
|
|
4057
4093
|
if (!hit) {
|
|
4058
4094
|
if (config.events?.enableDoubleClickZoom) cameraController.frameBounds(computeContentBounds(scene), true);
|
|
4059
4095
|
return;
|
|
@@ -4083,7 +4119,6 @@ function setupEventHandlers(canvas, scene, cameraController, config) {
|
|
|
4083
4119
|
}
|
|
4084
4120
|
};
|
|
4085
4121
|
if (config.events?.enableClickToFocus) {
|
|
4086
|
-
canvas.addEventListener("mousedown", handleMouseDown);
|
|
4087
4122
|
canvas.addEventListener("click", handleCanvasClick);
|
|
4088
4123
|
canvas.addEventListener("dblclick", handleDoubleClick);
|
|
4089
4124
|
}
|
|
@@ -4092,7 +4127,6 @@ function setupEventHandlers(canvas, scene, cameraController, config) {
|
|
|
4092
4127
|
canvas.addEventListener("keydown", handleKeydown);
|
|
4093
4128
|
}
|
|
4094
4129
|
const dispose = () => {
|
|
4095
|
-
canvas.removeEventListener("mousedown", handleMouseDown);
|
|
4096
4130
|
canvas.removeEventListener("click", handleCanvasClick);
|
|
4097
4131
|
canvas.removeEventListener("dblclick", handleDoubleClick);
|
|
4098
4132
|
canvas.removeEventListener("keydown", handleKeydown);
|
|
@@ -4107,6 +4141,69 @@ function setupEventHandlers(canvas, scene, cameraController, config) {
|
|
|
4107
4141
|
setHover
|
|
4108
4142
|
};
|
|
4109
4143
|
}
|
|
4144
|
+
// src/render/scene-setup/tap-gesture.ts
|
|
4145
|
+
/** A mouse press is precise; past this it was a drag to orbit or pan. */
|
|
4146
|
+
const MOUSE_SLOP_PX = 5;
|
|
4147
|
+
/** A fingertip wobbles a few pixels on a deliberate tap. */
|
|
4148
|
+
const TOUCH_SLOP_PX = 10;
|
|
4149
|
+
/** A touch held this long was the start of an orbit that never moved, not a tap. */
|
|
4150
|
+
const TOUCH_LONG_PRESS_MS = 500;
|
|
4151
|
+
function trackTaps(canvas) {
|
|
4152
|
+
const down = /* @__PURE__ */ new Map();
|
|
4153
|
+
let pointerType = "mouse";
|
|
4154
|
+
let startTime = 0;
|
|
4155
|
+
let maxPointers = 0;
|
|
4156
|
+
let maxTravel = 0;
|
|
4157
|
+
let cancelled = false;
|
|
4158
|
+
let lastWasTap = false;
|
|
4159
|
+
const travel = (event) => {
|
|
4160
|
+
const start = down.get(event.pointerId);
|
|
4161
|
+
if (!start) return;
|
|
4162
|
+
maxTravel = Math.max(maxTravel, Math.hypot(event.clientX - start.x, event.clientY - start.y));
|
|
4163
|
+
};
|
|
4164
|
+
const onDown = (event) => {
|
|
4165
|
+
if (down.size === 0) {
|
|
4166
|
+
pointerType = event.pointerType;
|
|
4167
|
+
startTime = event.timeStamp;
|
|
4168
|
+
maxPointers = 0;
|
|
4169
|
+
maxTravel = 0;
|
|
4170
|
+
cancelled = false;
|
|
4171
|
+
}
|
|
4172
|
+
down.set(event.pointerId, {
|
|
4173
|
+
x: event.clientX,
|
|
4174
|
+
y: event.clientY
|
|
4175
|
+
});
|
|
4176
|
+
maxPointers = Math.max(maxPointers, down.size);
|
|
4177
|
+
};
|
|
4178
|
+
const onMove = (event) => travel(event);
|
|
4179
|
+
const onEnd = (event) => {
|
|
4180
|
+
if (!down.has(event.pointerId)) return;
|
|
4181
|
+
travel(event);
|
|
4182
|
+
if (event.type === "pointercancel") cancelled = true;
|
|
4183
|
+
down.delete(event.pointerId);
|
|
4184
|
+
if (down.size > 0) return;
|
|
4185
|
+
const isMouse = pointerType === "mouse";
|
|
4186
|
+
lastWasTap = !cancelled && maxPointers === 1 && maxTravel <= (isMouse ? MOUSE_SLOP_PX : TOUCH_SLOP_PX) && (isMouse || event.timeStamp - startTime <= TOUCH_LONG_PRESS_MS);
|
|
4187
|
+
};
|
|
4188
|
+
const options = {
|
|
4189
|
+
capture: true,
|
|
4190
|
+
passive: true
|
|
4191
|
+
};
|
|
4192
|
+
canvas.addEventListener("pointerdown", onDown, options);
|
|
4193
|
+
canvas.addEventListener("pointermove", onMove, options);
|
|
4194
|
+
canvas.addEventListener("pointerup", onEnd, options);
|
|
4195
|
+
canvas.addEventListener("pointercancel", onEnd, options);
|
|
4196
|
+
return {
|
|
4197
|
+
wasTap: () => lastWasTap,
|
|
4198
|
+
pointerType: () => pointerType,
|
|
4199
|
+
dispose: () => {
|
|
4200
|
+
canvas.removeEventListener("pointerdown", onDown, options);
|
|
4201
|
+
canvas.removeEventListener("pointermove", onMove, options);
|
|
4202
|
+
canvas.removeEventListener("pointerup", onEnd, options);
|
|
4203
|
+
canvas.removeEventListener("pointercancel", onEnd, options);
|
|
4204
|
+
}
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4110
4207
|
// src/render/scene-setup/setup-renderer.ts
|
|
4111
4208
|
function setupRenderer(canvas, config, pixelRatio) {
|
|
4112
4209
|
const renderer = new THREE.WebGLRenderer({
|
|
@@ -4215,7 +4312,8 @@ const initThree = function(canvas, options) {
|
|
|
4215
4312
|
format: config.measure.format
|
|
4216
4313
|
}
|
|
4217
4314
|
}) : null;
|
|
4218
|
-
const
|
|
4315
|
+
const taps = trackTaps(canvas);
|
|
4316
|
+
const eventHandlers = config.events.enableEventHandlers !== false ? setupEventHandlers(canvas, scene, cameraController, config, taps) : {
|
|
4219
4317
|
dispose: () => {},
|
|
4220
4318
|
fitToView: () => {},
|
|
4221
4319
|
clearSelection: () => {},
|
|
@@ -4233,19 +4331,10 @@ const initThree = function(canvas, options) {
|
|
|
4233
4331
|
tool: gizmo,
|
|
4234
4332
|
priority: -100
|
|
4235
4333
|
});
|
|
4236
|
-
const DRAG_SLOP_PX = 5;
|
|
4237
|
-
let pressX = 0;
|
|
4238
|
-
let pressY = 0;
|
|
4239
|
-
const handlePointerDown = (event) => {
|
|
4240
|
-
pressX = event.clientX;
|
|
4241
|
-
pressY = event.clientY;
|
|
4242
|
-
};
|
|
4243
|
-
const wasDrag = (event) => Math.hypot(event.clientX - pressX, event.clientY - pressY) > DRAG_SLOP_PX;
|
|
4244
4334
|
const handleToolClick = (event) => {
|
|
4245
|
-
if (
|
|
4335
|
+
if (!taps.wasTap()) return;
|
|
4246
4336
|
if (tools.handleClick(event)) event.stopImmediatePropagation();
|
|
4247
4337
|
};
|
|
4248
|
-
canvas.addEventListener("mousedown", handlePointerDown, { capture: true });
|
|
4249
4338
|
canvas.addEventListener("click", handleToolClick, { capture: true });
|
|
4250
4339
|
const handleToolMove = (event) => tools.handleMove(event);
|
|
4251
4340
|
canvas.addEventListener("mousemove", handleToolMove, { passive: true });
|
|
@@ -4354,7 +4443,7 @@ const initThree = function(canvas, options) {
|
|
|
4354
4443
|
disposed = true;
|
|
4355
4444
|
disposeAnimation();
|
|
4356
4445
|
eventHandlers.dispose();
|
|
4357
|
-
|
|
4446
|
+
taps.dispose();
|
|
4358
4447
|
canvas.removeEventListener("click", handleToolClick, { capture: true });
|
|
4359
4448
|
canvas.removeEventListener("mousemove", handleToolMove);
|
|
4360
4449
|
measureTool?.dispose();
|