From 1a2b3effbf7cbb4635cb1db18bebddcd93049cd2 Mon Sep 17 00:00:00 2001 From: matthewtrahan Date: Wed, 14 Oct 2026 09:13:41 -0500 Subject: [PATCH] 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 --- web/app.js | 6 +++++- web/ui/part.js | 3 ++- web/ui/section.js | 36 +++++++++++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/web/app.js b/web/app.js index aeebc26..c2f4935 100644 --- a/web/app.js +++ b/web/app.js @@ -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, diff --git a/web/ui/part.js b/web/ui/part.js index c0a2772..ce6effa 100644 --- a/web/ui/part.js +++ b/web/ui/part.js @@ -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; } diff --git a/web/ui/section.js b/web/ui/section.js index 321d648..730da4c 100644 --- a/web/ui/section.js +++ b/web/ui/section.js @@ -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),