section view: review fixes -- a tap on the plane's handle no longer falls through to a pick (the handle clears its axis on release before app.js reads it, so the press is remembered); Move shows one arrow along the cut's normal; the cut turns and reseats with the part

This commit is contained in:
matthewtrahan authored and Matthew Trahan committed 2026-10-14 13:07:13 -05:00
1 parent 736cf9e5d1
commit 1a2b3effbf
3 files changed
+40 -5

No files matched your search

+5 -1
View File
@@ -68,6 +68,7 @@ addEventListener('keydown', (e) => {
// One set of viewport listeners, routed by mode: remove, then draw, then lay flat.
let pressAt = null;
let pressOnPlane = false; // the press landed on the section plane's handle
renderer.domElement.addEventListener('pointermove', (ev) => {
// The section plane's handle is under the pointer or being dragged: it owns the
// pointer, and no pick should light up through it.
@@ -109,6 +110,9 @@ renderer.domElement.addEventListener('pointerleave', () => {
renderer.domElement.addEventListener('pointerdown', (e) => {
pressAt = { x: e.clientX, y: e.clientY };
// Read at the press: the plane's TransformControls listens first, so it has set
// its axis by now -- and on release it clears it before this module's pointerup.
pressOnPlane = planeBusy();
});
renderer.domElement.addEventListener('pointerup', (e) => {
@@ -119,7 +123,7 @@ renderer.domElement.addEventListener('pointerup', (e) => {
// a drag is an orbit, not a pick
if (Math.hypot(e.clientX - from.x, e.clientY - from.y) > 4) return;
// a click on the section plane's handle is the handle's, not a pick
if (planeBusy()) return;
if (pressOnPlane) return;
// A right CLICK cancels; a right DRAG is an orbit or pan (the Onshape preset
// orbits on it) and leaves the wall in progress alone. Decided here, on release,
+2 -1
View File
@@ -8,7 +8,7 @@ import * as THREE from 'three';
import { buildTopology, analyze, DEFAULT_THRESHOLD, MIN_REGION_AREA } from '../overhangs.js';
import { el } from './dom.js';
import { scene, controls, frame, clip } from './scene.js';
import { setSection } from './section.js';
import { setSection, followPart } from './section.js';
import { removeMode, cancelRemove, resetRemovals } from './remove.js';
import { resetHistory } from './history.js';
import { importNote, renderImportNote } from './io.js';
@@ -271,6 +271,7 @@ export function shade() {
updateLayerView(size);
updateLoadReadout();
syncLoadUI(); // re-light the pad button for the load's new world direction
followPart(); // a section cut turns and reseats with the part
return size;
}
+33 -3
View File
@@ -8,8 +8,10 @@
* array, and switching the section on puts `plane` in it. Picks skip what the cut
* hides (keptHit), so a click lands on the surface you can see, not the one cut away.
*
* `plane` is in WORLD space and stays put while the part turns. Its normal points
* at the side that is KEPT (three.js discards where the signed distance is < 0).
* `plane` is in world space, but the cut is held in the PART's frame (followPart):
* turn the part and the cut turns with it, so it stays on the bore it was opening.
* Its normal points at the side that is KEPT (three.js discards where the signed
* distance is < 0).
*/
import * as THREE from 'three';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
@@ -40,11 +42,14 @@ scene.add(handle);
export const planeGizmo = new TransformControls(camera, renderer.domElement);
planeGizmo.setSize(0.7);
// Local space: Move's one arrow slides the cut along its own normal, straight
// through the part, whatever way it has been turned.
planeGizmo.setSpace('local');
planeGizmo.attach(handle);
const planeHelper = planeGizmo.getHelper ? planeGizmo.getHelper() : planeGizmo;
scene.add(planeHelper);
planeGizmo.addEventListener('dragging-changed', (e) => { controls.enabled = !e.value; });
planeGizmo.addEventListener('objectChange', syncPlane);
planeGizmo.addEventListener('objectChange', () => { syncPlane(); holdInPart(); });
// Same 5 degree snap as the part's rings; Shift frees it.
const SNAP = THREE.MathUtils.degToRad(5);
planeGizmo.setRotationSnap(SNAP);
@@ -59,6 +64,27 @@ function syncPlane() {
plane.setFromNormalAndCoplanarPoint(nrm, handle.position);
}
// The handle's pose in the part's frame, so the cut follows the part when it turns.
const inPart = new THREE.Matrix4();
const m4 = new THREE.Matrix4();
/** Remember where the handle sits on the part (after the user or a button moved it). */
function holdInPart() {
if (!part) return;
part.updateMatrixWorld();
handle.updateMatrix();
inPart.copy(part.matrixWorld).invert().multiply(handle.matrix);
}
/** The part turned or reseated (part.js shade): carry the cut along with it. */
export function followPart() {
if (!sectionOn || !part || planeGizmo.dragging) return;
part.updateMatrixWorld();
m4.multiplyMatrices(part.matrixWorld, inPart);
m4.decompose(handle.position, handle.quaternion, handle.scale);
syncPlane();
}
/** The part's world-space box: where the plane starts and how big it draws. */
function partBox() {
return new THREE.Box3().setFromObject(part);
@@ -76,6 +102,7 @@ function placeThroughCentre(n) {
partBox().getCenter(handle.position);
handle.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), n.clone().normalize());
syncPlane();
holdInPart();
}
/** The first hit the cut leaves visible (hits come sorted nearest first). */
@@ -94,6 +121,8 @@ function syncGizmo() {
planeGizmo.enabled = on;
if (!on) planeGizmo.axis = null;
if (handleMode) planeGizmo.setMode(handleMode);
// Move shows only the normal arrow: sliding the cut within its own plane does nothing.
planeGizmo.showX = planeGizmo.showY = handleMode !== 'translate';
planeHelper.visible = on;
handle.visible = sectionOn;
}
@@ -143,6 +172,7 @@ for (const [id, mode] of [['section-move', 'translate'], ['section-turn', 'rotat
el('section-flip').addEventListener('click', () => {
handle.rotateX(Math.PI);
syncPlane();
holdInPart();
});
const AXES = { x: new THREE.Vector3(1, 0, 0), y: new THREE.Vector3(0, 1, 0),