feat(stereo3d): Фаза 3 — readout-панель, точки на гранях, подписи вершин сечения
- live-readout overlay: тип сечения, площадь, периметр, последнее измерение (через info().readout; _notify добавлен в section/measure-пути) - _raycastFace(): в режиме точек клик по грани ставит точку на поверхности - подписи вершин сечения буквами K,L,M… (наклонное/произвольное/3-точки, ≤12 вершин) - bump stereo.js?v=6 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -401,6 +401,7 @@ class StereoSim {
|
||||
if (!this._measurements.length) return;
|
||||
this._measurements.pop();
|
||||
this._rebuildMeasureGroup();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
clearMeasurements() {
|
||||
@@ -408,6 +409,7 @@ class StereoSim {
|
||||
this._measurePicks = [];
|
||||
this._rebuildMeasureGroup();
|
||||
this._clearGroup(this._measurePickGroup);
|
||||
this._notify();
|
||||
}
|
||||
|
||||
_rebuildMeasureGroup() {
|
||||
@@ -724,6 +726,41 @@ class StereoSim {
|
||||
return this._polygonArea(this._sectionPolygon);
|
||||
}
|
||||
|
||||
_polygonPerimeter(pts) {
|
||||
let p = 0;
|
||||
for (let i = 0; i < pts.length; i++) p += pts[i].distanceTo(pts[(i + 1) % pts.length]);
|
||||
return p;
|
||||
}
|
||||
|
||||
// Live, human-readable lines for the viewport readout panel.
|
||||
getReadout() {
|
||||
const lines = [];
|
||||
const r = (v) => Math.round(v * 100) / 100;
|
||||
const curved = ['cylinder', 'cone', 'trunccone', 'sphere'].includes(this.figureType);
|
||||
|
||||
if (this._section3PData) {
|
||||
const d = this._section3PData;
|
||||
lines.push({ label: 'Сечение (3 точки)', value: d.typeName });
|
||||
lines.push({ label: 'Площадь S', value: r(d.area) });
|
||||
lines.push({ label: 'Периметр P', value: r(this._polygonPerimeter(d.polygon)) });
|
||||
} else if (this.showSection && this._sectionPolygon && this._sectionPolygon.length >= 3) {
|
||||
const poly = this._sectionPolygon;
|
||||
const polyName = (curved && poly.length > 8)
|
||||
? (this.figureType === 'sphere' || this.sectionType === 'horizontal' ? 'окружность' : 'эллипс')
|
||||
: ({ 3: 'треугольник', 4: 'четырёхугольник', 5: 'пятиугольник', 6: 'шестиугольник' }[poly.length] || `${poly.length}-угольник`);
|
||||
const kind = { horizontal: 'горизонтальное', diagonal: 'наклонное', custom: 'произвольное' }[this.sectionType] || '';
|
||||
lines.push({ label: 'Сечение' + (kind ? ` (${kind})` : ''), value: polyName });
|
||||
lines.push({ label: 'Площадь S', value: r(this.getSectionArea()) });
|
||||
lines.push({ label: 'Периметр P', value: r(this._polygonPerimeter(poly)) });
|
||||
}
|
||||
|
||||
if (this._measurements.length) {
|
||||
const m = this._measurements[this._measurements.length - 1];
|
||||
lines.push({ label: `Отрезок ${m.from}${m.to}`, value: m.dist });
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
info() {
|
||||
const f = this.getFormulas();
|
||||
return {
|
||||
@@ -734,6 +771,7 @@ class StereoSim {
|
||||
circumscribedR: this.showCircumscribed ? this._circumscribedRadius() : null,
|
||||
customPoints: this._customPoints.length,
|
||||
connections: this._connections.length,
|
||||
readout: this.getReadout(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1511,6 +1549,7 @@ class StereoSim {
|
||||
}
|
||||
}
|
||||
}
|
||||
this._notify();
|
||||
}
|
||||
|
||||
_figureHeight() {
|
||||
@@ -1744,16 +1783,22 @@ class StereoSim {
|
||||
label.scale.set(1.6, 0.5, 1);
|
||||
this._sectionGroup.add(label);
|
||||
|
||||
// Vertex markers only for genuine polygons; smooth conic sections (many
|
||||
// sampled points) would otherwise render a ring of dozens of spheres.
|
||||
// Vertex markers + letter labels for genuine polygons; smooth conic
|
||||
// sections (many sampled points) would otherwise render dozens of spheres.
|
||||
if (pts.length <= 12) {
|
||||
for (const p of pts) {
|
||||
const LETTERS = 'KLMNPQRSTUV';
|
||||
pts.forEach((p, i) => {
|
||||
const sGeo = new THREE.SphereGeometry(0.06, 8, 8);
|
||||
const sMat = new THREE.MeshBasicMaterial({ color: 0x06D6E0 });
|
||||
const sm = new THREE.Mesh(sGeo, sMat);
|
||||
sm.position.copy(p);
|
||||
this._sectionGroup.add(sm);
|
||||
}
|
||||
// letter label pushed slightly outward from the section centroid
|
||||
const lbl = this._makeTextSprite(LETTERS[i] || `${i + 1}`, '#06D6E0', 34);
|
||||
const out = new THREE.Vector3().subVectors(p, c).normalize().multiplyScalar(0.32);
|
||||
lbl.position.copy(p).add(out).add(new THREE.Vector3(0, 0.18, 0));
|
||||
this._sectionGroup.add(lbl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2221,14 +2266,20 @@ class StereoSim {
|
||||
const outlineMat = new THREE.LineBasicMaterial({ color: SECT_COLOR, linewidth: 2 });
|
||||
this._section3PGroup.add(new THREE.Line(outlineGeo, outlineMat));
|
||||
|
||||
// Vertex markers — only for true polygons; smooth conic sections skip them.
|
||||
// Vertex markers + letter labels — only for true polygons; smooth conic
|
||||
// sections skip them.
|
||||
if (polygon.length <= 12) {
|
||||
polygon.forEach(p => {
|
||||
const LETTERS = 'KLMNPQRSTUV';
|
||||
polygon.forEach((p, i) => {
|
||||
const sg = new THREE.SphereGeometry(0.07, 8, 8);
|
||||
const sm = new THREE.MeshBasicMaterial({ color: SECT_COLOR });
|
||||
const s = new THREE.Mesh(sg, sm);
|
||||
s.position.copy(p);
|
||||
this._section3PGroup.add(s);
|
||||
const lbl = this._makeTextSprite(LETTERS[i] || `${i + 1}`, '#7BF5A4', 32);
|
||||
const out = new THREE.Vector3().subVectors(p, c).normalize().multiplyScalar(0.3);
|
||||
lbl.position.copy(p).add(out).add(new THREE.Vector3(0, 0.16, 0));
|
||||
this._section3PGroup.add(lbl);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2308,6 +2359,7 @@ class StereoSim {
|
||||
this._measurePicks = [];
|
||||
this._clearGroup(this._measurePickGroup);
|
||||
this._rebuildMeasureGroup();
|
||||
this._notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2334,6 +2386,17 @@ class StereoSim {
|
||||
return { dist: Math.hypot(mx - px, my - py), t };
|
||||
}
|
||||
|
||||
// Raycast against the figure's solid mesh → a point on a face interior.
|
||||
_raycastFace(mx, my) {
|
||||
if (!this._raycaster) this._raycaster = new THREE.Raycaster();
|
||||
this._raycaster.setFromCamera({ x: mx, y: my }, this.camera);
|
||||
const hits = this._raycaster.intersectObjects(this._figGroup.children, true);
|
||||
for (const h of hits) {
|
||||
if (h.object && h.object.type === 'Mesh') return h.point.clone();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_onPointClick(e) {
|
||||
const { mx, my } = this._screenCoords(e);
|
||||
|
||||
@@ -2365,7 +2428,7 @@ class StereoSim {
|
||||
}
|
||||
}
|
||||
|
||||
// Also check: click near a vertex <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> snap to vertex
|
||||
// Also check: click near a vertex → snap to vertex
|
||||
for (const v of this._vertices) {
|
||||
const proj = v.pos.clone().project(this.camera);
|
||||
const dist = Math.sqrt((mx - proj.x) ** 2 + (my - proj.y) ** 2);
|
||||
@@ -2377,6 +2440,12 @@ class StereoSim {
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to a point on a face interior (raycast) when not near edge/vertex.
|
||||
if (!bestPos) {
|
||||
const fp = this._raycastFace(mx, my);
|
||||
if (fp) { bestPos = fp; bestEdge = -3; bestT = 0; }
|
||||
}
|
||||
|
||||
if (!bestPos) return;
|
||||
|
||||
const label = String(this._nextPointId++);
|
||||
@@ -4053,6 +4122,21 @@ class StereoSim {
|
||||
|
||||
// Section-3P panel
|
||||
_stereoUpdateSection3PPanel();
|
||||
|
||||
// Live readout overlay (section type/area/perimeter, last measurement)
|
||||
_stereoUpdateReadout(info);
|
||||
}
|
||||
|
||||
function _stereoUpdateReadout(info) {
|
||||
const el = document.getElementById('stereo-readout');
|
||||
if (!el) return;
|
||||
const lines = (info && info.readout) || [];
|
||||
if (!lines.length) { el.style.display = 'none'; el.innerHTML = ''; return; }
|
||||
el.style.display = '';
|
||||
el.innerHTML = lines.map(l =>
|
||||
'<div class="st-ro-row"><span class="st-ro-k">' + l.label + '</span>' +
|
||||
'<span class="st-ro-v">' + l.value + '</span></div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
function _stereoUpdatePointsInfo(info) {
|
||||
|
||||
Reference in New Issue
Block a user