@rslsp1/fa-app-tools 10.0.1 → 10.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/StageEngine-3MOBM2WX.mjs +590 -0
- package/dist/StageEngine-5VR3KEZB.js +590 -0
- package/dist/chunk-ACB2543O.js +218 -0
- package/dist/chunk-FTLEDU6C.js +157 -0
- package/dist/chunk-HFX6D53W.js +25729 -0
- package/dist/{chunk-UDBGDCKI.mjs → chunk-JP7LIWDR.mjs} +17936 -13232
- package/dist/chunk-QERFD7BY.js +1418 -0
- package/dist/{chunk-C5CGSPP3.mjs → chunk-RFTANBQ5.mjs} +80 -0
- package/dist/chunk-TFWIMFNF.mjs +218 -0
- package/dist/{contracts-DdYOMBl9.d.mts → contracts-B3AbzYQ4.d.mts} +1 -1
- package/dist/{contracts-DdYOMBl9.d.ts → contracts-B3AbzYQ4.d.ts} +1 -1
- package/dist/{hfEventTypes-BUztJySs.d.mts → hfEventTypes-DQPjEP4T.d.mts} +12 -1
- package/dist/{hfEventTypes-fAdA-0A9.d.ts → hfEventTypes-DharkVsi.d.ts} +12 -1
- package/dist/index.d.mts +27 -6
- package/dist/index.d.ts +27 -6
- package/dist/index.js +1831 -22287
- package/dist/index.mjs +173 -67
- package/dist/{storeContract-CJi0fpCA.d.mts → storeContract-C-em5E61.d.ts} +140 -14
- package/dist/{storeContract-gc2dxews.d.ts → storeContract-Cc8tFavd.d.mts} +140 -14
- package/dist/szenen-contract.d.mts +2 -2
- package/dist/szenen-contract.d.ts +2 -2
- package/dist/szenen-contract.js +12 -1474
- package/dist/szenen-contract.mjs +1 -1
- package/dist/szenen.d.mts +1095 -721
- package/dist/szenen.d.ts +1095 -721
- package/dist/szenen.js +1361 -23613
- package/dist/szenen.mjs +95 -128
- package/package.json +4 -2
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clampZoom,
|
|
3
|
+
fitPlaneView,
|
|
4
|
+
gridStep,
|
|
5
|
+
nudgePosition,
|
|
6
|
+
planeAxes,
|
|
7
|
+
screenToPlane,
|
|
8
|
+
snapPosition,
|
|
9
|
+
stageFrame,
|
|
10
|
+
vector3,
|
|
11
|
+
zoomPlaneView
|
|
12
|
+
} from "./chunk-TFWIMFNF.mjs";
|
|
13
|
+
|
|
14
|
+
// src/features/szenen/areas/StageEngine.tsx
|
|
15
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
16
|
+
import * as THREE from "three";
|
|
17
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
18
|
+
var radians = (v) => v.map((x) => THREE.MathUtils.degToRad(x));
|
|
19
|
+
function geometry(node) {
|
|
20
|
+
const [x, y, z] = node.dimensions;
|
|
21
|
+
if (node.primitive === "sphere") return new THREE.SphereGeometry(0.5, 16, 10).scale(x, y, z);
|
|
22
|
+
if (node.primitive === "cylinder") return new THREE.CylinderGeometry(0.5, 0.5, 1, 16).rotateX(Math.PI / 2).scale(x, y, z);
|
|
23
|
+
return new THREE.BoxGeometry(x, y, z);
|
|
24
|
+
}
|
|
25
|
+
function anchor(frame, target) {
|
|
26
|
+
const t = target;
|
|
27
|
+
const node = frame.nodes.find((n) => n.node_id === t?.node_ref);
|
|
28
|
+
if (!node) throw new Error("Das Kamera-Blickziel ist nicht platziert oder ausgeblendet.");
|
|
29
|
+
const key = t.anchor ?? "center";
|
|
30
|
+
const explicit = node.anchors?.[key];
|
|
31
|
+
if (!explicit && key !== "center" && key !== "base") throw new Error(`Anker \u201E${key}\u201C an \u201E${node.node_id}\u201C ben\xF6tigt eine explizite lokale Position.`);
|
|
32
|
+
const local = new THREE.Vector3(...vector3(explicit ?? (key === "base" ? [0, 0, -node.dimensions[2] / 2] : [0, 0, 0]), "Anker"));
|
|
33
|
+
local.multiply(new THREE.Vector3(...node.scale)).applyEuler(new THREE.Euler(...radians(node.rotation), "XYZ"));
|
|
34
|
+
return local.add(new THREE.Vector3(...node.position));
|
|
35
|
+
}
|
|
36
|
+
function cameraFor(frame) {
|
|
37
|
+
const c = frame.camera;
|
|
38
|
+
const camera = new THREE.PerspectiveCamera(c.vertical_fov_deg, c.aspect, 0.01, 1e4);
|
|
39
|
+
camera.position.set(...c.position);
|
|
40
|
+
camera.up.set(0, 0, 1);
|
|
41
|
+
if (c.orientation.mode === "explicit") camera.rotation.set(...radians(vector3(c.orientation.rotation_euler_deg, "Kameraausrichtung")), "XYZ");
|
|
42
|
+
else {
|
|
43
|
+
const target = anchor(frame, c.orientation.target);
|
|
44
|
+
const direction = target.clone().sub(camera.position);
|
|
45
|
+
if (direction.lengthSq() < 1e-12) throw new Error("Kamera und Blickziel liegen am selben Punkt.");
|
|
46
|
+
if (Math.abs(direction.normalize().dot(camera.up)) > 0.9999) {
|
|
47
|
+
if (!c.orientation.up_hint) throw new Error("F\xFCr diese senkrechte Blickrichtung bitte up_hint festlegen.");
|
|
48
|
+
camera.up.set(...vector3(c.orientation.up_hint, "Kamera-Up"));
|
|
49
|
+
if (camera.up.lengthSq() < 1e-12 || Math.abs(direction.dot(camera.up.clone().normalize())) > 0.9999) throw new Error("Kamera-Up und Blickrichtung m\xFCssen verschiedene Achsen bilden.");
|
|
50
|
+
}
|
|
51
|
+
camera.lookAt(target);
|
|
52
|
+
if (typeof c.orientation.roll_deg === "number") camera.rotateZ(THREE.MathUtils.degToRad(c.orientation.roll_deg));
|
|
53
|
+
}
|
|
54
|
+
camera.updateMatrixWorld();
|
|
55
|
+
return camera;
|
|
56
|
+
}
|
|
57
|
+
var SNAP = 0.05;
|
|
58
|
+
var CAMERA_COLOR = "#60a5fa";
|
|
59
|
+
var AXIS_COLOR = ["#f87171", "#4ade80", "#60a5fa"];
|
|
60
|
+
var meters = (v) => v.toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
61
|
+
function corners(node, position = node.position) {
|
|
62
|
+
const m = new THREE.Matrix4().compose(new THREE.Vector3(...position), new THREE.Quaternion().setFromEuler(new THREE.Euler(...radians(node.rotation), "XYZ")), new THREE.Vector3(...node.scale));
|
|
63
|
+
const [x, y, z] = node.dimensions.map((d) => d / 2);
|
|
64
|
+
return [-1, 1].flatMap((a) => [-1, 1].flatMap((b) => [-1, 1].map((c) => new THREE.Vector3(a * x, b * y, c * z).applyMatrix4(m))));
|
|
65
|
+
}
|
|
66
|
+
function withDrag(frame, drag) {
|
|
67
|
+
if (!drag) return frame;
|
|
68
|
+
if (drag.kind === "camera") return { ...frame, camera: { ...frame.camera, position: drag.position } };
|
|
69
|
+
return { ...frame, nodes: frame.nodes.map((n) => n.node_id === drag.id ? { ...n, position: drag.position } : n) };
|
|
70
|
+
}
|
|
71
|
+
function lineGeometry(points) {
|
|
72
|
+
return new THREE.BufferGeometry().setFromPoints(points);
|
|
73
|
+
}
|
|
74
|
+
function disposeScene(scene) {
|
|
75
|
+
const geometries = /* @__PURE__ */ new Set(), materials = /* @__PURE__ */ new Set();
|
|
76
|
+
scene.traverse((object) => {
|
|
77
|
+
const drawable = object;
|
|
78
|
+
if (drawable.geometry) geometries.add(drawable.geometry);
|
|
79
|
+
if (drawable.material) for (const m of Array.isArray(drawable.material) ? drawable.material : [drawable.material]) materials.add(m);
|
|
80
|
+
});
|
|
81
|
+
geometries.forEach((g) => g.dispose());
|
|
82
|
+
materials.forEach((m) => m.dispose());
|
|
83
|
+
}
|
|
84
|
+
function proxyMesh(node, position = node.position) {
|
|
85
|
+
const mesh = new THREE.Mesh(geometry(node), new THREE.MeshBasicMaterial({ color: node.preview_color.hex }));
|
|
86
|
+
mesh.position.set(...position);
|
|
87
|
+
mesh.rotation.set(...radians(node.rotation), "XYZ");
|
|
88
|
+
mesh.scale.set(...node.scale);
|
|
89
|
+
mesh.userData.nodeId = node.node_id;
|
|
90
|
+
return mesh;
|
|
91
|
+
}
|
|
92
|
+
function addGrid(scene, plane, view, width, height) {
|
|
93
|
+
const step = gridStep(view.zoom), major = gridStep(view.zoom, 72);
|
|
94
|
+
const span = Math.max(width, height) / view.zoom;
|
|
95
|
+
const [cu, cv] = view.center.map((c) => Math.round(c / (major * 4)) * major * 4);
|
|
96
|
+
const point = (u, v) => plane === "top" ? new THREE.Vector3(u, v, 0) : new THREE.Vector3(u, 0, v);
|
|
97
|
+
const reach = Math.ceil(span / major + 4) * major;
|
|
98
|
+
const minor = [], strong = [];
|
|
99
|
+
const isMajor = (x) => Math.abs(x / major - Math.round(x / major)) < 1e-6;
|
|
100
|
+
for (let i = Math.floor((cu - reach) / step); i <= Math.ceil((cu + reach) / step); i++) {
|
|
101
|
+
const u = i * step;
|
|
102
|
+
if (Math.abs(u) < 1e-9) continue;
|
|
103
|
+
(isMajor(u) ? strong : minor).push(point(u, cv - reach), point(u, cv + reach));
|
|
104
|
+
}
|
|
105
|
+
for (let i = Math.floor((cv - reach) / step); i <= Math.ceil((cv + reach) / step); i++) {
|
|
106
|
+
const v = i * step;
|
|
107
|
+
if (Math.abs(v) < 1e-9) continue;
|
|
108
|
+
(isMajor(v) ? strong : minor).push(point(cu - reach, v), point(cu + reach, v));
|
|
109
|
+
}
|
|
110
|
+
const flat = { depthTest: false, depthWrite: false, transparent: true };
|
|
111
|
+
const line = (points, tint, opacity, order) => {
|
|
112
|
+
const object = new THREE.LineSegments(lineGeometry(points), new THREE.LineBasicMaterial({ color: tint, opacity, ...flat }));
|
|
113
|
+
object.renderOrder = order;
|
|
114
|
+
scene.add(object);
|
|
115
|
+
};
|
|
116
|
+
if (plane === "side") {
|
|
117
|
+
const ground = new THREE.Mesh(
|
|
118
|
+
new THREE.PlaneGeometry(reach * 2, reach).rotateX(Math.PI / 2).translate(cu, 0, -reach / 2),
|
|
119
|
+
new THREE.MeshBasicMaterial({ color: "#000000", opacity: 0.35, ...flat, side: THREE.DoubleSide })
|
|
120
|
+
);
|
|
121
|
+
ground.renderOrder = -3;
|
|
122
|
+
scene.add(ground);
|
|
123
|
+
}
|
|
124
|
+
line(minor, "#ffffff", 0.05, -2);
|
|
125
|
+
line(strong, "#ffffff", 0.12, -2);
|
|
126
|
+
line([point(cu - reach, 0), point(cu + reach, 0)], AXIS_COLOR[0], 0.55, -1);
|
|
127
|
+
line([point(0, cv - reach), point(0, cv + reach)], AXIS_COLOR[plane === "top" ? 1 : 2], 0.55, -1);
|
|
128
|
+
}
|
|
129
|
+
function addCameraGlyph(scene, camera, length, pixel, active) {
|
|
130
|
+
const origin = camera.position.clone(), forward = camera.getWorldDirection(new THREE.Vector3());
|
|
131
|
+
const ends = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([x, y]) => {
|
|
132
|
+
const dir = new THREE.Vector3(x, y, 0.5).unproject(camera).sub(origin).normalize();
|
|
133
|
+
return origin.clone().add(dir.multiplyScalar(length / dir.dot(forward)));
|
|
134
|
+
});
|
|
135
|
+
const flat = { depthWrite: false, transparent: true };
|
|
136
|
+
const faces = new THREE.Mesh(
|
|
137
|
+
lineGeometry(ends.flatMap((c, i) => [origin, c, ends[(i + 1) % 4]])),
|
|
138
|
+
new THREE.MeshBasicMaterial({ color: CAMERA_COLOR, opacity: 0.05, side: THREE.DoubleSide, ...flat })
|
|
139
|
+
);
|
|
140
|
+
faces.renderOrder = 1;
|
|
141
|
+
scene.add(faces);
|
|
142
|
+
const edges = new THREE.LineSegments(
|
|
143
|
+
lineGeometry(ends.flatMap((c, i) => [origin, c, c, ends[(i + 1) % 4]])),
|
|
144
|
+
new THREE.LineBasicMaterial({ color: CAMERA_COLOR, opacity: active ? 0.9 : 0.4, ...flat })
|
|
145
|
+
);
|
|
146
|
+
edges.renderOrder = 2;
|
|
147
|
+
scene.add(edges);
|
|
148
|
+
const axis = new THREE.LineSegments(
|
|
149
|
+
lineGeometry([origin, origin.clone().add(forward.multiplyScalar(length))]),
|
|
150
|
+
new THREE.LineBasicMaterial({ color: CAMERA_COLOR, opacity: 0.3, ...flat })
|
|
151
|
+
);
|
|
152
|
+
axis.renderOrder = 2;
|
|
153
|
+
scene.add(axis);
|
|
154
|
+
for (const [radius, tint, order] of [[active ? 9 : 7, active ? "#ffffff" : CAMERA_COLOR, 3], [4, active ? CAMERA_COLOR : "#0b1220", 4]]) {
|
|
155
|
+
const dot = new THREE.Mesh(
|
|
156
|
+
new THREE.SphereGeometry(radius * pixel, 20, 12),
|
|
157
|
+
new THREE.MeshBasicMaterial({ color: tint, depthTest: false, transparent: true })
|
|
158
|
+
);
|
|
159
|
+
dot.position.copy(origin);
|
|
160
|
+
dot.renderOrder = order;
|
|
161
|
+
scene.add(dot);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function StageSurface({ frame, perspective, view, planeView, selected, hovered, frustum, world, cameraActive }) {
|
|
165
|
+
const canvas = useRef(null);
|
|
166
|
+
const renderer = useRef(null);
|
|
167
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
168
|
+
const [error, setError] = useState(null);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const el = canvas.current;
|
|
171
|
+
let gl;
|
|
172
|
+
try {
|
|
173
|
+
gl = new THREE.WebGLRenderer({ canvas: el, antialias: true });
|
|
174
|
+
gl.outputColorSpace = THREE.SRGBColorSpace;
|
|
175
|
+
gl.toneMapping = THREE.NoToneMapping;
|
|
176
|
+
renderer.current = gl;
|
|
177
|
+
} catch {
|
|
178
|
+
setError("Die 3D-B\xFChne ben\xF6tigt WebGL 2. Bitte Browser und Grafikunterst\xFCtzung pr\xFCfen.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const lost = (event) => {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
setError("Der WebGL-Kontext der 3D-B\xFChne wurde verloren. Bitte die B\xFChne erneut \xF6ffnen.");
|
|
184
|
+
};
|
|
185
|
+
el.addEventListener("webglcontextlost", lost);
|
|
186
|
+
const observer = new ResizeObserver(([entry]) => setSize({ width: entry.contentRect.width, height: entry.contentRect.height }));
|
|
187
|
+
observer.observe(el);
|
|
188
|
+
return () => {
|
|
189
|
+
observer.disconnect();
|
|
190
|
+
el.removeEventListener("webglcontextlost", lost);
|
|
191
|
+
renderer.current = null;
|
|
192
|
+
world.camera = null;
|
|
193
|
+
world.meshes.clear();
|
|
194
|
+
gl.dispose();
|
|
195
|
+
gl.forceContextLoss();
|
|
196
|
+
};
|
|
197
|
+
}, [world]);
|
|
198
|
+
React.useLayoutEffect(() => {
|
|
199
|
+
const gl = renderer.current;
|
|
200
|
+
if (!gl || !size.width || !size.height) return;
|
|
201
|
+
const scene = new THREE.Scene();
|
|
202
|
+
scene.background = new THREE.Color(view === "camera" ? "#eef0f3" : "#0d0f12");
|
|
203
|
+
const pixel = view === "camera" || !planeView ? 0.01 : 1 / planeView.zoom;
|
|
204
|
+
let camera = perspective;
|
|
205
|
+
try {
|
|
206
|
+
world.meshes.clear();
|
|
207
|
+
if (view !== "camera") {
|
|
208
|
+
if (!planeView) return;
|
|
209
|
+
const [u, v] = planeView.center;
|
|
210
|
+
const ortho = new THREE.OrthographicCamera(-size.width / 2, size.width / 2, size.height / 2, -size.height / 2, 0.1, 4e3);
|
|
211
|
+
ortho.zoom = planeView.zoom;
|
|
212
|
+
if (view === "top") {
|
|
213
|
+
ortho.position.set(u, v, 1e3);
|
|
214
|
+
ortho.up.set(0, 1, 0);
|
|
215
|
+
ortho.lookAt(u, v, 0);
|
|
216
|
+
} else {
|
|
217
|
+
ortho.position.set(u, -1e3, v);
|
|
218
|
+
ortho.up.set(0, 0, 1);
|
|
219
|
+
ortho.lookAt(u, 0, v);
|
|
220
|
+
}
|
|
221
|
+
ortho.updateProjectionMatrix();
|
|
222
|
+
ortho.updateMatrixWorld();
|
|
223
|
+
camera = ortho;
|
|
224
|
+
addGrid(scene, view, planeView, size.width, size.height);
|
|
225
|
+
addCameraGlyph(scene, perspective, frustum, pixel, cameraActive);
|
|
226
|
+
}
|
|
227
|
+
for (const node of frame.nodes) {
|
|
228
|
+
const mesh = proxyMesh(node);
|
|
229
|
+
const editor = view !== "camera", active = selected === node.node_id, hover = hovered === node.node_id;
|
|
230
|
+
mesh.material.transparent = editor;
|
|
231
|
+
mesh.material.opacity = editor ? active || hover ? 0.95 : 0.78 : 1;
|
|
232
|
+
if (editor || active) mesh.add(new THREE.LineSegments(
|
|
233
|
+
new THREE.EdgesGeometry(mesh.geometry, 15),
|
|
234
|
+
new THREE.LineBasicMaterial({ color: active ? "#ffffff" : hover ? "#f5f5f5" : node.preview_color.hex })
|
|
235
|
+
));
|
|
236
|
+
scene.add(mesh);
|
|
237
|
+
world.meshes.set(node.node_id, mesh);
|
|
238
|
+
if (editor && active) {
|
|
239
|
+
const halo = new THREE.Mesh(mesh.geometry, new THREE.MeshBasicMaterial({ color: "#ffffff", transparent: true, opacity: 0.14, depthWrite: false }));
|
|
240
|
+
halo.position.copy(mesh.position);
|
|
241
|
+
halo.rotation.copy(mesh.rotation);
|
|
242
|
+
halo.scale.set(...node.dimensions.map((d, i) => node.scale[i] + 2 * 5 * pixel / d));
|
|
243
|
+
halo.renderOrder = -1;
|
|
244
|
+
scene.add(halo);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
world.camera = camera;
|
|
248
|
+
scene.updateMatrixWorld(true);
|
|
249
|
+
gl.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
|
|
250
|
+
gl.setSize(size.width, size.height, false);
|
|
251
|
+
gl.render(scene, camera);
|
|
252
|
+
} catch (cause) {
|
|
253
|
+
setError(`Die 3D-B\xFChne konnte nicht gerendert werden: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
254
|
+
}
|
|
255
|
+
return () => {
|
|
256
|
+
world.meshes.clear();
|
|
257
|
+
world.camera = null;
|
|
258
|
+
disposeScene(scene);
|
|
259
|
+
};
|
|
260
|
+
}, [frame, perspective, view, planeView, selected, hovered, frustum, world, cameraActive, size]);
|
|
261
|
+
if (error) throw new Error(error);
|
|
262
|
+
return /* @__PURE__ */ jsx("canvas", { ref: canvas, className: "szenen-3d__canvas", "aria-label": "3D-Rendering" });
|
|
263
|
+
}
|
|
264
|
+
function StageEngine(props) {
|
|
265
|
+
const { view, selected, onSelect, onMove, onMoveCamera } = props;
|
|
266
|
+
const labels = props.labels ?? {};
|
|
267
|
+
const frame = React.useMemo(() => stageFrame(props.input), [props.input]);
|
|
268
|
+
const savedCamera = React.useMemo(() => cameraFor(frame), [frame]);
|
|
269
|
+
const [drag, setDrag] = useState(null);
|
|
270
|
+
const [hovered, setHovered] = useState(null);
|
|
271
|
+
const [views, setViews] = useState({ top: null, side: null });
|
|
272
|
+
const [box, setBox] = useState({ width: 0, height: 0 });
|
|
273
|
+
const viewport = useRef(null);
|
|
274
|
+
const surface = useRef(null);
|
|
275
|
+
const gesture = useRef(null);
|
|
276
|
+
const pointers = useRef(/* @__PURE__ */ new Map());
|
|
277
|
+
const pinch = useRef(null);
|
|
278
|
+
const world = React.useMemo(() => ({ camera: null, meshes: /* @__PURE__ */ new Map() }), []);
|
|
279
|
+
const live = React.useMemo(() => withDrag(frame, drag), [frame, drag]);
|
|
280
|
+
const perspective = React.useMemo(() => {
|
|
281
|
+
if (live === frame) return savedCamera;
|
|
282
|
+
try {
|
|
283
|
+
return cameraFor(live);
|
|
284
|
+
} catch {
|
|
285
|
+
return savedCamera;
|
|
286
|
+
}
|
|
287
|
+
}, [live, frame, savedCamera]);
|
|
288
|
+
const plane = view === "camera" ? null : view;
|
|
289
|
+
const planeView = plane ? views[plane] : null;
|
|
290
|
+
const points = React.useMemo(() => [...frame.nodes.flatMap((n) => corners(n).map((c) => c.toArray())), frame.camera.position], [frame]);
|
|
291
|
+
const frustum = React.useMemo(() => {
|
|
292
|
+
const forward = perspective.getWorldDirection(new THREE.Vector3());
|
|
293
|
+
const depth = Math.max(0, ...live.nodes.flatMap((n) => corners(n)).map((c) => c.sub(perspective.position).dot(forward)));
|
|
294
|
+
return Math.max(1, depth + 0.5);
|
|
295
|
+
}, [live, perspective]);
|
|
296
|
+
const fit = React.useCallback(() => {
|
|
297
|
+
if (!plane || !box.width) return;
|
|
298
|
+
setViews((v) => ({ ...v, [plane]: fitPlaneView(points, plane, Math.max(1, box.width - 32), Math.max(1, box.height - 2 * 56)) }));
|
|
299
|
+
}, [plane, box, points]);
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (plane && !views[plane] && box.width) fit();
|
|
302
|
+
}, [plane, views, box, fit]);
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
const el = surface.current;
|
|
305
|
+
if (!el) return;
|
|
306
|
+
const observer = new ResizeObserver(([entry]) => setBox({ width: entry.contentRect.width, height: entry.contentRect.height }));
|
|
307
|
+
observer.observe(el);
|
|
308
|
+
return () => observer.disconnect();
|
|
309
|
+
}, []);
|
|
310
|
+
const zoomBy = React.useCallback((factor, dx = 0, dy = 0) => {
|
|
311
|
+
if (!plane) return;
|
|
312
|
+
setViews((v) => v[plane] ? { ...v, [plane]: zoomPlaneView(v[plane], factor, dx, dy) } : v);
|
|
313
|
+
}, [plane]);
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
const el = surface.current;
|
|
316
|
+
if (!el || !plane) return;
|
|
317
|
+
const wheel = (event) => {
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
const rect = el.getBoundingClientRect();
|
|
320
|
+
const delta = event.deltaY * (event.deltaMode === 1 ? 16 : 1);
|
|
321
|
+
zoomBy(Math.exp(-delta * 15e-4), event.clientX - rect.left - rect.width / 2, event.clientY - rect.top - rect.height / 2);
|
|
322
|
+
};
|
|
323
|
+
el.addEventListener("wheel", wheel, { passive: false });
|
|
324
|
+
return () => el.removeEventListener("wheel", wheel);
|
|
325
|
+
}, [plane, zoomBy]);
|
|
326
|
+
function pick(clientX, clientY, touch = false) {
|
|
327
|
+
const el = surface.current, camera = world.camera;
|
|
328
|
+
if (!el || !camera) return null;
|
|
329
|
+
const rect = el.getBoundingClientRect();
|
|
330
|
+
const ndc = new THREE.Vector2((clientX - rect.left) / rect.width * 2 - 1, -(clientY - rect.top) / rect.height * 2 + 1);
|
|
331
|
+
if (plane) {
|
|
332
|
+
const c = perspective.position.clone().project(camera);
|
|
333
|
+
if (Math.hypot((c.x - ndc.x) * rect.width / 2, (c.y - ndc.y) * rect.height / 2) <= (touch ? 24 : 14)) return { kind: "camera", id: "camera" };
|
|
334
|
+
}
|
|
335
|
+
const raycaster = new THREE.Raycaster();
|
|
336
|
+
raycaster.setFromCamera(ndc, camera);
|
|
337
|
+
const hit = raycaster.intersectObjects([...world.meshes.values()], false)[0];
|
|
338
|
+
if (hit) return { kind: "node", id: hit.object.userData.nodeId };
|
|
339
|
+
if (!touch) return null;
|
|
340
|
+
const px = clientX - rect.left, py = clientY - rect.top;
|
|
341
|
+
let best = null;
|
|
342
|
+
for (const node of live.nodes) {
|
|
343
|
+
const screen = corners(node).map((c) => c.project(camera)).map((c) => [(c.x + 1) / 2 * rect.width, (1 - c.y) / 2 * rect.height]);
|
|
344
|
+
const xs = screen.map((p) => p[0]), ys = screen.map((p) => p[1]);
|
|
345
|
+
const distance = Math.hypot(Math.max(Math.min(...xs) - px, 0, px - Math.max(...xs)), Math.max(Math.min(...ys) - py, 0, py - Math.max(...ys)));
|
|
346
|
+
if (distance <= 20 && (!best || distance < best.distance)) best = { id: node.node_id, distance };
|
|
347
|
+
}
|
|
348
|
+
return best ? { kind: "node", id: best.id } : null;
|
|
349
|
+
}
|
|
350
|
+
const local = (x, y) => {
|
|
351
|
+
const rect = surface.current.getBoundingClientRect();
|
|
352
|
+
return [x - rect.left - rect.width / 2, y - rect.top - rect.height / 2];
|
|
353
|
+
};
|
|
354
|
+
function onPointerDown(event) {
|
|
355
|
+
pointers.current.set(event.pointerId, [event.clientX, event.clientY]);
|
|
356
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
357
|
+
if (pointers.current.size === 2 && plane && planeView) {
|
|
358
|
+
const [[ax, ay], [bx, by]] = [...pointers.current.values()];
|
|
359
|
+
gesture.current = null;
|
|
360
|
+
setDrag(null);
|
|
361
|
+
pinch.current = { distance: Math.max(1, Math.hypot(ax - bx, ay - by)), mid: local((ax + bx) / 2, (ay + by) / 2), view: planeView };
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (gesture.current || pinch.current) return;
|
|
365
|
+
viewport.current?.focus({ preventScroll: true });
|
|
366
|
+
const hit = event.button === 0 ? pick(event.clientX, event.clientY, event.pointerType !== "mouse") : null;
|
|
367
|
+
const node = hit?.kind === "node" ? live.nodes.find((n) => n.node_id === hit.id) : null;
|
|
368
|
+
const kind = !plane ? hit ? hit.kind : "none" : hit ? hit.kind : "pan";
|
|
369
|
+
gesture.current = {
|
|
370
|
+
pointer: event.pointerId,
|
|
371
|
+
kind,
|
|
372
|
+
id: hit?.id ?? "",
|
|
373
|
+
x: event.clientX,
|
|
374
|
+
y: event.clientY,
|
|
375
|
+
view: planeView,
|
|
376
|
+
moved: false,
|
|
377
|
+
start: hit?.kind === "camera" ? live.camera.position : node?.position ?? [0, 0, 0],
|
|
378
|
+
threshold: event.pointerType === "mouse" ? 3 : 8
|
|
379
|
+
};
|
|
380
|
+
if (hit?.kind === "node") onSelect(hit.id);
|
|
381
|
+
}
|
|
382
|
+
function onPointerMove(event) {
|
|
383
|
+
if (pointers.current.has(event.pointerId)) pointers.current.set(event.pointerId, [event.clientX, event.clientY]);
|
|
384
|
+
const p = pinch.current;
|
|
385
|
+
if (p && plane) {
|
|
386
|
+
const [[ax, ay], [bx, by]] = [...pointers.current.values()];
|
|
387
|
+
const zoom2 = clampZoom(p.view.zoom * Math.hypot(ax - bx, ay - by) / p.distance);
|
|
388
|
+
const [mx, my] = local((ax + bx) / 2, (ay + by) / 2);
|
|
389
|
+
const [u, v] = screenToPlane(p.view, p.mid[0], p.mid[1]);
|
|
390
|
+
setViews((vs) => ({ ...vs, [plane]: { zoom: zoom2, center: [u - mx / zoom2, v + my / zoom2] } }));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const g = gesture.current;
|
|
394
|
+
if (!g) {
|
|
395
|
+
if (event.pointerType !== "mouse") return;
|
|
396
|
+
const hit = pick(event.clientX, event.clientY);
|
|
397
|
+
const id = hit ? hit.kind === "camera" ? "#camera" : hit.id : null;
|
|
398
|
+
if (id !== hovered) setHovered(id);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (g.pointer !== event.pointerId) return;
|
|
402
|
+
const dx = event.clientX - g.x, dy = event.clientY - g.y;
|
|
403
|
+
if (!g.moved && Math.hypot(dx, dy) < g.threshold) return;
|
|
404
|
+
g.moved = true;
|
|
405
|
+
if (!plane || !g.view) return;
|
|
406
|
+
const zoom = g.view.zoom;
|
|
407
|
+
if (g.kind === "pan") {
|
|
408
|
+
const [u, v] = g.view.center;
|
|
409
|
+
setViews((vs) => ({ ...vs, [plane]: { zoom, center: [u - dx / zoom, v + dy / zoom] } }));
|
|
410
|
+
} else if (g.kind === "node" || g.kind === "camera" && onMoveCamera) {
|
|
411
|
+
const moved = nudgePosition(g.start, plane, dx / zoom, -dy / zoom);
|
|
412
|
+
setDrag({ kind: g.kind, id: g.id, position: event.shiftKey ? moved : snapPosition(moved, plane, SNAP) });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function finish(event, save) {
|
|
416
|
+
pointers.current.delete(event.pointerId);
|
|
417
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
418
|
+
if (pinch.current) {
|
|
419
|
+
if (!pointers.current.size) pinch.current = null;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const g = gesture.current;
|
|
423
|
+
if (!g || g.pointer !== event.pointerId) return;
|
|
424
|
+
gesture.current = null;
|
|
425
|
+
if (save && g.moved && drag) {
|
|
426
|
+
if (drag.kind === "node") onMove(drag.id, drag.position);
|
|
427
|
+
else onMoveCamera?.(drag.position);
|
|
428
|
+
}
|
|
429
|
+
if (save && !g.moved && g.kind === "pan") onSelect(null);
|
|
430
|
+
setDrag(null);
|
|
431
|
+
}
|
|
432
|
+
function onKeyDown(event) {
|
|
433
|
+
const arrows = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, 1], ArrowDown: [0, -1] };
|
|
434
|
+
const node = live.nodes.find((n) => n.node_id === selected);
|
|
435
|
+
if (arrows[event.key] && plane && node) {
|
|
436
|
+
const step = event.shiftKey ? 0.5 : SNAP;
|
|
437
|
+
onMove(node.node_id, nudgePosition(node.position, plane, arrows[event.key][0] * step, arrows[event.key][1] * step));
|
|
438
|
+
} else if (event.key === "Escape" && selected) onSelect(null);
|
|
439
|
+
else if ((event.key === "f" || event.key === "F") && plane) fit();
|
|
440
|
+
else if ((event.key === "+" || event.key === "=") && plane) zoomBy(1.25);
|
|
441
|
+
else if (event.key === "-" && plane) zoomBy(0.8);
|
|
442
|
+
else return;
|
|
443
|
+
event.preventDefault();
|
|
444
|
+
}
|
|
445
|
+
const axes = plane ? planeAxes(plane) : null;
|
|
446
|
+
const toScreen = (u, v) => planeView ? [box.width / 2 + (u - planeView.center[0]) * planeView.zoom, box.height / 2 - (v - planeView.center[1]) * planeView.zoom] : [0, 0];
|
|
447
|
+
const target = drag ? drag.kind === "camera" ? "#camera" : drag.id : hovered;
|
|
448
|
+
const name = (id) => id === "#camera" ? frame.camera.name ?? "Kamera" : labels[id] ?? id;
|
|
449
|
+
const position = drag?.position ?? (target === "#camera" ? live.camera.position : live.nodes.find((n) => n.node_id === target)?.position);
|
|
450
|
+
const cursor = !plane ? hovered ? "pointer" : "default" : drag || gesture.current?.kind === "pan" && gesture.current.moved ? "grabbing" : hovered && (hovered !== "#camera" || onMoveCamera) ? "grab" : "default";
|
|
451
|
+
return /* @__PURE__ */ jsxs(
|
|
452
|
+
"div",
|
|
453
|
+
{
|
|
454
|
+
ref: viewport,
|
|
455
|
+
className: "szenen-3d__viewport",
|
|
456
|
+
"data-view": view,
|
|
457
|
+
tabIndex: 0,
|
|
458
|
+
onKeyDown,
|
|
459
|
+
"aria-label": `3D-B\xFChne, ${view === "top" ? "Draufsicht" : view === "side" ? "Seitenansicht" : "Kameraansicht"}. Ziehen verschiebt, Pfeiltasten verschieben das gew\xE4hlte Element.`,
|
|
460
|
+
children: [
|
|
461
|
+
/* @__PURE__ */ jsxs(
|
|
462
|
+
"div",
|
|
463
|
+
{
|
|
464
|
+
ref: surface,
|
|
465
|
+
className: "szenen-3d__surface",
|
|
466
|
+
style: { cursor, ...view === "camera" ? { ["--seitenverhaeltnis"]: String(frame.camera.aspect) } : {} },
|
|
467
|
+
onPointerDown,
|
|
468
|
+
onPointerMove,
|
|
469
|
+
onPointerUp: (e) => finish(e, true),
|
|
470
|
+
onPointerCancel: (e) => finish(e, false),
|
|
471
|
+
onPointerLeave: () => {
|
|
472
|
+
if (!gesture.current && hovered) setHovered(null);
|
|
473
|
+
},
|
|
474
|
+
children: [
|
|
475
|
+
/* @__PURE__ */ jsx(
|
|
476
|
+
StageSurface,
|
|
477
|
+
{
|
|
478
|
+
frame: live,
|
|
479
|
+
perspective,
|
|
480
|
+
view,
|
|
481
|
+
planeView,
|
|
482
|
+
selected,
|
|
483
|
+
hovered,
|
|
484
|
+
frustum,
|
|
485
|
+
world,
|
|
486
|
+
cameraActive: target === "#camera"
|
|
487
|
+
}
|
|
488
|
+
),
|
|
489
|
+
plane && planeView && /* @__PURE__ */ jsxs("div", { className: "szenen-3d__labels", "aria-hidden": "true", children: [
|
|
490
|
+
live.nodes.map((n) => {
|
|
491
|
+
const [u, v] = planeAxes(plane);
|
|
492
|
+
const edge = plane === "top" ? Math.min(...corners(n).map((c) => c.getComponent(v))) : Math.max(...corners(n).map((c) => c.getComponent(v)));
|
|
493
|
+
const [x, y] = toScreen(n.position[u], edge);
|
|
494
|
+
return /* @__PURE__ */ jsx(
|
|
495
|
+
"span",
|
|
496
|
+
{
|
|
497
|
+
className: `szenen-3d__label szenen-3d__label--${plane}${selected === n.node_id ? " is-selected" : ""}`,
|
|
498
|
+
style: { left: x, top: y, ["--farbe"]: n.preview_color.hex },
|
|
499
|
+
children: labels[n.node_id] ?? n.node_id
|
|
500
|
+
},
|
|
501
|
+
n.node_id
|
|
502
|
+
);
|
|
503
|
+
}),
|
|
504
|
+
(() => {
|
|
505
|
+
const [x, y] = toScreen(live.camera.position[planeAxes(plane)[0]], live.camera.position[planeAxes(plane)[1]]);
|
|
506
|
+
return /* @__PURE__ */ jsx("span", { className: `szenen-3d__label szenen-3d__label--kamera${x > box.width - 160 ? " is-links" : ""}`, style: { left: x, top: y }, children: frame.camera.name ?? "Kamera" });
|
|
507
|
+
})()
|
|
508
|
+
] })
|
|
509
|
+
]
|
|
510
|
+
}
|
|
511
|
+
),
|
|
512
|
+
plane ? /* @__PURE__ */ jsxs("div", { className: "szenen-3d__hud szenen-3d__hud--werkzeuge", role: "group", "aria-label": "Ansicht steuern", children: [
|
|
513
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => zoomBy(1 / 1.25), "aria-label": "Verkleinern", title: "Verkleinern (\u2212)", children: "\u2212" }),
|
|
514
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => zoomBy(1.25), "aria-label": "Vergr\xF6\xDFern", title: "Vergr\xF6\xDFern (+)", children: "+" }),
|
|
515
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: fit, title: "Alles einpassen (F)", children: "Einpassen" })
|
|
516
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "szenen-3d__hud szenen-3d__hud--kamera", children: [
|
|
517
|
+
frame.camera.name ?? "Kamera",
|
|
518
|
+
" \xB7 ",
|
|
519
|
+
frame.camera.aspect_ratio,
|
|
520
|
+
" \xB7 ",
|
|
521
|
+
meters(frame.camera.focal_length_mm).replace(/,00$/, ""),
|
|
522
|
+
" mm"
|
|
523
|
+
] }),
|
|
524
|
+
/* @__PURE__ */ jsx("div", { className: "szenen-3d__hud szenen-3d__hud--info", "aria-live": "polite", children: target && position ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
525
|
+
/* @__PURE__ */ jsx("strong", { children: name(target) }),
|
|
526
|
+
(axes ?? [0, 1, 2]).map((a) => /* @__PURE__ */ jsxs("span", { children: [
|
|
527
|
+
/* @__PURE__ */ jsx("i", { style: { color: AXIS_COLOR[a] }, children: "XYZ"[a] }),
|
|
528
|
+
" ",
|
|
529
|
+
meters(position[a])
|
|
530
|
+
] }, a)),
|
|
531
|
+
/* @__PURE__ */ jsx("span", { children: "m" })
|
|
532
|
+
] }) : plane ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
533
|
+
/* @__PURE__ */ jsx("span", { className: "szenen-3d__nur-maus", children: "Ziehen verschiebt \xB7 Mausrad zoomt \xB7 Shift ohne Raster" }),
|
|
534
|
+
/* @__PURE__ */ jsx("span", { className: "szenen-3d__nur-touch", children: "Ziehen verschiebt \xB7 zwei Finger zoomen" })
|
|
535
|
+
] }) : /* @__PURE__ */ jsx("span", { children: "So sieht das Kamera-Preview aus" }) }),
|
|
536
|
+
axes && /* @__PURE__ */ jsxs("div", { className: "szenen-3d__hud szenen-3d__hud--achsen", "aria-hidden": "true", children: [
|
|
537
|
+
/* @__PURE__ */ jsxs("span", { style: { color: AXIS_COLOR[axes[0]] }, children: [
|
|
538
|
+
"XYZ"[axes[0]],
|
|
539
|
+
" \u2192"
|
|
540
|
+
] }),
|
|
541
|
+
/* @__PURE__ */ jsxs("span", { style: { color: AXIS_COLOR[axes[1]] }, children: [
|
|
542
|
+
"XYZ"[axes[1]],
|
|
543
|
+
" \u2191"
|
|
544
|
+
] })
|
|
545
|
+
] })
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
async function renderCameraPreview(input) {
|
|
551
|
+
const frame = stageFrame(input);
|
|
552
|
+
const [rw, rh] = frame.camera.aspect_ratio.split(":").map(Number);
|
|
553
|
+
const scale = 768 / Math.max(rw, rh);
|
|
554
|
+
const width = Math.round(rw * scale), height = Math.round(rh * scale);
|
|
555
|
+
if (Math.abs(width / height - frame.camera.aspect) > 1e-9) throw new Error("Dieses Kameraformat l\xE4sst sich mit der Preview-Aufl\xF6sung nicht exakt abbilden. Bitte ein Format-Preset w\xE4hlen.");
|
|
556
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
557
|
+
const scene = new THREE.Scene();
|
|
558
|
+
scene.background = new THREE.Color("#eef0f3");
|
|
559
|
+
try {
|
|
560
|
+
renderer.setSize(width, height, false);
|
|
561
|
+
renderer.setPixelRatio(1);
|
|
562
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
563
|
+
renderer.toneMapping = THREE.NoToneMapping;
|
|
564
|
+
for (const node of frame.nodes) {
|
|
565
|
+
scene.add(proxyMesh(node));
|
|
566
|
+
}
|
|
567
|
+
renderer.render(scene, cameraFor(frame));
|
|
568
|
+
const gl = renderer.getContext();
|
|
569
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
570
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, rgba);
|
|
571
|
+
const solid_color_pixels = Object.fromEntries(frame.nodes.map((n) => [n.node_id, 0]));
|
|
572
|
+
const lookup = new Map(frame.nodes.map((n) => [parseInt(n.preview_color.hex.slice(1), 16), n.node_id]));
|
|
573
|
+
for (let i = 0; i < rgba.length; i += 4) {
|
|
574
|
+
const id = lookup.get(rgba[i] << 16 | rgba[i + 1] << 8 | rgba[i + 2]);
|
|
575
|
+
if (id) solid_color_pixels[id]++;
|
|
576
|
+
}
|
|
577
|
+
const data = renderer.domElement.toDataURL("image/png").split(",")[1];
|
|
578
|
+
const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
|
579
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
580
|
+
return { data, solid_color_pixels, mime_type: "image/png", width, height, camera_id: frame.camera.camera_id, sha256: Array.from(new Uint8Array(digest), (v) => v.toString(16).padStart(2, "0")).join("") };
|
|
581
|
+
} finally {
|
|
582
|
+
disposeScene(scene);
|
|
583
|
+
renderer.dispose();
|
|
584
|
+
renderer.forceContextLoss();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
export {
|
|
588
|
+
StageEngine,
|
|
589
|
+
renderCameraPreview
|
|
590
|
+
};
|