feat(stereo): выделение цветом — многоугольник по точкам (с палитрой)
Новый инструмент «Многоугольник по точкам» (секция «Выделение цветом»): кликаешь точки/вершины по контуру → «Замкнуть» (или клик по первой точке) → область заливается полупрозрачным цветом + контур + вершины. Палитра из 6 цветов (свотчи), переключается. Можно выделить треугольник/грань/сечение из выбранных точек, чтобы подсветить «фигуру по точкам». - StereoSim: _polyMode/_polyPicks/_polyHighlights/_polyColor + _polyGroup; setPolyMode (взаимоисключение с другими инструментами), setPolyColor, closePoly (≥3 точек), removeLastPolyPick, clearPoly, _onPolyClick (авто-замыкание кликом по первой вершине), _rebuildPoly/_drawPolyHighlight/ _drawPolyPreview (превью: пунктир + крупная 1-я точка-подсказка). Пикинг вершин/точек через _pickConstructPoint. Сброс в setFigure, очистка в dispose. - Панель: секция «Выделение цветом» (кнопка, палитра .st-sw, Замкнуть/ Отменить точку/Очистить, #poly-hint); glue stereoPolyMode/Color/Close/ Undo/Clear; интеграция в _stereoDeactivateTools. CSS палитры в lab.css. Верификация: node --check OK; headless-смоук 21/21 (режим+взаимоисключение, пик→замыкание, дефолт/выбранный цвет, авто-замыкание по 1-й точке, требование ≥3, undo точки/выделения, clear, setFigure-сброс, dispose, счётчики fill+контур+вершины); эмодзи/eval/new Function — 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -413,6 +413,15 @@
|
||||
.stereo-panel .st-acc-body { margin: 0 0 8px; padding: 0 1px; }
|
||||
.stereo-panel .st-sublabel { opacity: .8; margin: 8px 0 6px; }
|
||||
|
||||
/* highlight-polygon colour palette */
|
||||
.stereo-panel .st-poly-palette { display: flex; gap: 6px; margin: 4px 0 2px; flex-wrap: wrap; }
|
||||
.stereo-panel .st-sw {
|
||||
width: 20px; height: 20px; border-radius: 50%; cursor: pointer; padding: 0;
|
||||
border: 2px solid rgba(255,255,255,.25); transition: transform .1s, border-color .12s, box-shadow .12s;
|
||||
}
|
||||
.stereo-panel .st-sw:hover { transform: scale(1.12); }
|
||||
.stereo-panel .st-sw.active { border-color: #fff; box-shadow: 0 0 0 2px rgba(255,255,255,.25); }
|
||||
|
||||
.gp-preset-group { margin-bottom: 8px; }
|
||||
.gp-preset-label {
|
||||
font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
|
||||
|
||||
+159
-3
@@ -79,7 +79,7 @@ class StereoSim {
|
||||
this._velX = 0; this._velY = 0;
|
||||
try { el.setPointerCapture(e.pointerId); } catch (_) {}
|
||||
if (this._panning) el.style.cursor = 'move';
|
||||
else if (!this._pointMode && !this._connectMode && !this._measureMode && !this._angleMode && !this._section3PMode && !this._lineMode && !this._planeMode && !this._relMode && !this._divideMode && !this._dragPointMode) el.style.cursor = 'grabbing';
|
||||
else if (!this._pointMode && !this._connectMode && !this._measureMode && !this._angleMode && !this._section3PMode && !this._lineMode && !this._planeMode && !this._relMode && !this._divideMode && !this._dragPointMode && !this._polyMode) el.style.cursor = 'grabbing';
|
||||
this._invalidate();
|
||||
});
|
||||
on(el, 'contextmenu', e => e.preventDefault()); // allow right-drag pan without menu
|
||||
@@ -106,6 +106,7 @@ class StereoSim {
|
||||
else if (this._section3PMode) { el.style.cursor = 'crosshair'; if (!wasDrag) this._onSection3PClick(e); }
|
||||
else if (this._lineMode || this._planeMode || this._divideMode) { el.style.cursor = 'crosshair'; if (!wasDrag) this._onConstructClick(e); }
|
||||
else if (this._relMode) { el.style.cursor = 'crosshair'; if (!wasDrag) this._onRelClick(e); }
|
||||
else if (this._polyMode) { el.style.cursor = 'crosshair'; if (!wasDrag) this._onPolyClick(e); }
|
||||
else el.style.cursor = 'grab';
|
||||
this._invalidate();
|
||||
});
|
||||
@@ -347,6 +348,15 @@ class StereoSim {
|
||||
this._redoStack = [];
|
||||
this._undoMax = 60;
|
||||
|
||||
/* ── Highlight polygons by points (colour fill) ── */
|
||||
this._polyMode = false; // click points → close → filled coloured polygon
|
||||
this._polyPicks = []; // Vector3[] in-progress vertices
|
||||
this._polyHighlights = []; // [{id, pts:[{x,y,z}], color}]
|
||||
this._polyColor = 0xF59E0B; // current highlight colour
|
||||
this._polySeq = 0;
|
||||
this._polyGroup = new THREE.Group();
|
||||
this.scene.add(this._polyGroup);
|
||||
|
||||
this.onUpdate = null;
|
||||
|
||||
this._buildGrid();
|
||||
@@ -393,6 +403,8 @@ class StereoSim {
|
||||
this._relMode = null; this._sectionPlaneId = null; this._lastConstructMsg = '';
|
||||
this._divideMode = false; this._dragPointMode = false; this._draggingCP = null; this._dragPlane = null;
|
||||
this._undoStack = []; this._redoStack = [];
|
||||
this._polyMode = false; this._polyPicks = []; this._polyHighlights = [];
|
||||
this._clearGroup(this._polyGroup);
|
||||
this._constructPicks = [];
|
||||
this._nextLineName = 0; this._nextPlaneName = 0; this._nextCPointName = 0; this._constructSeq = 0;
|
||||
this._clearGroup(this._constructGroup);
|
||||
@@ -1001,7 +1013,7 @@ class StereoSim {
|
||||
[this._figGroup, this._labelGroup, this._sectionGroup, this._sphereGroup,
|
||||
this._measureGroup, this._measurePickGroup, this._gridGroup, this._markGroup,
|
||||
this._derivedGroup, this._section3PGroup, this._angleGroup, this._pointGroup,
|
||||
this._constructGroup]
|
||||
this._constructGroup, this._polyGroup]
|
||||
.forEach(g => g && this._clearGroup(g));
|
||||
if (this._tooltipEl && this._tooltipEl.parentNode) this._tooltipEl.parentNode.removeChild(this._tooltipEl);
|
||||
if (this.renderer) {
|
||||
@@ -4208,6 +4220,118 @@ class StereoSim {
|
||||
this._dragPlane = null;
|
||||
}
|
||||
|
||||
/* ── Highlight polygon by points (colour fill) ── */
|
||||
|
||||
setPolyMode(on) {
|
||||
this._polyMode = on;
|
||||
if (on) {
|
||||
this._lineMode = false; this._planeMode = false; this._divideMode = false;
|
||||
this._dragPointMode = false; this._intersectMode = false; this._intersectSel = [];
|
||||
this._relMode = null; this._constructPicks = [];
|
||||
} else {
|
||||
this._polyPicks = [];
|
||||
}
|
||||
this.renderer.domElement.style.cursor = on ? 'crosshair' : 'grab';
|
||||
this._rebuildPoly();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
setPolyColor(color) {
|
||||
this._polyColor = color;
|
||||
this._rebuildPoly();
|
||||
}
|
||||
|
||||
// Close the in-progress polygon into a filled highlight. Returns true if closed.
|
||||
closePoly() {
|
||||
if (this._polyPicks.length < 3) return false;
|
||||
this._polyHighlights.push({
|
||||
id: 'H' + (this._polySeq++),
|
||||
pts: this._polyPicks.map(p => ({ x: p.x, y: p.y, z: p.z })),
|
||||
color: this._polyColor,
|
||||
});
|
||||
this._polyPicks = [];
|
||||
this._rebuildPoly();
|
||||
this._notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
removeLastPolyPick() {
|
||||
if (this._polyPicks.length) this._polyPicks.pop();
|
||||
else if (this._polyHighlights.length) this._polyHighlights.pop();
|
||||
this._rebuildPoly();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
clearPoly() {
|
||||
this._polyPicks = [];
|
||||
this._polyHighlights = [];
|
||||
this._rebuildPoly();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
_onPolyClick(e) {
|
||||
const pos = this._pickConstructPoint(e);
|
||||
if (!pos) return;
|
||||
// click the first vertex again → close the loop
|
||||
if (this._polyPicks.length >= 3 && pos.distanceTo(this._polyPicks[0]) < 1e-3) { this.closePoly(); return; }
|
||||
const last = this._polyPicks[this._polyPicks.length - 1];
|
||||
if (last && last.distanceTo(pos) < 1e-4) return; // ignore duplicate click
|
||||
this._polyPicks.push(pos);
|
||||
if (window.LabFX) LabFX.sound.play('tick', { pitch: 1.5, volume: 0.25 });
|
||||
this._rebuildPoly();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
_rebuildPoly() {
|
||||
if (!this._polyGroup) return;
|
||||
this._clearGroup(this._polyGroup);
|
||||
for (const h of this._polyHighlights) this._drawPolyHighlight(h);
|
||||
if (this._polyPicks.length) this._drawPolyPreview();
|
||||
this._invalidate();
|
||||
}
|
||||
|
||||
_drawPolyHighlight(h) {
|
||||
const pts = h.pts.map(p => new THREE.Vector3(p.x, p.y, p.z));
|
||||
if (pts.length >= 3) {
|
||||
const positions = [], indices = [];
|
||||
pts.forEach(p => positions.push(p.x, p.y, p.z));
|
||||
for (let i = 1; i < pts.length - 1; i++) indices.push(0, i, i + 1);
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
geo.setIndex(indices);
|
||||
geo.computeVertexNormals();
|
||||
const fill = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({
|
||||
color: h.color, transparent: true, opacity: 0.34, side: THREE.DoubleSide, depthWrite: false }));
|
||||
fill.renderOrder = 2;
|
||||
this._polyGroup.add(fill);
|
||||
}
|
||||
const outline = new THREE.Line(new THREE.BufferGeometry().setFromPoints([...pts, pts[0]]),
|
||||
new THREE.LineBasicMaterial({ color: h.color, transparent: true, opacity: 0.95 }));
|
||||
outline.renderOrder = 3;
|
||||
this._polyGroup.add(outline);
|
||||
for (const p of pts) {
|
||||
const s = new THREE.Mesh(new THREE.SphereGeometry(0.08, 10, 10), new THREE.MeshBasicMaterial({ color: h.color }));
|
||||
s.position.copy(p); s.renderOrder = 4;
|
||||
this._polyGroup.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
_drawPolyPreview() {
|
||||
const pts = this._polyPicks;
|
||||
if (pts.length >= 2) {
|
||||
const ln = new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),
|
||||
new THREE.LineDashedMaterial({ color: this._polyColor, dashSize: 0.14, gapSize: 0.08, transparent: true, opacity: 0.9 }));
|
||||
ln.computeLineDistances();
|
||||
this._polyGroup.add(ln);
|
||||
}
|
||||
pts.forEach((p, i) => {
|
||||
const r = (i === 0 && pts.length >= 3) ? 0.15 : 0.09; // bigger 1st dot = "click to close"
|
||||
const s = new THREE.Mesh(new THREE.SphereGeometry(r, 12, 12), new THREE.MeshBasicMaterial({ color: this._polyColor }));
|
||||
s.position.copy(p); s.renderOrder = 5;
|
||||
this._polyGroup.add(s);
|
||||
});
|
||||
}
|
||||
|
||||
_createLine(pA, pB) {
|
||||
if (pA.distanceTo(pB) < 1e-6) return null;
|
||||
this._pushHistory();
|
||||
@@ -4921,7 +5045,7 @@ class StereoSim {
|
||||
'stereo-derive-mid-btn','stereo-derive-fc-btn','stereo-derive-alt-btn','stereo-derive-cen-btn',
|
||||
'stereo-sect3p-btn','stereo-line-btn','stereo-plane-btn','stereo-intersect-btn',
|
||||
'stereo-rel-lpar-btn','stereo-rel-lperp-btn','stereo-rel-ppar-btn','stereo-rel-pperp-btn',
|
||||
'stereo-divide-btn','stereo-dragpt-btn'].forEach(id => {
|
||||
'stereo-divide-btn','stereo-dragpt-btn','stereo-poly-btn'].forEach(id => {
|
||||
document.getElementById(id)?.classList.remove('active');
|
||||
});
|
||||
if (stereoSim) {
|
||||
@@ -4938,6 +5062,7 @@ class StereoSim {
|
||||
stereoSim.setRelMode(null);
|
||||
stereoSim.setDivideMode(false);
|
||||
stereoSim.setDragPointMode(false);
|
||||
stereoSim.setPolyMode(false);
|
||||
}
|
||||
const hint = document.getElementById('angle-hint');
|
||||
if (hint) hint.textContent = '';
|
||||
@@ -5178,6 +5303,37 @@ class StereoSim {
|
||||
if (h) h.textContent = on ? 'Тащите построенные точки (M, N…) мышью — двигаются в плоскости экрана' : '';
|
||||
}
|
||||
|
||||
/* ── Highlight polygon by points (colour fill) ── */
|
||||
function stereoPolyMode(btn) {
|
||||
const on = !btn.classList.contains('active');
|
||||
_stereoDeactivateTools();
|
||||
btn.classList.toggle('active', on);
|
||||
if (stereoSim) stereoSim.setPolyMode(on);
|
||||
const h = document.getElementById('poly-hint');
|
||||
if (h) h.textContent = on ? 'Кликайте точки/вершины по контуру; клик по первой или «Замкнуть» — заливка' : '';
|
||||
}
|
||||
|
||||
function stereoPolyColor(hex, btn) {
|
||||
document.querySelectorAll('#sim-stereo .st-sw').forEach(b => b.classList.remove('active'));
|
||||
if (btn) btn.classList.add('active');
|
||||
if (stereoSim) stereoSim.setPolyColor(parseInt(hex, 16));
|
||||
}
|
||||
|
||||
function stereoPolyClose() {
|
||||
if (!stereoSim) return;
|
||||
const done = stereoSim.closePoly();
|
||||
const h = document.getElementById('poly-hint');
|
||||
if (h) h.textContent = done ? 'Многоугольник выделен. Можно начать новый контур.' : 'Нужно минимум 3 точки';
|
||||
}
|
||||
|
||||
function stereoPolyUndo() { if (stereoSim) stereoSim.removeLastPolyPick(); }
|
||||
|
||||
function stereoPolyClear() {
|
||||
if (stereoSim) stereoSim.clearPoly();
|
||||
const h = document.getElementById('poly-hint');
|
||||
if (h) h.textContent = '';
|
||||
}
|
||||
|
||||
function _stereoUpdateConstructList() {
|
||||
const el = document.getElementById('construct-list');
|
||||
if (!el || !stereoSim) return;
|
||||
|
||||
@@ -3676,6 +3676,30 @@
|
||||
</div>
|
||||
<div id="points-info" style="font-size:0.65rem;color:rgba(255,255,255,0.4);margin-top:2px"></div>
|
||||
|
||||
<!-- ── Выделение цветом (многоугольник по точкам) ── -->
|
||||
<div class="gp-section-title" style="margin-top:8px;margin-bottom:6px">Выделение цветом</div>
|
||||
<div class="st-tool-grid">
|
||||
<button class="st-tool-btn st-tool-btn-wide" id="stereo-poly-btn" onclick="stereoPolyMode(this)" title="Кликайте точки/вершины по контуру → область заливается выбранным цветом">
|
||||
<svg viewBox="0 0 24 24"><polygon points="4,20 9,5 19,9 16,20" fill="currentColor" fill-opacity="0.25"/><circle cx="4" cy="20" r="1.8" fill="currentColor"/><circle cx="9" cy="5" r="1.8" fill="currentColor"/><circle cx="19" cy="9" r="1.8" fill="currentColor"/><circle cx="16" cy="20" r="1.8" fill="currentColor"/></svg>Многоугольник по точкам
|
||||
</button>
|
||||
</div>
|
||||
<div class="st-poly-palette">
|
||||
<button class="st-sw active" style="background:#F59E0B" onclick="stereoPolyColor('F59E0B',this)" title="Янтарный"></button>
|
||||
<button class="st-sw" style="background:#06D6E0" onclick="stereoPolyColor('06D6E0',this)" title="Бирюзовый"></button>
|
||||
<button class="st-sw" style="background:#EF476F" onclick="stereoPolyColor('EF476F',this)" title="Розовый"></button>
|
||||
<button class="st-sw" style="background:#7BF5A4" onclick="stereoPolyColor('7BF5A4',this)" title="Зелёный"></button>
|
||||
<button class="st-sw" style="background:#C4B5FD" onclick="stereoPolyColor('C4B5FD',this)" title="Фиолетовый"></button>
|
||||
<button class="st-sw" style="background:#60A5FA" onclick="stereoPolyColor('60A5FA',this)" title="Синий"></button>
|
||||
</div>
|
||||
<div class="st-action-grid" style="margin-top:4px">
|
||||
<button class="st-action-btn" onclick="stereoPolyClose()">Замкнуть</button>
|
||||
<button class="st-action-btn" onclick="stereoPolyUndo()">Отменить точку</button>
|
||||
</div>
|
||||
<div class="st-action-grid" style="margin-top:3px">
|
||||
<button class="st-action-btn st-tool-btn-wide" onclick="stereoPolyClear()" style="grid-column:span 2">Очистить выделения</button>
|
||||
</div>
|
||||
<div id="poly-hint" style="font-size:0.63rem;color:rgba(255,255,255,0.38);margin-top:3px;line-height:1.4"></div>
|
||||
|
||||
<!-- ── Построения (прямые / плоскости) ── -->
|
||||
<div class="gp-section-title" style="margin-top:8px;margin-bottom:6px">Построения</div>
|
||||
<div class="st-tool-grid">
|
||||
|
||||
Reference in New Issue
Block a user