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