feat(stereo): A2 — пересечения построений + интерактивное дерево объектов
Фаза A2 раунда «Конструктор». Пересечения как list-based операция: - прямая ∩ плоскость → точка (_cpoints, имена M,N,K…); - плоскость ∩ плоскость → прямая; - прямая ∩ прямая → точка либо «скрещиваются»/«параллельны». Точки-пересечения пикабельны — по ним строятся новые прямые/плоскости. - StereoSim: setIntersectMode/pickConstructObject (выбор 2 объектов), _computeIntersection + _intersectLinePlane/_intersectPlanePlane/ _intersectLineLine, _createCPoint/_drawCPointObject/_cpointLabel, removeConstruction(id)/toggleConstructionVis(id), getConstructions переписан в дерево (id/type/hidden/selected/info), _pickConstructPoint теперь учитывает точки-пересечения. Сброс в setFigure, очистка/clear. - Панель: кнопка «Пересечение»; список — интерактивные строки (выбор для пересечения, глаз=видимость, ×=удаление) через glue stereoIntersectMode/ ConstructSelect/ConstructVis/ConstructDelete; интеграция в _stereoDeactivateTools. Верификация: node --check OK; headless-смоук 34/34 (точная геометрия line∩plane / plane∩plane / line∩line, параллельные/скрещ. без объекта, list-pick поток, гард точки, дерево, видимость/удаление/remove-last, setFigure-сброс, dispose); эмодзи/eval/new Function — 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+259
-22
@@ -292,15 +292,19 @@ class StereoSim {
|
||||
|
||||
/* ── Construction layer (Phase A): lines & planes as named objects ──
|
||||
Stored serialisably as plain {x,y,z}; rebuilt into _constructGroup. */
|
||||
this._lines = []; // [{id,seq,name, a:{x,y,z}, b:{x,y,z}, color}]
|
||||
this._planes = []; // [{id,seq,name, point:{x,y,z}, normal:{x,y,z}, def:[{x,y,z}×3], color}]
|
||||
this._cpoints = []; // [{id,seq,name, pos:{x,y,z}, color, hidden}] — construction points (intersections)
|
||||
this._lines = []; // [{id,seq,name, a:{x,y,z}, b:{x,y,z}, color, hidden}]
|
||||
this._planes = []; // [{id,seq,name, point:{x,y,z}, normal:{x,y,z}, def:[{x,y,z}×3], color, hidden}]
|
||||
this._constructGroup = new THREE.Group();
|
||||
this.scene.add(this._constructGroup);
|
||||
this._lineMode = false; // pick 2 points → infinite line
|
||||
this._planeMode = false; // pick 3 points → plane (+ its cross-section of the solid)
|
||||
this._intersectMode = false; // list-based: select 2 objects → intersection
|
||||
this._intersectSel = []; // ids of objects selected for intersection
|
||||
this._constructPicks = []; // temp Vector3 picks for the active construction tool
|
||||
this._nextLineName = 0; // → a, b, c, …
|
||||
this._nextPlaneName = 0; // → α, β, γ, …
|
||||
this._nextCPointName = 0; // → M, N, K, …
|
||||
this._constructSeq = 0; // monotonic insertion order (for "remove last")
|
||||
|
||||
this.onUpdate = null;
|
||||
@@ -343,10 +347,11 @@ class StereoSim {
|
||||
this._section3PMode = false;
|
||||
this._section3PStep = 0;
|
||||
this._clearGroup(this._section3PGroup);
|
||||
this._lines = []; this._planes = [];
|
||||
this._cpoints = []; this._lines = []; this._planes = [];
|
||||
this._lineMode = false; this._planeMode = false;
|
||||
this._intersectMode = false; this._intersectSel = [];
|
||||
this._constructPicks = [];
|
||||
this._nextLineName = 0; this._nextPlaneName = 0; this._constructSeq = 0;
|
||||
this._nextLineName = 0; this._nextPlaneName = 0; this._nextCPointName = 0; this._constructSeq = 0;
|
||||
this._clearGroup(this._constructGroup);
|
||||
this._buildFigure();
|
||||
this._notify();
|
||||
@@ -3605,31 +3610,60 @@ class StereoSim {
|
||||
|
||||
removeLastConstruction() {
|
||||
let best = -1, arr = null, idx = -1;
|
||||
this._lines.forEach((l, i) => { if (l.seq > best) { best = l.seq; arr = this._lines; idx = i; } });
|
||||
this._planes.forEach((p, i) => { if (p.seq > best) { best = p.seq; arr = this._planes; idx = i; } });
|
||||
const scan = (a) => a.forEach((o, i) => { if (o.seq > best) { best = o.seq; arr = a; idx = i; } });
|
||||
scan(this._cpoints); scan(this._lines); scan(this._planes);
|
||||
if (!arr) return;
|
||||
arr.splice(idx, 1);
|
||||
this._rebuildConstructions();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
// Delete one object by id (point / line / plane).
|
||||
removeConstruction(id) {
|
||||
for (const a of [this._cpoints, this._lines, this._planes]) {
|
||||
const i = a.findIndex(o => o.id === id);
|
||||
if (i >= 0) { a.splice(i, 1); break; }
|
||||
}
|
||||
this._intersectSel = this._intersectSel.filter(x => x !== id);
|
||||
this._rebuildConstructions();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
// Toggle the visibility of one object by id (kept in the tree, hidden in 3D).
|
||||
toggleConstructionVis(id) {
|
||||
const o = this._findObj(id);
|
||||
if (o) { o.obj.hidden = !o.obj.hidden; this._rebuildConstructions(); this._notify(); }
|
||||
}
|
||||
|
||||
clearConstructions() {
|
||||
this._lines = []; this._planes = []; this._constructPicks = [];
|
||||
this._cpoints = []; this._lines = []; this._planes = []; this._constructPicks = [];
|
||||
this._lineMode = false; this._planeMode = false;
|
||||
this._intersectMode = false; this._intersectSel = [];
|
||||
this._clearGroup(this._constructGroup);
|
||||
this._notify();
|
||||
}
|
||||
|
||||
// Human-readable summary for the panel "construction tree".
|
||||
// Interactive tree summary for the panel (ids/types/visibility/selection).
|
||||
getConstructions() {
|
||||
const r = (v) => Math.round(v * 100) / 100;
|
||||
const sel = this._intersectSel;
|
||||
return {
|
||||
lines: this._lines.map(l => ({ name: l.name })),
|
||||
intersectMode: this._intersectMode,
|
||||
points: this._cpoints.map(p => ({
|
||||
id: p.id, name: p.name, hidden: !!p.hidden, selected: sel.includes(p.id),
|
||||
info: `(${r(p.pos.x)}, ${r(p.pos.y)}, ${r(p.pos.z)})`,
|
||||
})),
|
||||
lines: this._lines.map(l => ({
|
||||
id: l.id, name: l.name, hidden: !!l.hidden, selected: sel.includes(l.id),
|
||||
})),
|
||||
planes: this._planes.map(p => {
|
||||
const n = p.normal;
|
||||
const D = -(n.x * p.point.x + n.y * p.point.y + n.z * p.point.z);
|
||||
const sign = (v) => (v >= 0 ? '+ ' : '− ') + Math.abs(r(v));
|
||||
return { name: p.name, eq: `${r(n.x)}x ${sign(n.y)}y ${sign(n.z)}z ${sign(D)} = 0` };
|
||||
return {
|
||||
id: p.id, name: p.name, hidden: !!p.hidden, selected: sel.includes(p.id),
|
||||
info: `${r(n.x)}x ${sign(n.y)}y ${sign(n.z)}z ${sign(D)} = 0`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -3655,10 +3689,153 @@ class StereoSim {
|
||||
return r;
|
||||
}
|
||||
|
||||
// Pick the nearest vertex / custom point under the cursor (Vector3 | null).
|
||||
// Pick the nearest vertex / custom point / construction point (Vector3 | null).
|
||||
_pickConstructPoint(e) {
|
||||
const p = this._pickNearestPoint(e);
|
||||
return p ? p.pos.clone() : null;
|
||||
const { mx, my } = this._screenCoords(e);
|
||||
let bestDist = 0.08, best = null;
|
||||
const consider = (pos) => {
|
||||
const p = pos.clone().project(this.camera);
|
||||
const d = Math.hypot(p.x - mx, p.y - my);
|
||||
if (d < bestDist) { bestDist = d; best = pos.clone(); }
|
||||
};
|
||||
for (const v of this._vertices) consider(v.pos);
|
||||
for (const cp of this._customPoints) consider(cp.pos);
|
||||
for (const cp of this._cpoints) consider(new THREE.Vector3(cp.pos.x, cp.pos.y, cp.pos.z));
|
||||
return best;
|
||||
}
|
||||
|
||||
_cpointLabel(i) {
|
||||
const P = ['M', 'N', 'K', 'P', 'Q', 'S', 'T', 'U', 'V', 'W', 'F', 'G'];
|
||||
const base = P[i % P.length];
|
||||
const sub = Math.floor(i / P.length);
|
||||
return sub > 0 ? base + '_' + sub : base;
|
||||
}
|
||||
|
||||
/* ── Intersections (Phase A2): list-based — select 2 objects ── */
|
||||
|
||||
setIntersectMode(on) {
|
||||
this._intersectMode = on;
|
||||
this._intersectSel = [];
|
||||
if (on) { this._lineMode = false; this._planeMode = false; this._constructPicks = []; }
|
||||
this._rebuildConstructions();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
_findObj(id) {
|
||||
let o = this._cpoints.find(x => x.id === id); if (o) return { type: 'point', obj: o };
|
||||
o = this._lines.find(x => x.id === id); if (o) return { type: 'line', obj: o };
|
||||
o = this._planes.find(x => x.id === id); if (o) return { type: 'plane', obj: o };
|
||||
return null;
|
||||
}
|
||||
|
||||
// Select an object for intersection; once 2 (line/plane) are chosen, compute.
|
||||
// Returns { msg } describing the result (or the prompt), for the panel hint.
|
||||
pickConstructObject(id) {
|
||||
if (!this._intersectMode) return { msg: '' };
|
||||
const found = this._findObj(id);
|
||||
if (!found || found.type === 'point') return { msg: 'Для пересечения выберите прямую или плоскость' };
|
||||
const i = this._intersectSel.indexOf(id);
|
||||
if (i >= 0) { this._intersectSel.splice(i, 1); this._notify(); return { msg: '' }; }
|
||||
this._intersectSel.push(id);
|
||||
if (this._intersectSel.length < 2) { this._notify(); return { msg: 'Выберите второй объект' }; }
|
||||
|
||||
const [idA, idB] = this._intersectSel;
|
||||
const res = this._computeIntersection(idA, idB);
|
||||
this._intersectSel = [];
|
||||
this._rebuildConstructions();
|
||||
this._notify();
|
||||
return { msg: res };
|
||||
}
|
||||
|
||||
_lineFromObj(l) {
|
||||
const p0 = new THREE.Vector3(l.a.x, l.a.y, l.a.z);
|
||||
const d = new THREE.Vector3(l.b.x, l.b.y, l.b.z).sub(p0).normalize();
|
||||
return { p0, d };
|
||||
}
|
||||
|
||||
_planeFromObj(p) {
|
||||
return {
|
||||
n: new THREE.Vector3(p.normal.x, p.normal.y, p.normal.z).normalize(),
|
||||
point: new THREE.Vector3(p.point.x, p.point.y, p.point.z),
|
||||
};
|
||||
}
|
||||
|
||||
_computeIntersection(idA, idB) {
|
||||
const A = this._findObj(idA), B = this._findObj(idB);
|
||||
if (!A || !B) return 'Объект не найден';
|
||||
const types = [A.type, B.type].sort().join('+');
|
||||
|
||||
if (types === 'plane+plane') {
|
||||
const r = this._intersectPlanePlane(A.obj, B.obj);
|
||||
if (!r) return `${A.obj.name} ∥ ${B.obj.name}: прямой пересечения нет`;
|
||||
const ln = this._createLine(r.p0, r.p0.clone().add(r.dir));
|
||||
return `прямая ${ln} = ${A.obj.name} ∩ ${B.obj.name}`;
|
||||
}
|
||||
|
||||
const line = A.type === 'line' ? A.obj : B.obj;
|
||||
const plane = A.type === 'plane' ? A.obj : B.obj;
|
||||
if (types === 'line+plane') {
|
||||
const pt = this._intersectLinePlane(line, plane);
|
||||
if (!pt) return `${line.name} ∥ ${plane.name}: точки пересечения нет`;
|
||||
const nm = this._createCPoint(pt);
|
||||
return `точка ${nm} = ${line.name} ∩ ${plane.name}`;
|
||||
}
|
||||
|
||||
if (types === 'line+line') {
|
||||
const r = this._intersectLineLine(A.obj, B.obj);
|
||||
if (r === 'parallel') return `${A.obj.name} ∥ ${B.obj.name}: точки нет`;
|
||||
if (r === 'skew') return `${A.obj.name} и ${B.obj.name} скрещиваются: точки нет`;
|
||||
const nm = this._createCPoint(r);
|
||||
return `точка ${nm} = ${A.obj.name} ∩ ${B.obj.name}`;
|
||||
}
|
||||
return 'Нельзя пересечь эти объекты';
|
||||
}
|
||||
|
||||
_intersectLinePlane(l, pl) {
|
||||
const { p0, d } = this._lineFromObj(l);
|
||||
const { n, point } = this._planeFromObj(pl);
|
||||
const denom = n.dot(d);
|
||||
if (Math.abs(denom) < 1e-9) return null; // line ∥ plane
|
||||
const t = n.dot(point.clone().sub(p0)) / denom;
|
||||
return p0.clone().addScaledVector(d, t);
|
||||
}
|
||||
|
||||
_intersectPlanePlane(pa, pb) {
|
||||
const n1 = this._planeFromObj(pa).n, n2 = this._planeFromObj(pb).n;
|
||||
const dir = new THREE.Vector3().crossVectors(n1, n2);
|
||||
if (dir.length() < 1e-7) return null; // parallel planes
|
||||
const c1 = n1.dot(this._planeFromObj(pa).point);
|
||||
const c2 = n2.dot(this._planeFromObj(pb).point);
|
||||
const term1 = new THREE.Vector3().crossVectors(n2, dir).multiplyScalar(c1);
|
||||
const term2 = new THREE.Vector3().crossVectors(dir, n1).multiplyScalar(c2);
|
||||
const p0 = term1.add(term2).divideScalar(dir.lengthSq());
|
||||
return { p0, dir: dir.normalize() };
|
||||
}
|
||||
|
||||
_intersectLineLine(la, lb) {
|
||||
const { p0: P0, d: D0 } = this._lineFromObj(la);
|
||||
const { p0: P1, d: D1 } = this._lineFromObj(lb);
|
||||
const b = D0.dot(D1);
|
||||
const denom = 1 - b * b; // D0,D1 are unit
|
||||
if (Math.abs(denom) < 1e-9) return 'parallel';
|
||||
const r = P0.clone().sub(P1);
|
||||
const dd = D0.dot(r), e = D1.dot(r);
|
||||
const s = (b * e - dd) / denom;
|
||||
const tt = (e - b * dd) / denom;
|
||||
const cp0 = P0.clone().addScaledVector(D0, s);
|
||||
const cp1 = P1.clone().addScaledVector(D1, tt);
|
||||
const tol = Math.max(0.12, this._sceneRadius() * 0.03);
|
||||
if (cp0.distanceTo(cp1) > tol) return 'skew';
|
||||
return cp0.add(cp1).multiplyScalar(0.5);
|
||||
}
|
||||
|
||||
_createCPoint(pos) {
|
||||
const name = this._cpointLabel(this._nextCPointName++);
|
||||
this._cpoints.push({
|
||||
id: 'C' + this._constructSeq, seq: this._constructSeq++,
|
||||
name, pos: { x: pos.x, y: pos.y, z: pos.z }, color: 0x34D399, hidden: false,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
_onConstructClick(e) {
|
||||
@@ -3682,36 +3859,39 @@ class StereoSim {
|
||||
}
|
||||
|
||||
_createLine(pA, pB) {
|
||||
if (pA.distanceTo(pB) < 1e-6) return;
|
||||
if (pA.distanceTo(pB) < 1e-6) return null;
|
||||
const name = this._lineLabel(this._nextLineName++);
|
||||
this._lines.push({
|
||||
id: 'L' + this._constructSeq, seq: this._constructSeq++,
|
||||
name, a: { x: pA.x, y: pA.y, z: pA.z }, b: { x: pB.x, y: pB.y, z: pB.z },
|
||||
color: 0x38BDF8,
|
||||
color: 0x38BDF8, hidden: false,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
_createPlane(p1, p2, p3) {
|
||||
const v1 = new THREE.Vector3().subVectors(p2, p1);
|
||||
const v2 = new THREE.Vector3().subVectors(p3, p1);
|
||||
const n = new THREE.Vector3().crossVectors(v1, v2);
|
||||
if (n.length() < 1e-6) return; // 3 collinear points → no plane
|
||||
if (n.length() < 1e-6) return null; // 3 collinear points → no plane
|
||||
n.normalize();
|
||||
const name = this._planeLabel(this._nextPlaneName++);
|
||||
this._planes.push({
|
||||
id: 'P' + this._constructSeq, seq: this._constructSeq++,
|
||||
name, point: { x: p1.x, y: p1.y, z: p1.z }, normal: { x: n.x, y: n.y, z: n.z },
|
||||
def: [p1, p2, p3].map(p => ({ x: p.x, y: p.y, z: p.z })),
|
||||
color: 0xC4B5FD,
|
||||
color: 0xC4B5FD, hidden: false,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
_rebuildConstructions() {
|
||||
if (!this._constructGroup) return;
|
||||
this._clearGroup(this._constructGroup);
|
||||
const ext = this._sceneRadius() * 1.6 + 2;
|
||||
for (const pl of this._planes) this._drawPlaneObject(pl, ext);
|
||||
for (const l of this._lines) this._drawLineObject(l, ext);
|
||||
for (const pl of this._planes) if (!pl.hidden) this._drawPlaneObject(pl, ext);
|
||||
for (const l of this._lines) if (!l.hidden) this._drawLineObject(l, ext);
|
||||
for (const cp of this._cpoints) if (!cp.hidden) this._drawCPointObject(cp);
|
||||
// in-progress picks (highlight spheres)
|
||||
for (const p of this._constructPicks) {
|
||||
const s = new THREE.Mesh(
|
||||
@@ -3799,6 +3979,22 @@ class StereoSim {
|
||||
}
|
||||
}
|
||||
|
||||
_drawCPointObject(cp) {
|
||||
const pos = new THREE.Vector3(cp.pos.x, cp.pos.y, cp.pos.z);
|
||||
const glow = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 12),
|
||||
new THREE.MeshBasicMaterial({ color: cp.color, transparent: true, opacity: 0.18, blending: THREE.AdditiveBlending, depthWrite: false }));
|
||||
glow.position.copy(pos);
|
||||
this._constructGroup.add(glow);
|
||||
const s = new THREE.Mesh(new THREE.SphereGeometry(0.11, 12, 12), new THREE.MeshBasicMaterial({ color: cp.color }));
|
||||
s.position.copy(pos); s.renderOrder = 5;
|
||||
this._constructGroup.add(s);
|
||||
if (this.showLabels) {
|
||||
const lbl = this._makeTextSprite(cp.name, '#6EE7B7', 44);
|
||||
lbl.position.copy(pos).add(new THREE.Vector3(0.2, 0.28, 0));
|
||||
this._constructGroup.add(lbl);
|
||||
}
|
||||
}
|
||||
|
||||
_clearGroup(group) {
|
||||
const disposeObj = (o) => {
|
||||
if (o.geometry) o.geometry.dispose();
|
||||
@@ -4271,7 +4467,7 @@ class StereoSim {
|
||||
'stereo-angle-edge-btn','stereo-angle-lp-btn','stereo-angle-dih-btn','stereo-angle-pp-btn','stereo-angle-skew-btn',
|
||||
'stereo-mark-tick-btn','stereo-mark-par-btn',
|
||||
'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'].forEach(id => {
|
||||
'stereo-sect3p-btn','stereo-line-btn','stereo-plane-btn','stereo-intersect-btn'].forEach(id => {
|
||||
document.getElementById(id)?.classList.remove('active');
|
||||
});
|
||||
if (stereoSim) {
|
||||
@@ -4284,6 +4480,7 @@ class StereoSim {
|
||||
stereoSim.toggleSection3P(false);
|
||||
stereoSim.setLineMode(false);
|
||||
stereoSim.setPlaneMode(false);
|
||||
stereoSim.setIntersectMode(false);
|
||||
}
|
||||
const hint = document.getElementById('angle-hint');
|
||||
if (hint) hint.textContent = '';
|
||||
@@ -4444,13 +4641,53 @@ class StereoSim {
|
||||
if (h) h.textContent = '';
|
||||
}
|
||||
|
||||
function stereoIntersectMode(btn) {
|
||||
const on = !btn.classList.contains('active');
|
||||
_stereoDeactivateTools();
|
||||
btn.classList.toggle('active', on);
|
||||
if (stereoSim) stereoSim.setIntersectMode(on);
|
||||
const h = document.getElementById('construct-hint');
|
||||
if (h) h.textContent = on ? 'Выберите 2 объекта (прямые/плоскости) в списке ниже' : '';
|
||||
}
|
||||
|
||||
function stereoConstructSelect(id) {
|
||||
if (!stereoSim) return;
|
||||
const res = stereoSim.pickConstructObject(id);
|
||||
const h = document.getElementById('construct-hint');
|
||||
if (h && res && res.msg) h.textContent = res.msg;
|
||||
}
|
||||
|
||||
function stereoConstructDelete(id) { if (stereoSim) stereoSim.removeConstruction(id); }
|
||||
function stereoConstructVis(id) { if (stereoSim) stereoSim.toggleConstructionVis(id); }
|
||||
|
||||
function _stereoUpdateConstructList() {
|
||||
const el = document.getElementById('construct-list');
|
||||
if (!el || !stereoSim) return;
|
||||
const c = stereoSim.getConstructions();
|
||||
const EYE_ON = '<svg viewBox="0 0 24 24" style="width:14px;height:14px"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>';
|
||||
const EYE_OFF = '<svg viewBox="0 0 24 24" style="width:14px;height:14px"><path d="M3 3l18 18M10.6 10.6a3 3 0 0 0 4.2 4.2M9.9 5.1A10.9 10.9 0 0 1 12 5c7 0 11 7 11 7a18 18 0 0 1-3.2 3.9M6.1 6.1A18 18 0 0 0 1 12s4 7 11 7c1.4 0 2.7-.2 3.9-.6" fill="none" stroke="currentColor" stroke-width="2"/></svg>';
|
||||
const X_IC = '<svg viewBox="0 0 24 24" style="width:14px;height:14px"><line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
||||
const icBtn = (id, fn, title, svg) =>
|
||||
'<button onclick="' + fn + "('" + id + '\')" title="' + title + '" style="background:none;border:none;color:rgba(255,255,255,0.55);cursor:pointer;padding:1px;display:flex;align-items:center">' + svg + '</button>';
|
||||
|
||||
const row = (o, kind, color, selectable) => {
|
||||
const selBg = o.selected ? 'background:rgba(56,189,248,0.2);' : '';
|
||||
const dim = o.hidden ? 'opacity:0.4;' : '';
|
||||
const main = '<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + dim +
|
||||
(selectable ? 'cursor:pointer" onclick="stereoConstructSelect(\'' + o.id + '\')"' : '"') + '>' +
|
||||
'<b style="color:' + color + '">' + kind + ' ' + o.name + '</b>' +
|
||||
(o.info ? ' <span style="color:rgba(255,255,255,0.4)">' + o.info + '</span>' : '') + '</span>';
|
||||
return '<div style="display:flex;align-items:center;gap:3px;padding:1px 3px;border-radius:5px;' + selBg + '">' +
|
||||
main +
|
||||
icBtn(o.id, 'stereoConstructVis', 'Скрыть/показать', o.hidden ? EYE_OFF : EYE_ON) +
|
||||
icBtn(o.id, 'stereoConstructDelete', 'Удалить', X_IC) + '</div>';
|
||||
};
|
||||
|
||||
const sel = c.intersectMode;
|
||||
const rows = [];
|
||||
c.lines.forEach(l => rows.push('<div style="color:#7DD3FC">прямая <b>' + l.name + '</b></div>'));
|
||||
c.planes.forEach(p => rows.push('<div style="color:#DDD6FE">плоскость <b>' + p.name + '</b>: ' + p.eq + '</div>'));
|
||||
c.points.forEach(p => rows.push(row(p, 'точка', '#6EE7B7', false)));
|
||||
c.lines.forEach(l => rows.push(row(l, 'прямая', '#7DD3FC', sel)));
|
||||
c.planes.forEach(p => rows.push(row(p, 'плоскость', '#DDD6FE', sel)));
|
||||
el.innerHTML = rows.join('');
|
||||
}
|
||||
|
||||
|
||||
@@ -3681,6 +3681,9 @@
|
||||
<button class="st-tool-btn" id="stereo-plane-btn" onclick="stereoPlaneMode(this)" title="Плоскость через 3 точки — кликните три вершины или точки">
|
||||
<svg viewBox="0 0 24 24"><polygon points="3,16 13,20 21,8 11,4" fill="none"/><circle cx="3" cy="16" r="1.8" fill="currentColor"/><circle cx="21" cy="8" r="1.8" fill="currentColor"/><circle cx="11" cy="4" r="1.8" fill="currentColor"/></svg>Плоскость
|
||||
</button>
|
||||
<button class="st-tool-btn st-tool-btn-wide" id="stereo-intersect-btn" onclick="stereoIntersectMode(this)" title="Пересечение: выберите 2 объекта в списке (прямая∩плоскость → точка, плоскость∩плоскость → прямая)">
|
||||
<svg viewBox="0 0 24 24"><line x1="3" y1="7" x2="21" y2="17"/><line x1="3" y1="17" x2="21" y2="7"/><circle cx="12" cy="12" r="2.4" fill="currentColor"/></svg>Пересечение
|
||||
</button>
|
||||
</div>
|
||||
<div class="st-action-grid" style="margin-top:3px">
|
||||
<button class="st-action-btn" onclick="stereoConstructUndo()">Удалить последнее</button>
|
||||
|
||||
Reference in New Issue
Block a user