mirror of
https://github.com/gittrahan/support-fins.git
synced 2026-10-19 02:10:52 -05:00
sway preview: smoother hover -- the ghost fades in and out (120 ms) instead of popping and a big jump in height cross-fades, a pointer slipping off the side for a moment (an edge, a facet, a gap, the rotate rings) keeps the preview 150 ms before it fades, the label fades, eases after the cursor and holds a min width, the cursor dot eases to amber, capped tops step 1 mm instead of 2, and shorter label text (all five languages). The motion lives in ui/braceghost.js; which brace a click stands is unchanged
This commit is contained in:
1 parent
240604ebf5
commit
acfbed6919
9 files changed
+243
-42
No files matched your search
+5
-3
@@ -34,7 +34,7 @@ import './ui/scale.js';
|
||||
import { updateReadout } from './ui/readout.js';
|
||||
import {
|
||||
drawnWalls, drawnTris, drawStart, selectedWall, drawActive, sizeMarkers, clearPreview,
|
||||
selectWall, removeSelected, drawHover, drawClick, braceActive, braceHover, braceClick, dropBracePreview,
|
||||
selectWall, removeSelected, drawHover, drawClick, braceActive, braceHover, braceClick, dropBracePreview, braceAway,
|
||||
} from './ui/walls.js';
|
||||
import { finTris, padTris, lastBuilt } from './ui/finbuild.js';
|
||||
import { initSettings } from './ui/settings.js';
|
||||
@@ -96,9 +96,11 @@ renderer.domElement.addEventListener('pointermove', (ev) => {
|
||||
return;
|
||||
}
|
||||
// Auto / Full with Sway braces on: an upright side previews the brace a click stands.
|
||||
// An orbit drag or the rotate rings own the pointer: the preview goes, not left behind.
|
||||
// An orbit or ring drag owns the pointer: the preview goes, not left behind. Over
|
||||
// the rings without a drag it lingers a moment, as when it slips off the side.
|
||||
if (!layActive() && braceActive()) {
|
||||
if (gizmo.dragging || gizmo.axis || pressAt) dropBracePreview();
|
||||
if (gizmo.dragging || pressAt) dropBracePreview();
|
||||
else if (gizmo.axis) braceAway();
|
||||
else if (braceHover(ev)) { hoverFace.visible = false; return; }
|
||||
}
|
||||
// Face-lay hover ONLY while armed: otherwise a highlighted, clickable-looking
|
||||
|
||||
+2
-2
@@ -419,7 +419,7 @@ export default {
|
||||
"Cross-rib height as a percentage of sway brace height. 10–90%, default 50%. Requires Brace tall walls and the brace to exceed its start height. Does not change the horizontal reach.": "ارتفاع الأضلاع العرضية كنسبة مئوية من ارتفاع دعامة منع التمايل. 10–90%، الافتراضي 50%. يتطلب «تدعيم الجدران العالية» ودعامة أعلى من ارتفاع البدء الخاص به. لا يغيّر الامتداد على السرير.",
|
||||
"{n} brace ribs": { one: "{n} ضلع دعامة", other: "{n} أضلاع دعامة" },
|
||||
"that’s too low for a brace — move up the side": "هذا منخفض جدًا لدعامة — تحرّك إلى أعلى الجانب",
|
||||
"Sway brace, full height: {mm} mm": "دعامة منع التمايل، بالارتفاع الكامل: {mm} مم",
|
||||
"Sway brace, {mm} mm tall — move up the side for full height": "دعامة منع التمايل، بارتفاع {mm} مم — تحرّك إلى أعلى الجانب للارتفاع الكامل",
|
||||
"Brace · full height {mm} mm": "دعامة · الارتفاع الكامل {mm} مم",
|
||||
"Brace · {mm} mm · move up for full height": "دعامة · {mm} مم · تحرّك لأعلى للارتفاع الكامل",
|
||||
"Click two points across an overhang — straight onto the red faces — to lay a breakaway wall along that line. Hover an upright side to preview a sway brace: its top follows the cursor, and near the top of the side it runs full height. Click to stand it. Esc or right-click cancels.": "انقر على نقطتين عبر بروز معلّق — مباشرةً على الأوجه الحمراء — لوضع جدار قابل للكسر على امتداد الخط. ومرّر المؤشر فوق جانب رأسي لمعاينة دعامة تمنع التمايل: يتبع أعلاها المؤشر، وقرب أعلى الجانب تمتد بالارتفاع الكامل. انقر لإضافتها. اضغط Esc أو انقر بزر الفأرة الأيمن للإلغاء.",
|
||||
};
|
||||
+2
-2
@@ -435,7 +435,7 @@ export default {
|
||||
"Cross-rib height as a percentage of sway brace height. 10–90%, default 50%. Requires Brace tall walls and the brace to exceed its start height. Does not change the horizontal reach.": "Höhe der Kreuzrippen in Prozent der Stützrippen-Höhe. 10–90 %, Standard 50 %. Braucht „Hohe Wände versteifen“ und eine Stützrippe über dessen Starthöhe. Ändert die Reichweite auf dem Druckbett nicht.",
|
||||
"{n} brace ribs": { one: "{n} Kreuzrippe", other: "{n} Kreuzrippen" },
|
||||
"that’s too low for a brace — move up the side": "zu niedrig für eine Stützrippe – weiter oben an der Seite",
|
||||
"Sway brace, full height: {mm} mm": "Stützrippe, volle Höhe: {mm} mm",
|
||||
"Sway brace, {mm} mm tall — move up the side for full height": "Stützrippe, {mm} mm hoch – weiter oben an der Seite für volle Höhe",
|
||||
"Brace · full height {mm} mm": "Stützrippe · volle Höhe {mm} mm",
|
||||
"Brace · {mm} mm · move up for full height": "Stützrippe · {mm} mm · höher für volle Höhe",
|
||||
"Click two points across an overhang — straight onto the red faces — to lay a breakaway wall along that line. Hover an upright side to preview a sway brace: its top follows the cursor, and near the top of the side it runs full height. Click to stand it. Esc or right-click cancels.": "Klicken Sie zwei Punkte über einen Überhang – direkt auf die roten Flächen –, um eine Stützwand zu ziehen. Fahren Sie mit der Maus über eine aufrechte Seite, um eine Stützrippe vorab zu sehen: Ihre Oberkante folgt dem Mauszeiger, und nahe dem oberen Ende der Seite reicht sie über die volle Höhe. Klicken setzt sie. Esc oder Rechtsklick bricht ab.",
|
||||
};
|
||||
+2
-2
@@ -415,7 +415,7 @@ export default {
|
||||
"Cross-rib height as a percentage of sway brace height. 10–90%, default 50%. Requires Brace tall walls and the brace to exceed its start height. Does not change the horizontal reach.": "Hoogte van de kruisribben als percentage van de hoogte van de verstevigingsrib. 10–90%, standaard 50%. Werkt alleen met ribben voor hoge wanden aan en een verstevigingsrib boven de beginhoogte daarvan. Verandert het bereik op het printbed niet.",
|
||||
"{n} brace ribs": { one: "{n} kruisrib", other: "{n} kruisribben" },
|
||||
"that’s too low for a brace — move up the side": "te laag voor een verstevigingsrib — ga hoger op de zijde",
|
||||
"Sway brace, full height: {mm} mm": "Verstevigingsrib, volle hoogte: {mm} mm",
|
||||
"Sway brace, {mm} mm tall — move up the side for full height": "Verstevigingsrib, {mm} mm hoog — ga hoger op de zijde voor volle hoogte",
|
||||
"Brace · full height {mm} mm": "Rib · volle hoogte {mm} mm",
|
||||
"Brace · {mm} mm · move up for full height": "Rib · {mm} mm · hoger voor volle hoogte",
|
||||
"Click two points across an overhang — straight onto the red faces — to lay a breakaway wall along that line. Hover an upright side to preview a sway brace: its top follows the cursor, and near the top of the side it runs full height. Click to stand it. Esc or right-click cancels.": "Klik op twee punten aan weerszijden van een overhang — rechtstreeks op de rode vlakken — om langs die lijn een afbreekbare steunwand te plaatsen. Beweeg over een rechtopstaand zijvlak om een verstevigingsrib vooraf te zien: de bovenkant volgt de cursor, en vlak bij de bovenkant van de zijde loopt hij over de volle hoogte. Klik om hem te plaatsen. Esc of rechtsklikken annuleert.",
|
||||
};
|
||||
+2
-2
@@ -485,7 +485,7 @@ export default {
|
||||
"Cross-rib height as a percentage of sway brace height. 10–90%, default 50%. Requires Brace tall walls and the brace to exceed its start height. Does not change the horizontal reach.": "Altura das nervuras cruzadas como porcentagem da altura da escora lateral. 10–90%, padrão 50%. Exige Reforçar paredes altas e uma escora acima da altura inicial dele. Não muda o alcance na mesa.",
|
||||
"{n} brace ribs": { one: "{n} nervura de escora", other: "{n} nervuras de escora" },
|
||||
"that’s too low for a brace — move up the side": "baixo demais para uma escora — suba pelo lado",
|
||||
"Sway brace, full height: {mm} mm": "Escora lateral, altura total: {mm} mm",
|
||||
"Sway brace, {mm} mm tall — move up the side for full height": "Escora lateral, {mm} mm de altura — suba pelo lado para a altura total",
|
||||
"Brace · full height {mm} mm": "Escora · altura total {mm} mm",
|
||||
"Brace · {mm} mm · move up for full height": "Escora · {mm} mm · suba para altura total",
|
||||
"Click two points across an overhang — straight onto the red faces — to lay a breakaway wall along that line. Hover an upright side to preview a sway brace: its top follows the cursor, and near the top of the side it runs full height. Click to stand it. Esc or right-click cancels.": "Clique em dois pontos atravessando um balanço — direto nas faces vermelhas — para criar uma parede destacável ao longo dessa linha. Passe o mouse sobre um lado vertical para ver a prévia de uma escora lateral: o topo segue o cursor, e perto do alto do lado ela vai até a altura total. Clique para colocá-la. Esc ou botão direito cancela.",
|
||||
};
|
||||
+2
-2
@@ -529,7 +529,7 @@ export default {
|
||||
"Cross-rib height as a percentage of sway brace height. 10–90%, default 50%. Requires Brace tall walls and the brace to exceed its start height. Does not change the horizontal reach.": "十字肋高度占防晃撑高度的百分比。可调范围 10%~90%,默认 50%。需勾选「加固高墙」,且防晃撑超过加固起始高度才生效。不改变横向伸出长度。",
|
||||
"{n} brace ribs": { other: "{n} 条撑肋" },
|
||||
"that’s too low for a brace — move up the side": "此处太低,放不下防晃撑——请沿侧面上移",
|
||||
"Sway brace, full height: {mm} mm": "防晃撑,全高:{mm} mm",
|
||||
"Sway brace, {mm} mm tall — move up the side for full height": "防晃撑,高 {mm} mm——沿侧面上移可得全高",
|
||||
"Brace · full height {mm} mm": "防晃撑 · 全高 {mm} mm",
|
||||
"Brace · {mm} mm · move up for full height": "防晃撑 · {mm} mm · 上移可得全高",
|
||||
"Click two points across an overhang — straight onto the red faces — to lay a breakaway wall along that line. Hover an upright side to preview a sway brace: its top follows the cursor, and near the top of the side it runs full height. Click to stand it. Esc or right-click cancels.": "在悬垂面上点击两点(直接点在红色面上),即可沿该线生成一堵易拆墙。将鼠标悬停在直立侧面上可预览防晃撑:撑顶跟随光标,靠近侧面顶部时为全高。点击即可放置。按 Esc 或右键取消。",
|
||||
};
|
||||
+7
-3
@@ -818,11 +818,13 @@ html, body {
|
||||
transition: opacity .4s ease, visibility 0s linear .4s;
|
||||
}
|
||||
.vb-pop.vb-tip.show { opacity: 1; visibility: visible; transition: none; }
|
||||
/* Draw's sway brace preview label (ui/walls.js): beside the cursor, glass like the
|
||||
view bar's popovers, amber when the spot can't take a brace. */
|
||||
/* The sway brace preview label (ui/walls.js, eased by ui/braceghost.js): beside the
|
||||
cursor, glass like the view bar's popovers, amber when the spot can't take a brace.
|
||||
It fades rather than pops, and a min-width keeps it from resizing as the text changes. */
|
||||
#brace-tip {
|
||||
position: fixed;
|
||||
z-index: 4;
|
||||
min-width: 13rem;
|
||||
max-width: 18rem;
|
||||
padding: 4px 9px;
|
||||
border: 1px solid color-mix(in srgb, #fff 9%, transparent);
|
||||
@@ -833,9 +835,11 @@ html, body {
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity .12s ease, color .12s ease, visibility 0s linear .12s;
|
||||
}
|
||||
#brace-tip.show { visibility: visible; }
|
||||
#brace-tip.show { opacity: 1; visibility: visible; transition: opacity .12s ease, color .12s ease; }
|
||||
#brace-tip.bad { color: #ffb454; }
|
||||
/* any popover open (Suggest left open during a drag, Rotate opened mid-fade) wins */
|
||||
#viewbar:has(.vb-pop:not([hidden]):not(.vb-tip)) #drag-angle { display: none; }
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* The sway brace hover preview's motion (walls.js decides WHAT it shows): the ghost
|
||||
* fades in and out instead of popping, a big jump in height cross-fades, a pointer
|
||||
* slipping off the upright side for a moment (an edge, a small facet) doesn't blink
|
||||
* it, the label eases after the cursor, and the cursor dot eases to amber on a spot
|
||||
* that can't take a brace. Nothing here changes which brace a click stands.
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import { scene, meshFrom, MODEL, clip, requestRender } from './scene.js';
|
||||
import { el } from './dom.js';
|
||||
|
||||
const FADE_MS = 120; // ghost fade in / out
|
||||
const GRACE_MS = 150; // off the side this long before the preview starts to fade
|
||||
const OPACITY = 0.45; // the wall ghost's opacity
|
||||
const CROSS_MM = 4; // a height change past this cross-fades; smaller ones swap in place
|
||||
const TIP_EASE = 0.35; // share of the gap the label closes each frame
|
||||
const CURSOR_OK = new THREE.Color(0xcffbe4), CURSOR_BAD = new THREE.Color(0xffb454);
|
||||
|
||||
const tip = el('brace-tip');
|
||||
const ghostMat = () => new THREE.MeshStandardMaterial({
|
||||
color: 0x8ff0bd, roughness: 0.7, transparent: true, opacity: 0,
|
||||
side: THREE.DoubleSide, clippingPlanes: clip,
|
||||
});
|
||||
let ghosts = []; // { mesh, on }: `on` fades it in, otherwise out and away
|
||||
let height = null; // the shown brace's height, to tell a step from a jump
|
||||
let cursorMat = null, cursorBad = false;
|
||||
let tipAt = null, tipTo = null;
|
||||
let grace = 0, graceDone = null;
|
||||
let running = false, lastT = 0;
|
||||
|
||||
/** walls.js hands over its cursor dot's material, whose colour this eases. */
|
||||
export function initBraceGhost(material) { cursorMat = material; }
|
||||
|
||||
/** Show the brace `tris` (height `h` mm); `tris` null = this spot is refused. */
|
||||
export function showBrace(tris, h, text, bad) {
|
||||
cancelGrace();
|
||||
const cur = ghosts.find((g) => g.on);
|
||||
if (!tris) {
|
||||
if (cur) cur.on = false;
|
||||
height = null;
|
||||
} else if (cur && Math.abs(h - height) <= CROSS_MM) {
|
||||
// a step: same mesh, new shape, opacity untouched
|
||||
cur.mesh.geometry.dispose();
|
||||
cur.mesh.geometry = shape(tris);
|
||||
height = h;
|
||||
} else {
|
||||
if (cur) cur.on = false;
|
||||
const mesh = meshFrom(tris, ghostMat());
|
||||
if (mesh) { mesh.layers.disable(MODEL); ghosts.push({ mesh, on: true }); }
|
||||
height = h;
|
||||
}
|
||||
cursorBad = bad;
|
||||
tip.textContent = text;
|
||||
tip.classList.toggle('bad', bad);
|
||||
tip.classList.add('show');
|
||||
kick();
|
||||
}
|
||||
|
||||
/** Where the label should sit (screen px); it eases there, or jumps if hidden. */
|
||||
export function moveTip(x, y) {
|
||||
tipTo = [x, y];
|
||||
if (!tip.classList.contains('show') || !tipAt) { tipAt = [x, y]; place(); }
|
||||
kick();
|
||||
}
|
||||
|
||||
/** Fade the preview out, after GRACE_MS when `soft` (the pointer may come straight
|
||||
* back). `then` runs when the fade starts, e.g. to hide the cursor dot. */
|
||||
export function hideBrace(soft, then) {
|
||||
if (soft) {
|
||||
if (grace) return;
|
||||
graceDone = then;
|
||||
grace = setTimeout(() => { grace = 0; const f = graceDone; graceDone = null; fadeAll(); f?.(); }, GRACE_MS);
|
||||
return;
|
||||
}
|
||||
cancelGrace();
|
||||
fadeAll();
|
||||
then?.();
|
||||
}
|
||||
|
||||
/** Gone this frame: the geometry is stale (a rebuild) or a click just placed it. */
|
||||
export function dropBrace() {
|
||||
cancelGrace();
|
||||
for (const g of ghosts) remove(g);
|
||||
ghosts = [];
|
||||
height = null;
|
||||
cursorBad = false;
|
||||
if (cursorMat) cursorMat.color.copy(CURSOR_OK);
|
||||
tip.classList.remove('show');
|
||||
tipAt = null;
|
||||
}
|
||||
|
||||
export const braceGrace = () => !!grace;
|
||||
|
||||
function cancelGrace() {
|
||||
if (grace) clearTimeout(grace);
|
||||
grace = 0;
|
||||
graceDone = null;
|
||||
}
|
||||
|
||||
function fadeAll() {
|
||||
for (const g of ghosts) g.on = false;
|
||||
height = null;
|
||||
cursorBad = false;
|
||||
tip.classList.remove('show');
|
||||
tipAt = null;
|
||||
kick();
|
||||
}
|
||||
|
||||
function shape(tris) {
|
||||
const arr = new Float32Array(tris.length * 3);
|
||||
for (let i = 0; i < tris.length; i++) arr.set(tris[i].slice(0, 3), i * 3);
|
||||
const g = new THREE.BufferGeometry();
|
||||
g.setAttribute('position', new THREE.BufferAttribute(arr, 3));
|
||||
g.computeVertexNormals();
|
||||
return g;
|
||||
}
|
||||
|
||||
function remove(g) {
|
||||
scene.remove(g.mesh);
|
||||
g.mesh.geometry.dispose();
|
||||
g.mesh.material.dispose();
|
||||
}
|
||||
|
||||
function place() {
|
||||
tip.style.left = `${tipAt[0]}px`;
|
||||
tip.style.top = `${tipAt[1]}px`;
|
||||
}
|
||||
|
||||
function kick() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
lastT = performance.now();
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
/** One animation frame: step the fades, the label and the cursor colour; stop when all settle. */
|
||||
function frame(now) {
|
||||
const step = Math.min(1, (now - lastT) / FADE_MS) * OPACITY;
|
||||
lastT = now;
|
||||
let busy = false;
|
||||
for (const g of ghosts) {
|
||||
const m = g.mesh.material, to = g.on ? OPACITY : 0;
|
||||
m.opacity = to > m.opacity ? Math.min(to, m.opacity + step) : Math.max(to, m.opacity - step);
|
||||
if (m.opacity !== to) busy = true;
|
||||
}
|
||||
for (const g of ghosts) if (!g.on && g.mesh.material.opacity === 0) remove(g);
|
||||
ghosts = ghosts.filter((g) => g.on || g.mesh.material.opacity > 0);
|
||||
if (tipAt && tipTo) {
|
||||
const dx = tipTo[0] - tipAt[0], dy = tipTo[1] - tipAt[1];
|
||||
if (Math.abs(dx) + Math.abs(dy) > 0.5) {
|
||||
tipAt = [tipAt[0] + dx * TIP_EASE, tipAt[1] + dy * TIP_EASE];
|
||||
busy = true;
|
||||
} else tipAt = [...tipTo];
|
||||
place();
|
||||
}
|
||||
if (cursorMat) {
|
||||
const to = cursorBad ? CURSOR_BAD : CURSOR_OK, c = cursorMat.color;
|
||||
if (Math.abs(c.r - to.r) + Math.abs(c.g - to.g) + Math.abs(c.b - to.b) > 0.01) {
|
||||
c.lerp(to, Math.min(1, step / OPACITY));
|
||||
busy = true;
|
||||
} else c.copy(to);
|
||||
}
|
||||
requestRender(100);
|
||||
if (busy) requestAnimationFrame(frame);
|
||||
else running = false;
|
||||
}
|
||||
+55
-26
@@ -9,6 +9,7 @@ import * as THREE from 'three';
|
||||
import { drawnWall } from '../draw.js';
|
||||
import { planGrid } from '../fins/tall.js';
|
||||
import { swayAtFace, faceIsUpright } from '../sway.js';
|
||||
import { initBraceGhost, showBrace, moveTip, hideBrace, dropBrace, braceGrace } from './braceghost.js';
|
||||
import { el } from './dom.js';
|
||||
import { viewport, renderer, scene, camera, meshFrom, ifaceMaterial, raycaster, pointer, clip, MODEL } from './scene.js';
|
||||
import { keptHit } from './section.js';
|
||||
@@ -63,7 +64,7 @@ const guideMat = (color) => new THREE.MeshBasicMaterial(
|
||||
const drawDot = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 14), guideMat(0x59d98e));
|
||||
// The cursor is see-through so the surface under it stays readable; the OS
|
||||
// crosshair is the precise aim point, this dot just shows the surface hit.
|
||||
const CURSOR_OK = 0xcffbe4, CURSOR_BAD = 0xffb454;
|
||||
const CURSOR_OK = 0xcffbe4; // braceghost.js eases it to amber on a refused brace spot
|
||||
const drawCursor = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 14),
|
||||
new THREE.MeshBasicMaterial({ color: CURSOR_OK, depthTest: false, transparent: true, opacity: 0.6 }));
|
||||
const bandGeom = new THREE.BufferGeometry()
|
||||
@@ -71,6 +72,7 @@ const bandGeom = new THREE.BufferGeometry()
|
||||
const drawBand = new THREE.Line(bandGeom,
|
||||
new THREE.LineBasicMaterial({ color: 0x6dffab, depthTest: false, transparent: true }));
|
||||
drawBand.frustumCulled = false; // its endpoints move every frame; stale bounds would cull it
|
||||
initBraceGhost(drawCursor.material);
|
||||
for (const o of [drawDot, drawCursor, drawBand]) {
|
||||
o.visible = false;
|
||||
o.renderOrder = 11; // above the hoverFace (renderOrder 1) and the part
|
||||
@@ -132,8 +134,7 @@ export function clearPreview() {
|
||||
function dropGhost() {
|
||||
if (ghostMesh) { scene.remove(ghostMesh); ghostMesh.geometry.dispose(); ghostMesh = null; }
|
||||
braceShown = null;
|
||||
braceTip.classList.remove('show');
|
||||
drawCursor.material.color.setHex(CURSOR_OK);
|
||||
dropBrace();
|
||||
}
|
||||
|
||||
/** Rebuild the committed drawn walls for the current orientation. */
|
||||
@@ -365,11 +366,11 @@ function autoSupports() {
|
||||
// The ghost IS the brace the click builds (both go through braceAt). Its top follows
|
||||
// the cursor; in the top quarter of what a brace could reach there it snaps to the
|
||||
// whole face, the height a brace always had before the clicked-height cap. A spot
|
||||
// that can't take one turns the cursor red and says why, before any click.
|
||||
// that can't take one turns the cursor amber and says why, before any click.
|
||||
// braceghost.js fades and eases what this shows, so moving about doesn't flash.
|
||||
const SNAP_TOP = 0.75;
|
||||
const STEP_Z = 2; // capped tops in 2 mm steps, so a creeping cursor reuses its ghost
|
||||
const STEP_Z = 1; // capped tops in 1 mm steps, so a creeping cursor reuses its ghost
|
||||
const MEMO_MAX = 200;
|
||||
const braceTip = el('brace-tip');
|
||||
let braceMemo = { stamp: null, map: new Map() };
|
||||
let braceShown = null; // the braceAt key the ghost and tip show now
|
||||
let braceQueued = null;
|
||||
@@ -415,8 +416,7 @@ function updateBracePreview(hit, ev) {
|
||||
drawCursor.position.copy(hit.point);
|
||||
drawCursor.visible = true;
|
||||
drawBand.visible = drawDot.visible = false;
|
||||
braceTip.style.left = `${ev.clientX + 16}px`;
|
||||
braceTip.style.top = `${ev.clientY + 16}px`;
|
||||
moveTip(ev.clientX + 16, ev.clientY + 16);
|
||||
// at most one brace build per frame, as the wall ghost
|
||||
const already = !!braceQueued;
|
||||
braceQueued = { face: hit.faceIndex, p: [hit.point.x, hit.point.y, hit.point.z] };
|
||||
@@ -427,26 +427,32 @@ function updateBracePreview(hit, ev) {
|
||||
if (!q || drawStart || !(drawActive() || braceActive())) return;
|
||||
const b = braceAt(q.face, q.p);
|
||||
if (b.key === braceShown) return;
|
||||
dropGhost();
|
||||
braceShown = b.key;
|
||||
if (b.r.ok) {
|
||||
ghostMesh = meshFrom(b.r.tris, ghostMaterial);
|
||||
ghostMesh?.layers.disable(MODEL);
|
||||
} else drawCursor.material.color.setHex(CURSOR_BAD);
|
||||
const mm = Math.round(b.r.height ?? 0);
|
||||
braceTip.textContent = !b.r.ok ? t(b.r.reason)
|
||||
: b.full ? t('Sway brace, full height: {mm} mm', { mm })
|
||||
: t('Sway brace, {mm} mm tall — move up the side for full height', { mm });
|
||||
braceTip.classList.toggle('bad', !b.r.ok);
|
||||
braceTip.classList.add('show');
|
||||
showBrace(b.r.ok ? b.r.tris : null, b.r.height ?? 0,
|
||||
!b.r.ok ? t(b.r.reason)
|
||||
: b.full ? t('Brace · full height {mm} mm', { mm })
|
||||
: t('Brace · {mm} mm · move up for full height', { mm }),
|
||||
!b.r.ok);
|
||||
});
|
||||
}
|
||||
|
||||
renderer.domElement.addEventListener('pointerleave', () => {
|
||||
braceQueued = null;
|
||||
if (!drawStart) { drawCursor.visible = false; dropGhost(); }
|
||||
if (!drawStart) { drawCursor.visible = false; braceShown = null; hideBrace(false); }
|
||||
});
|
||||
|
||||
/** The pointer is on the part but off an upright side: the dot follows it, and the
|
||||
* brace stays a moment (an edge or a small facet on the way) before it fades. */
|
||||
function slipOff(hit, keepCursor) {
|
||||
braceQueued = null;
|
||||
if (!braceShown) return false;
|
||||
braceShown = null;
|
||||
drawCursor.position.copy(hit.point);
|
||||
hideBrace(true, keepCursor ? null : () => { drawCursor.visible = false; });
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Stand a sway brace on the upright face the user clicked: the one its preview showed. */
|
||||
function placeSway(hit) {
|
||||
braceQueued = null;
|
||||
@@ -503,14 +509,19 @@ export function drawHover(ev) {
|
||||
updateBracePreview(hit, ev);
|
||||
renderer.domElement.style.cursor = 'crosshair';
|
||||
} else if (hit) {
|
||||
braceQueued = null;
|
||||
if (braceShown) dropGhost();
|
||||
slipOff(hit, true); // Draw's own cursor carries on over the rest of the part
|
||||
updatePreview(hit.point);
|
||||
renderer.domElement.style.cursor = 'crosshair';
|
||||
} else if (!drawStart) {
|
||||
drawBand.visible = false;
|
||||
braceAway(); // off the part: a gap crossed on the way doesn't blink the brace
|
||||
renderer.domElement.style.cursor = '';
|
||||
} else {
|
||||
braceQueued = null;
|
||||
drawCursor.visible = drawBand.visible = false;
|
||||
dropGhost();
|
||||
braceShown = null;
|
||||
hideBrace(false);
|
||||
if (ghostMesh) { scene.remove(ghostMesh); ghostMesh.geometry.dispose(); ghostMesh = null; }
|
||||
renderer.domElement.style.cursor = '';
|
||||
}
|
||||
}
|
||||
@@ -524,15 +535,33 @@ export function braceHover(ev) {
|
||||
renderer.domElement.style.cursor = 'crosshair';
|
||||
return true;
|
||||
}
|
||||
dropBracePreview();
|
||||
if (hit && (slipOff(hit, false) || braceGrace())) {
|
||||
drawCursor.position.copy(hit.point); // still fading in its grace: keep following
|
||||
return false;
|
||||
}
|
||||
if (!hit) braceAway();
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Hide braceActive's cursor, ghost and tip (off an upright side, or while an orbit
|
||||
* or a ring drag has the pointer). */
|
||||
/** The pointer left the part (a gap, the background) or is over the rotate rings
|
||||
* without dragging: the same grace as slipOff, then the preview and its dot go. */
|
||||
export function braceAway() {
|
||||
braceQueued = null;
|
||||
if (!braceShown && !braceGrace()) {
|
||||
if (drawCursor.visible) { drawCursor.visible = false; renderer.domElement.style.cursor = ''; }
|
||||
return;
|
||||
}
|
||||
braceShown = null;
|
||||
hideBrace(true, () => { drawCursor.visible = false; renderer.domElement.style.cursor = ''; });
|
||||
}
|
||||
|
||||
/** Fade braceActive's ghost and tip and hide its cursor (an orbit or a ring drag
|
||||
* has the pointer). */
|
||||
export function dropBracePreview() {
|
||||
braceQueued = null;
|
||||
if (drawCursor.visible) { drawCursor.visible = false; dropGhost(); renderer.domElement.style.cursor = ''; }
|
||||
braceShown = null;
|
||||
hideBrace(false);
|
||||
if (drawCursor.visible) { drawCursor.visible = false; renderer.domElement.style.cursor = ''; }
|
||||
}
|
||||
|
||||
/** braceActive's click: select a support placed by hand, or stand a brace on an
|
||||
|
||||
Reference in new issue
Block a user