Grotti 2.0.0: eigenes Repo, Umbenennung von Farm.
This commit is contained in:
commit
b2e6443c68
69 files changed
+18719
No files matched your search
@@ -0,0 +1,723 @@
|
||||
/* global THREE */
|
||||
|
||||
window.GrottiGcodeViewer = (function () {
|
||||
const PHI_MIN = 0.08;
|
||||
const PHI_MAX = Math.PI - 0.08;
|
||||
const COLOR_DONE = 0x5dffb0;
|
||||
const COLOR_TODO = 0x6b7280;
|
||||
const COLOR_PREVIEW = 0xf97316;
|
||||
const COLOR_OBJ = 0x60a5fa;
|
||||
const COLOR_OBJ_EX = 0xef4444;
|
||||
const COLOR_OBJ_CUR = 0xfbbf24;
|
||||
const GRID_STEP = 10;
|
||||
// Nur leichte Doppelung — starke Offsets + Punkte wirkten wie Vollkörper mit Löchern
|
||||
const FAT_OFFSETS = [0, -0.22, 0.22];
|
||||
|
||||
function create(canvas) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "gcode-viewer-wrap";
|
||||
if (canvas.parentNode) {
|
||||
canvas.parentNode.insertBefore(wrap, canvas);
|
||||
wrap.appendChild(canvas);
|
||||
}
|
||||
const legend = document.createElement("div");
|
||||
legend.className = "gcode-legend";
|
||||
legend.innerHTML =
|
||||
'<span class="gcode-leg-item live"><span class="gcode-leg-swatch done thick"></span><span>gedruckt</span></span>' +
|
||||
'<span class="gcode-leg-item live"><span class="gcode-leg-swatch todo"></span><span>offen</span></span>' +
|
||||
'<span class="gcode-leg-item preview-only"><span class="gcode-leg-swatch done"></span><span>Pfad</span></span>' +
|
||||
'<span class="gcode-leg-scale">Gitter ' + GRID_STEP + " mm</span>";
|
||||
wrap.appendChild(legend);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x121212);
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 5000);
|
||||
camera.up.set(0, 0, 1);
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
|
||||
const light = new THREE.DirectionalLight(0xffffff, 0.9);
|
||||
light.position.set(1, -2, 3);
|
||||
scene.add(light);
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.45));
|
||||
|
||||
const bedGroup = new THREE.Group();
|
||||
scene.add(bedGroup);
|
||||
const pathGroup = new THREE.Group();
|
||||
scene.add(pathGroup);
|
||||
const excludeGroup = new THREE.Group();
|
||||
scene.add(excludeGroup);
|
||||
|
||||
let points = [];
|
||||
let layerBreaks = [0];
|
||||
let progress = null;
|
||||
let layer = null;
|
||||
let layers = 0;
|
||||
let center = { x: 0, y: 0, z: 0 };
|
||||
let span = 80;
|
||||
let excludeData = { objects: [], excluded: [], current: null };
|
||||
let selectedName = null;
|
||||
let onExcludeSelect = null;
|
||||
let excludeMeshes = [];
|
||||
|
||||
let theta = 0.55;
|
||||
let phi = 1.05;
|
||||
let distance = 320;
|
||||
let target = new THREE.Vector3(0, 0, 20);
|
||||
let defaultView = { theta: 0.55, phi: 1.05, distance: 320, tx: 0, ty: 0, tz: 20 };
|
||||
|
||||
let mode = null;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
let downX = 0;
|
||||
let downY = 0;
|
||||
let pointers = new Map();
|
||||
let pinchDist = 0;
|
||||
let alive = true;
|
||||
let raf = 0;
|
||||
let dirty = true;
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const pointerNdc = new THREE.Vector2();
|
||||
|
||||
canvas.style.cursor = "grab";
|
||||
canvas.style.touchAction = "none";
|
||||
|
||||
function resize() {
|
||||
const width = canvas.clientWidth || canvas.parentElement.clientWidth || 480;
|
||||
const height = canvas.clientHeight || 320;
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
renderer.setSize(width, height, false);
|
||||
camera.aspect = width / Math.max(1, height);
|
||||
camera.updateProjectionMatrix();
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
function orbit() {
|
||||
const sinPhi = Math.sin(phi);
|
||||
camera.position.set(
|
||||
target.x + distance * sinPhi * Math.sin(theta),
|
||||
target.y - distance * sinPhi * Math.cos(theta),
|
||||
target.z + distance * Math.cos(phi)
|
||||
);
|
||||
camera.lookAt(target);
|
||||
}
|
||||
|
||||
function clearGroup(group) {
|
||||
while (group.children.length) {
|
||||
const child = group.children[0];
|
||||
group.remove(child);
|
||||
if (child.geometry) child.geometry.dispose();
|
||||
if (child.material) {
|
||||
const mats = Array.isArray(child.material) ? child.material : [child.material];
|
||||
mats.forEach((m) => {
|
||||
if (m.map) m.map.dispose();
|
||||
m.dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearLines() {
|
||||
clearGroup(pathGroup);
|
||||
}
|
||||
|
||||
function clearBed() {
|
||||
clearGroup(bedGroup);
|
||||
}
|
||||
|
||||
function clearExclude() {
|
||||
clearGroup(excludeGroup);
|
||||
excludeMeshes = [];
|
||||
}
|
||||
|
||||
function boundsOf(pts) {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let minZ = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
let maxZ = -Infinity;
|
||||
for (const point of pts) {
|
||||
minX = Math.min(minX, point[0]);
|
||||
minY = Math.min(minY, point[1]);
|
||||
minZ = Math.min(minZ, point[2]);
|
||||
maxX = Math.max(maxX, point[0]);
|
||||
maxY = Math.max(maxY, point[1]);
|
||||
maxZ = Math.max(maxZ, point[2]);
|
||||
}
|
||||
return { minX, minY, minZ, maxX, maxY, maxZ };
|
||||
}
|
||||
|
||||
function niceFloor(value, step) {
|
||||
return Math.floor(value / step) * step;
|
||||
}
|
||||
|
||||
function niceCeil(value, step) {
|
||||
return Math.ceil(value / step) * step;
|
||||
}
|
||||
|
||||
function makeLabelSprite(text) {
|
||||
const c = document.createElement("canvas");
|
||||
c.width = 256;
|
||||
c.height = 64;
|
||||
const ctx = c.getContext("2d");
|
||||
ctx.clearRect(0, 0, c.width, c.height);
|
||||
ctx.fillStyle = "rgba(18,18,18,0.72)";
|
||||
roundRect(ctx, 8, 8, c.width - 16, c.height - 16, 10);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#e8e8e8";
|
||||
ctx.font = "700 36px ui-sans-serif, system-ui, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, c.width / 2, c.height / 2 + 1);
|
||||
const tex = new THREE.CanvasTexture(c);
|
||||
tex.needsUpdate = true;
|
||||
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
|
||||
const sprite = new THREE.Sprite(mat);
|
||||
sprite.scale.set(18, 4.5, 1);
|
||||
sprite.renderOrder = 10;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
function roundRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function buildBed(b) {
|
||||
clearBed();
|
||||
const pad = GRID_STEP * 3;
|
||||
const minX = niceFloor(b.minX - pad, GRID_STEP) - center.x;
|
||||
const maxX = niceCeil(b.maxX + pad, GRID_STEP) - center.x;
|
||||
const minY = niceFloor(b.minY - pad, GRID_STEP) - center.y;
|
||||
const maxY = niceCeil(b.maxY + pad, GRID_STEP) - center.y;
|
||||
const w = Math.max(GRID_STEP, maxX - minX);
|
||||
const h = Math.max(GRID_STEP, maxY - minY);
|
||||
|
||||
const plate = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(w, h),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: 0x1a1a1a,
|
||||
transparent: true,
|
||||
opacity: 0.88,
|
||||
side: THREE.DoubleSide,
|
||||
})
|
||||
);
|
||||
plate.position.set((minX + maxX) / 2, (minY + maxY) / 2, -0.08);
|
||||
bedGroup.add(plate);
|
||||
|
||||
const gridLines = [];
|
||||
for (let x = minX; x <= maxX + 0.01; x += GRID_STEP) {
|
||||
const major = Math.round(x) % 50 === 0;
|
||||
gridLines.push(x, minY, 0, x, maxY, 0);
|
||||
if (major) gridLines.push(x + 0.2, minY, 0, x + 0.2, maxY, 0);
|
||||
}
|
||||
for (let y = minY; y <= maxY + 0.01; y += GRID_STEP) {
|
||||
const major = Math.round(y) % 50 === 0;
|
||||
gridLines.push(minX, y, 0, maxX, y, 0);
|
||||
if (major) gridLines.push(minX, y + 0.2, 0, maxX, y + 0.2, 0);
|
||||
}
|
||||
const gridGeo = new THREE.BufferGeometry();
|
||||
gridGeo.setAttribute("position", new THREE.Float32BufferAttribute(gridLines, 3));
|
||||
bedGroup.add(
|
||||
new THREE.LineSegments(
|
||||
gridGeo,
|
||||
new THREE.LineBasicMaterial({ color: 0x3f3f3f })
|
||||
)
|
||||
);
|
||||
|
||||
const tick = [];
|
||||
for (let x = minX; x <= maxX + 0.01; x += GRID_STEP) {
|
||||
const len = Math.round(x) % 50 === 0 ? 7 : 3.5;
|
||||
tick.push(x, minY, 0.02, x, minY - len, 0.02);
|
||||
}
|
||||
for (let y = minY; y <= maxY + 0.01; y += GRID_STEP) {
|
||||
const len = Math.round(y) % 50 === 0 ? 7 : 3.5;
|
||||
tick.push(minX, y, 0.02, minX - len, y, 0.02);
|
||||
}
|
||||
tick.push(minX, minY, 0.02, maxX, minY, 0.02);
|
||||
tick.push(minX, minY, 0.02, minX, maxY, 0.02);
|
||||
tick.push(maxX, minY, 0.02, maxX, maxY, 0.02);
|
||||
tick.push(minX, maxY, 0.02, maxX, maxY, 0.02);
|
||||
// Achsen-Hinweis
|
||||
tick.push(minX, minY, 0.05, minX + 25, minY, 0.05);
|
||||
tick.push(minX, minY, 0.05, minX, minY + 25, 0.05);
|
||||
|
||||
const tickGeo = new THREE.BufferGeometry();
|
||||
tickGeo.setAttribute("position", new THREE.Float32BufferAttribute(tick, 3));
|
||||
bedGroup.add(
|
||||
new THREE.LineSegments(
|
||||
tickGeo,
|
||||
new THREE.LineBasicMaterial({ color: 0xb0b0b0 })
|
||||
)
|
||||
);
|
||||
|
||||
for (let x = minX; x <= maxX + 0.01; x += 50) {
|
||||
const mm = Math.round(x + center.x);
|
||||
const sprite = makeLabelSprite(String(mm) + " mm");
|
||||
sprite.position.set(x, minY - 14, 1.5);
|
||||
bedGroup.add(sprite);
|
||||
}
|
||||
for (let y = minY + 50; y <= maxY + 0.01; y += 50) {
|
||||
const mm = Math.round(y + center.y);
|
||||
const sprite = makeLabelSprite(String(mm));
|
||||
sprite.position.set(minX - 16, y, 1.5);
|
||||
bedGroup.add(sprite);
|
||||
}
|
||||
const zero = makeLabelSprite("0");
|
||||
zero.position.set(minX - 14, minY - 14, 1.5);
|
||||
bedGroup.add(zero);
|
||||
const xAxis = makeLabelSprite("X →");
|
||||
xAxis.position.set(minX + 32, minY - 14, 1.5);
|
||||
bedGroup.add(xAxis);
|
||||
const yAxis = makeLabelSprite("Y →");
|
||||
yAxis.position.set(minX - 16, minY + 32, 1.5);
|
||||
bedGroup.add(yAxis);
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
theta = defaultView.theta;
|
||||
phi = defaultView.phi;
|
||||
distance = defaultView.distance;
|
||||
target.set(defaultView.tx, defaultView.ty, defaultView.tz);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
function isExtrude(point) {
|
||||
return !point || point.length < 4 || point[3];
|
||||
}
|
||||
|
||||
function consecutiveMove(prev, next) {
|
||||
// Original-seq (Index 4): nach Thinning keine Sehnen über Lücken
|
||||
if (!prev || !next || prev.length < 5 || next.length < 5) return true;
|
||||
return Number(next[4]) === Number(prev[4]) + 1;
|
||||
}
|
||||
|
||||
function fillSegments(from, to, out, fat) {
|
||||
const end = Math.min(to, points.length - 1);
|
||||
const start = Math.max(0, from);
|
||||
const offsets = fat ? FAT_OFFSETS : [0];
|
||||
for (let i = start + 1; i <= end; i++) {
|
||||
if (!isExtrude(points[i])) continue;
|
||||
if (!consecutiveMove(points[i - 1], points[i])) continue;
|
||||
const ax = points[i - 1][0] - center.x;
|
||||
const ay = points[i - 1][1] - center.y;
|
||||
const az = points[i - 1][2] - center.z;
|
||||
const bx = points[i][0] - center.x;
|
||||
const by = points[i][1] - center.y;
|
||||
const bz = points[i][2] - center.z;
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const nx = -dy / len;
|
||||
const ny = dx / len;
|
||||
for (const off of offsets) {
|
||||
out.push(
|
||||
ax + nx * off, ay + ny * off, az,
|
||||
bx + nx * off, by + ny * off, bz
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeSegLine(from, to, color, opacity, fat) {
|
||||
const segs = [];
|
||||
fillSegments(from, to, segs, fat);
|
||||
if (segs.length < 6) return null;
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute("position", new THREE.Float32BufferAttribute(segs, 3));
|
||||
const mat = new THREE.LineBasicMaterial({
|
||||
color,
|
||||
transparent: opacity < 0.99,
|
||||
opacity,
|
||||
depthWrite: opacity > 0.9,
|
||||
});
|
||||
return new THREE.LineSegments(geo, mat);
|
||||
}
|
||||
|
||||
function cutIndex() {
|
||||
if (!points.length) return 0;
|
||||
if (progress == null) return points.length - 1;
|
||||
if (progress >= 0.995 || (layers > 0 && layer != null && Number(layer) >= layers)) {
|
||||
return points.length - 1;
|
||||
}
|
||||
// Primär Datei-Fortschritt. layer_breaks zählt oft Z-Hops mit —
|
||||
// Index === current_layer schnitt viel zu früh ab.
|
||||
let cut = Math.floor(points.length * progress);
|
||||
if (layerBreaks.length > 1 && layers > 0 && layer != null && Number(layer) > 0) {
|
||||
const ratio = Math.min(1, Number(layer) / Number(layers));
|
||||
const idx = Math.floor(ratio * (layerBreaks.length - 1));
|
||||
const byLayer = layerBreaks[Math.max(0, Math.min(layerBreaks.length - 1, idx))];
|
||||
cut = Math.max(cut, byLayer);
|
||||
}
|
||||
return Math.max(0, Math.min(points.length - 1, cut));
|
||||
}
|
||||
|
||||
function rebuildProgress() {
|
||||
clearLines();
|
||||
if (!points.length) {
|
||||
legend.classList.add("preview");
|
||||
dirty = true;
|
||||
return;
|
||||
}
|
||||
if (progress == null) {
|
||||
legend.classList.add("preview");
|
||||
const line = makeSegLine(0, points.length - 1, COLOR_PREVIEW, 0.95, true);
|
||||
if (line) pathGroup.add(line);
|
||||
dirty = true;
|
||||
return;
|
||||
}
|
||||
legend.classList.remove("preview");
|
||||
const cut = cutIndex();
|
||||
const todo = makeSegLine(Math.max(0, cut - 1), points.length - 1, COLOR_TODO, 0.42, false);
|
||||
const done = makeSegLine(0, cut, COLOR_DONE, 1, true);
|
||||
if (todo) pathGroup.add(todo);
|
||||
if (done) pathGroup.add(done);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
function polygonHitMesh(polyLocal, name) {
|
||||
// unsichtbare Fläche für Raycast (Dreiecke fan)
|
||||
if (!polyLocal || polyLocal.length < 3) return null;
|
||||
const positions = [];
|
||||
const c = { x: 0, y: 0 };
|
||||
for (const p of polyLocal) {
|
||||
c.x += p[0];
|
||||
c.y += p[1];
|
||||
}
|
||||
c.x /= polyLocal.length;
|
||||
c.y /= polyLocal.length;
|
||||
for (let i = 0; i < polyLocal.length; i++) {
|
||||
const a = polyLocal[i];
|
||||
const b = polyLocal[(i + 1) % polyLocal.length];
|
||||
positions.push(c.x, c.y, 0.15, a[0], a[1], 0.15, b[0], b[1], 0.15);
|
||||
}
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
const mesh = new THREE.Mesh(
|
||||
geo,
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
transparent: true,
|
||||
opacity: 0.01,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
})
|
||||
);
|
||||
mesh.userData.excludeName = name;
|
||||
return mesh;
|
||||
}
|
||||
|
||||
function rebuildExclude() {
|
||||
clearExclude();
|
||||
const objects = excludeData.objects || [];
|
||||
const excluded = new Set(excludeData.excluded || []);
|
||||
const current = excludeData.current || null;
|
||||
for (const obj of objects) {
|
||||
const name = obj.name || "";
|
||||
const poly = obj.polygon || [];
|
||||
if (!name || poly.length < 2) continue;
|
||||
const local = poly.map((p) => [p[0] - center.x, p[1] - center.y]);
|
||||
const isEx = excluded.has(name);
|
||||
const isCur = current === name;
|
||||
const isSel = selectedName === name;
|
||||
let color = COLOR_OBJ;
|
||||
if (isEx) color = COLOR_OBJ_EX;
|
||||
else if (isCur) color = COLOR_OBJ_CUR;
|
||||
if (isSel && !isEx) color = 0xffffff;
|
||||
|
||||
const loop = [];
|
||||
for (let i = 0; i < local.length; i++) {
|
||||
const a = local[i];
|
||||
const b = local[(i + 1) % local.length];
|
||||
loop.push(a[0], a[1], 0.4, b[0], b[1], 0.4);
|
||||
if (isEx || isSel) {
|
||||
loop.push(a[0], a[1], 0.55, b[0], b[1], 0.55);
|
||||
}
|
||||
}
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute("position", new THREE.Float32BufferAttribute(loop, 3));
|
||||
const line = new THREE.LineSegments(
|
||||
geo,
|
||||
new THREE.LineBasicMaterial({
|
||||
color,
|
||||
transparent: true,
|
||||
opacity: isEx ? 0.45 : 0.95,
|
||||
})
|
||||
);
|
||||
excludeGroup.add(line);
|
||||
|
||||
const hit = polygonHitMesh(local, name);
|
||||
if (hit) {
|
||||
excludeGroup.add(hit);
|
||||
excludeMeshes.push(hit);
|
||||
}
|
||||
|
||||
if (obj.center && obj.center.length >= 2) {
|
||||
const mark = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(1.8, 12),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color,
|
||||
transparent: true,
|
||||
opacity: isEx ? 0.35 : 0.9,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
})
|
||||
);
|
||||
mark.position.set(obj.center[0] - center.x, obj.center[1] - center.y, 0.5);
|
||||
mark.userData.excludeName = name;
|
||||
excludeGroup.add(mark);
|
||||
excludeMeshes.push(mark);
|
||||
}
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
function setPath(data, opts) {
|
||||
const keepView = !!(opts && opts.keepView);
|
||||
clearLines();
|
||||
points = (data && data.points) || [];
|
||||
layerBreaks = (data && data.layer_breaks && data.layer_breaks.length)
|
||||
? data.layer_breaks
|
||||
: [0];
|
||||
if (!points.length) {
|
||||
clearBed();
|
||||
clearExclude();
|
||||
rebuildProgress();
|
||||
return;
|
||||
}
|
||||
const b = boundsOf(points);
|
||||
center = {
|
||||
x: (b.minX + b.maxX) / 2,
|
||||
y: (b.minY + b.maxY) / 2,
|
||||
z: b.minZ,
|
||||
};
|
||||
span = Math.max(b.maxX - b.minX, b.maxY - b.minY, b.maxZ - b.minZ, 40);
|
||||
const midZ = (b.maxZ - b.minZ) * 0.35;
|
||||
const fitDist = Math.max(60, span * 1.85);
|
||||
defaultView = {
|
||||
theta: 0.55,
|
||||
phi: 1.05,
|
||||
distance: fitDist,
|
||||
tx: 0,
|
||||
ty: 0,
|
||||
tz: midZ,
|
||||
};
|
||||
if (!keepView) {
|
||||
distance = fitDist;
|
||||
target.set(0, 0, midZ);
|
||||
theta = defaultView.theta;
|
||||
phi = defaultView.phi;
|
||||
}
|
||||
buildBed(b);
|
||||
rebuildProgress();
|
||||
rebuildExclude();
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
function setProgress(value, meta) {
|
||||
if (value == null || value === "") {
|
||||
progress = null;
|
||||
} else {
|
||||
progress = Math.max(0, Math.min(1, Number(value) || 0));
|
||||
}
|
||||
if (meta && typeof meta === "object") {
|
||||
layer = meta.layer != null ? Number(meta.layer) : layer;
|
||||
layers = meta.layers != null ? Number(meta.layers) : layers;
|
||||
}
|
||||
rebuildProgress();
|
||||
}
|
||||
|
||||
function setExcludeObjects(data, opts) {
|
||||
excludeData = data && typeof data === "object"
|
||||
? {
|
||||
objects: data.objects || [],
|
||||
excluded: data.excluded || data.excluded_objects || [],
|
||||
current: data.current || data.current_object || null,
|
||||
}
|
||||
: { objects: [], excluded: [], current: null };
|
||||
if (opts && opts.selected != null) selectedName = opts.selected;
|
||||
if (opts && typeof opts.onSelect === "function") onExcludeSelect = opts.onSelect;
|
||||
rebuildExclude();
|
||||
}
|
||||
|
||||
function setSelectedExclude(name) {
|
||||
selectedName = name || null;
|
||||
rebuildExclude();
|
||||
}
|
||||
|
||||
function pickExclude(clientX, clientY) {
|
||||
if (!excludeMeshes.length) return null;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
pointerNdc.x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
||||
pointerNdc.y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
||||
raycaster.setFromCamera(pointerNdc, camera);
|
||||
const hits = raycaster.intersectObjects(excludeMeshes, false);
|
||||
if (!hits.length) return null;
|
||||
return hits[0].object.userData.excludeName || null;
|
||||
}
|
||||
|
||||
function panBy(dx, dy) {
|
||||
const right = new THREE.Vector3();
|
||||
const up = new THREE.Vector3();
|
||||
right.setFromMatrixColumn(camera.matrixWorld, 0).normalize();
|
||||
up.setFromMatrixColumn(camera.matrixWorld, 1).normalize();
|
||||
const scale = distance * 0.0018;
|
||||
target.addScaledVector(right, -dx * scale);
|
||||
target.addScaledVector(up, dy * scale);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
function frame() {
|
||||
if (!alive) return;
|
||||
resize();
|
||||
if (dirty || mode) {
|
||||
orbit();
|
||||
renderer.render(scene, camera);
|
||||
dirty = false;
|
||||
}
|
||||
raf = requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
alive = false;
|
||||
if (raf) cancelAnimationFrame(raf);
|
||||
clearLines();
|
||||
clearBed();
|
||||
clearExclude();
|
||||
renderer.dispose();
|
||||
pointers.clear();
|
||||
if (legend.parentNode) legend.parentNode.removeChild(legend);
|
||||
if (wrap.parentNode && canvas.parentNode === wrap) {
|
||||
wrap.parentNode.insertBefore(canvas, wrap);
|
||||
wrap.parentNode.removeChild(wrap);
|
||||
}
|
||||
}
|
||||
|
||||
function pointerCount() {
|
||||
return pointers.size;
|
||||
}
|
||||
|
||||
function pinchDistance() {
|
||||
const pts = [...pointers.values()];
|
||||
if (pts.length < 2) return 0;
|
||||
const dx = pts[0].x - pts[1].x;
|
||||
const dy = pts[0].y - pts[1].y;
|
||||
return Math.hypot(dx, dy);
|
||||
}
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||||
downX = event.clientX;
|
||||
downY = event.clientY;
|
||||
if (pointerCount() === 2) {
|
||||
mode = "pinch";
|
||||
pinchDist = pinchDistance();
|
||||
canvas.style.cursor = "grabbing";
|
||||
return;
|
||||
}
|
||||
if (event.button === 1 || event.button === 2 || event.shiftKey || event.altKey) {
|
||||
mode = "pan";
|
||||
} else if (event.button === 0) {
|
||||
mode = "orbit";
|
||||
} else {
|
||||
mode = null;
|
||||
return;
|
||||
}
|
||||
lastX = event.clientX;
|
||||
lastY = event.clientY;
|
||||
canvas.style.cursor = "grabbing";
|
||||
});
|
||||
|
||||
function endPointer(event) {
|
||||
const moved = Math.hypot(event.clientX - downX, event.clientY - downY);
|
||||
if (mode === "orbit" && moved < 6 && event.button === 0) {
|
||||
const name = pickExclude(event.clientX, event.clientY);
|
||||
if (name) {
|
||||
selectedName = name;
|
||||
rebuildExclude();
|
||||
if (onExcludeSelect) onExcludeSelect(name);
|
||||
}
|
||||
}
|
||||
pointers.delete(event.pointerId);
|
||||
if (pointerCount() === 0) {
|
||||
mode = null;
|
||||
canvas.style.cursor = "grab";
|
||||
} else if (pointerCount() === 1) {
|
||||
const remaining = [...pointers.values()][0];
|
||||
mode = "orbit";
|
||||
lastX = remaining.x;
|
||||
lastY = remaining.y;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.addEventListener("pointerup", endPointer);
|
||||
canvas.addEventListener("pointercancel", endPointer);
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
if (!pointers.has(event.pointerId)) return;
|
||||
pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||||
|
||||
if (mode === "pinch" && pointerCount() >= 2) {
|
||||
const next = pinchDistance();
|
||||
if (pinchDist > 0 && next > 0) {
|
||||
distance = Math.max(25, Math.min(2500, distance * (pinchDist / next)));
|
||||
dirty = true;
|
||||
}
|
||||
pinchDist = next;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mode) return;
|
||||
const dx = event.clientX - lastX;
|
||||
const dy = event.clientY - lastY;
|
||||
lastX = event.clientX;
|
||||
lastY = event.clientY;
|
||||
|
||||
if (mode === "pan") {
|
||||
panBy(dx, dy);
|
||||
return;
|
||||
}
|
||||
theta -= dx * 0.008;
|
||||
phi = Math.max(PHI_MIN, Math.min(PHI_MAX, phi + dy * 0.008));
|
||||
dirty = true;
|
||||
});
|
||||
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
// Chrome (Pixel-Deltas / Trackpad) feuert aggressiver als z. B. Embedded-Browser.
|
||||
let dy = event.deltaY;
|
||||
if (event.deltaMode === 1) dy *= 16; // Zeilen
|
||||
else if (event.deltaMode === 2) dy *= canvas.clientHeight || 320; // Seiten
|
||||
// sanfte Exponential-Kurve, gekappt gegen Ausreißer
|
||||
const factor = Math.exp(Math.max(-80, Math.min(80, dy)) * 0.0012);
|
||||
distance = Math.max(25, Math.min(2500, distance * factor));
|
||||
dirty = true;
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
canvas.addEventListener("dblclick", (event) => {
|
||||
event.preventDefault();
|
||||
resetView();
|
||||
});
|
||||
|
||||
frame();
|
||||
return {
|
||||
setPath,
|
||||
setProgress,
|
||||
setExcludeObjects,
|
||||
setSelectedExclude,
|
||||
resize,
|
||||
destroy,
|
||||
resetView,
|
||||
};
|
||||
}
|
||||
|
||||
return { create };
|
||||
})();
|
||||
Reference in new issue
Block a user