@layoutit/polycss-react 0.0.1
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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/index.cjs +4004 -0
- package/dist/index.d.cts +669 -0
- package/dist/index.d.ts +669 -0
- package/dist/index.js +3978 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3978 @@
|
|
|
1
|
+
// src/camera/PolyPerspectiveCamera.tsx
|
|
2
|
+
import { memo, useMemo as useMemo2 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/camera/context.ts
|
|
5
|
+
import { createContext, useContext } from "react";
|
|
6
|
+
var PolyCameraContext = createContext(null);
|
|
7
|
+
function useCameraContext() {
|
|
8
|
+
const ctx = useContext(PolyCameraContext);
|
|
9
|
+
if (!ctx) {
|
|
10
|
+
throw new Error("polycss: PolyScene must be used inside a PolyCamera.");
|
|
11
|
+
}
|
|
12
|
+
return ctx;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/camera/useCamera.ts
|
|
16
|
+
import { useRef as useRef2, useCallback as useCallback2, useEffect, useMemo } from "react";
|
|
17
|
+
import { createIsometricCamera, BASE_TILE } from "@layoutit/polycss-core";
|
|
18
|
+
|
|
19
|
+
// src/store/sceneStore.ts
|
|
20
|
+
import { useSyncExternalStore, useRef, useCallback } from "react";
|
|
21
|
+
function createSceneStore(initial) {
|
|
22
|
+
let state = {
|
|
23
|
+
cameraState: { ...initial }
|
|
24
|
+
};
|
|
25
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
26
|
+
function notify() {
|
|
27
|
+
for (const listener of listeners) listener();
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
getState() {
|
|
31
|
+
return state;
|
|
32
|
+
},
|
|
33
|
+
setState(partial) {
|
|
34
|
+
state = { ...state, ...partial };
|
|
35
|
+
notify();
|
|
36
|
+
},
|
|
37
|
+
subscribe(listener) {
|
|
38
|
+
listeners.add(listener);
|
|
39
|
+
return () => listeners.delete(listener);
|
|
40
|
+
},
|
|
41
|
+
updateCameraFromRef(handle) {
|
|
42
|
+
state = { cameraState: { ...handle.state } };
|
|
43
|
+
notify();
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
notifyAll() {
|
|
47
|
+
notify();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/camera/useCamera.ts
|
|
53
|
+
function usePolyCamera(options) {
|
|
54
|
+
const handleRef = useRef2(null);
|
|
55
|
+
if (!handleRef.current) {
|
|
56
|
+
handleRef.current = createIsometricCamera({
|
|
57
|
+
zoom: options.zoom,
|
|
58
|
+
target: options.target,
|
|
59
|
+
rotX: options.rotX,
|
|
60
|
+
rotY: options.rotY,
|
|
61
|
+
distance: options.distance
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const sceneElRef = useRef2(null);
|
|
65
|
+
const cameraElRef = useRef2(null);
|
|
66
|
+
const store = useMemo(
|
|
67
|
+
() => createSceneStore(handleRef.current.state),
|
|
68
|
+
[]
|
|
69
|
+
);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const handle = handleRef.current;
|
|
72
|
+
const next = {};
|
|
73
|
+
if (options.zoom !== void 0) next.zoom = options.zoom;
|
|
74
|
+
if (options.target !== void 0) next.target = options.target;
|
|
75
|
+
if (options.rotX !== void 0) next.rotX = options.rotX;
|
|
76
|
+
if (options.rotY !== void 0) next.rotY = options.rotY;
|
|
77
|
+
if (options.distance !== void 0) next.distance = options.distance;
|
|
78
|
+
if (Object.keys(next).length > 0) {
|
|
79
|
+
handle.update(next);
|
|
80
|
+
const el = sceneElRef.current;
|
|
81
|
+
if (el) {
|
|
82
|
+
const s = handle.state;
|
|
83
|
+
const tileSize = BASE_TILE;
|
|
84
|
+
const [tx, ty, tz] = s.target;
|
|
85
|
+
const cssX = ty * tileSize;
|
|
86
|
+
const cssY = tx * tileSize;
|
|
87
|
+
const cssZ = tz * tileSize;
|
|
88
|
+
const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
|
|
89
|
+
el.style.transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
|
|
90
|
+
}
|
|
91
|
+
store.updateCameraFromRef(handle);
|
|
92
|
+
store.notifyAll();
|
|
93
|
+
}
|
|
94
|
+
}, [options.zoom, options.target, options.rotX, options.rotY, options.distance, store]);
|
|
95
|
+
const applyTransformDirect = useCallback2(() => {
|
|
96
|
+
const el = sceneElRef.current;
|
|
97
|
+
if (!el) return;
|
|
98
|
+
const handle = handleRef.current;
|
|
99
|
+
const s = handle.state;
|
|
100
|
+
const tileSize = BASE_TILE;
|
|
101
|
+
const [tx, ty, tz] = s.target;
|
|
102
|
+
const cssX = ty * tileSize;
|
|
103
|
+
const cssY = tx * tileSize;
|
|
104
|
+
const cssZ = tz * tileSize;
|
|
105
|
+
const distancePart = s.distance !== 0 ? `translateZ(${-s.distance}px) ` : "";
|
|
106
|
+
el.style.transform = `${distancePart}scale(${s.zoom}) rotateX(${s.rotX}deg) rotate(${s.rotY}deg) translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
|
|
107
|
+
}, []);
|
|
108
|
+
return {
|
|
109
|
+
store,
|
|
110
|
+
cameraRef: handleRef,
|
|
111
|
+
sceneElRef,
|
|
112
|
+
cameraElRef,
|
|
113
|
+
applyTransformDirect
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/camera/PolyPerspectiveCamera.tsx
|
|
118
|
+
import { jsx } from "react/jsx-runtime";
|
|
119
|
+
var DEFAULT_PERSPECTIVE = 8e3;
|
|
120
|
+
function PolyPerspectiveCameraInner({
|
|
121
|
+
zoom,
|
|
122
|
+
target,
|
|
123
|
+
rotX,
|
|
124
|
+
rotY,
|
|
125
|
+
distance,
|
|
126
|
+
perspective,
|
|
127
|
+
children,
|
|
128
|
+
className,
|
|
129
|
+
style
|
|
130
|
+
}) {
|
|
131
|
+
const {
|
|
132
|
+
store,
|
|
133
|
+
cameraRef,
|
|
134
|
+
sceneElRef,
|
|
135
|
+
cameraElRef,
|
|
136
|
+
applyTransformDirect
|
|
137
|
+
} = usePolyCamera({ zoom, target, rotX, rotY, distance });
|
|
138
|
+
const contextValue = useMemo2(
|
|
139
|
+
() => ({ store, cameraRef, sceneElRef, cameraElRef, applyTransformDirect }),
|
|
140
|
+
[store, cameraRef, sceneElRef, cameraElRef, applyTransformDirect]
|
|
141
|
+
);
|
|
142
|
+
const perspectiveValue = `${typeof perspective === "number" ? perspective : DEFAULT_PERSPECTIVE}px`;
|
|
143
|
+
const cameraStyle = {
|
|
144
|
+
...style,
|
|
145
|
+
perspective: perspectiveValue
|
|
146
|
+
};
|
|
147
|
+
return /* @__PURE__ */ jsx(PolyCameraContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
148
|
+
"div",
|
|
149
|
+
{
|
|
150
|
+
ref: cameraElRef,
|
|
151
|
+
className: `polycss-camera${className ? ` ${className}` : ""}`,
|
|
152
|
+
style: cameraStyle,
|
|
153
|
+
children
|
|
154
|
+
}
|
|
155
|
+
) });
|
|
156
|
+
}
|
|
157
|
+
var PolyPerspectiveCamera = memo(PolyPerspectiveCameraInner);
|
|
158
|
+
|
|
159
|
+
// src/camera/PolyOrthographicCamera.tsx
|
|
160
|
+
import { memo as memo2, useMemo as useMemo3 } from "react";
|
|
161
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
162
|
+
function PolyOrthographicCameraInner({
|
|
163
|
+
zoom,
|
|
164
|
+
target,
|
|
165
|
+
rotX,
|
|
166
|
+
rotY,
|
|
167
|
+
distance,
|
|
168
|
+
children,
|
|
169
|
+
className,
|
|
170
|
+
style
|
|
171
|
+
}) {
|
|
172
|
+
const {
|
|
173
|
+
store,
|
|
174
|
+
cameraRef,
|
|
175
|
+
sceneElRef,
|
|
176
|
+
cameraElRef,
|
|
177
|
+
applyTransformDirect
|
|
178
|
+
} = usePolyCamera({ zoom, target, rotX, rotY, distance });
|
|
179
|
+
const contextValue = useMemo3(
|
|
180
|
+
() => ({ store, cameraRef, sceneElRef, cameraElRef, applyTransformDirect }),
|
|
181
|
+
[store, cameraRef, sceneElRef, cameraElRef, applyTransformDirect]
|
|
182
|
+
);
|
|
183
|
+
const cameraStyle = {
|
|
184
|
+
...style,
|
|
185
|
+
perspective: "none"
|
|
186
|
+
};
|
|
187
|
+
return /* @__PURE__ */ jsx2(PolyCameraContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx2(
|
|
188
|
+
"div",
|
|
189
|
+
{
|
|
190
|
+
ref: cameraElRef,
|
|
191
|
+
className: `polycss-camera${className ? ` ${className}` : ""}`,
|
|
192
|
+
style: cameraStyle,
|
|
193
|
+
children
|
|
194
|
+
}
|
|
195
|
+
) });
|
|
196
|
+
}
|
|
197
|
+
var PolyOrthographicCamera = memo2(PolyOrthographicCameraInner);
|
|
198
|
+
|
|
199
|
+
// src/scene/PolyScene.tsx
|
|
200
|
+
import { memo as memo3, useCallback as useCallback4, useEffect as useEffect3, useMemo as useMemo6, useRef as useRef3 } from "react";
|
|
201
|
+
import { createIsometricCamera as createIsometricCamera2, parseHexColor } from "@layoutit/polycss-core";
|
|
202
|
+
|
|
203
|
+
// src/scene/useSceneContext.ts
|
|
204
|
+
import { useMemo as useMemo4 } from "react";
|
|
205
|
+
import { buildSceneContext, mergePolygons } from "@layoutit/polycss-core";
|
|
206
|
+
function usePolySceneContext(polygons, options) {
|
|
207
|
+
const { directionalLight: _directionalLight } = options;
|
|
208
|
+
return useMemo4(() => {
|
|
209
|
+
const built = buildSceneContext({ polygons });
|
|
210
|
+
const finalPolygons = mergePolygons(built.context.polygons);
|
|
211
|
+
return {
|
|
212
|
+
polygons: finalPolygons,
|
|
213
|
+
sceneBbox: built.context.sceneBbox
|
|
214
|
+
};
|
|
215
|
+
}, [polygons]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/styles/styles.ts
|
|
219
|
+
var POLYCSS_STYLE_ID = "polycss-styles";
|
|
220
|
+
function injectPolyBaseStyles(doc = typeof document !== "undefined" ? document : null) {
|
|
221
|
+
if (!doc || doc.getElementById(POLYCSS_STYLE_ID)) return;
|
|
222
|
+
const style = doc.createElement("style");
|
|
223
|
+
style.id = POLYCSS_STYLE_ID;
|
|
224
|
+
style.textContent = CORE_BASE_STYLES;
|
|
225
|
+
doc.head.appendChild(style);
|
|
226
|
+
}
|
|
227
|
+
var CORE_BASE_STYLES = `
|
|
228
|
+
/* \u2500\u2500 Scene container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
229
|
+
|
|
230
|
+
.polycss-scene,
|
|
231
|
+
.polycss-scene *,
|
|
232
|
+
.polycss-scene *::before,
|
|
233
|
+
.polycss-scene *::after {
|
|
234
|
+
box-sizing: border-box;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.polycss-scene {
|
|
238
|
+
position: absolute;
|
|
239
|
+
top: 50%;
|
|
240
|
+
left: 50%;
|
|
241
|
+
width: 0;
|
|
242
|
+
height: 0;
|
|
243
|
+
transform-style: preserve-3d;
|
|
244
|
+
perspective: none;
|
|
245
|
+
transform: var(--scene-transform);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.polycss-offset {
|
|
249
|
+
transform-style: preserve-3d;
|
|
250
|
+
transform: var(--offset-transform);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* \u2500\u2500 Camera wrapper (perspective + interactive drag) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
254
|
+
|
|
255
|
+
.polycss-camera {
|
|
256
|
+
display: flex;
|
|
257
|
+
width: 100%;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
align-items: center;
|
|
260
|
+
perspective: 8000px;
|
|
261
|
+
min-height: inherit;
|
|
262
|
+
height: 100%;
|
|
263
|
+
position: relative;
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
contain: paint;
|
|
266
|
+
isolation: isolate;
|
|
267
|
+
}
|
|
268
|
+
.polycss-camera * {
|
|
269
|
+
transform-style: preserve-3d;
|
|
270
|
+
position: absolute;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* \u2500\u2500 Polygon leaf element \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
274
|
+
|
|
275
|
+
/*
|
|
276
|
+
* Polygon faces render as internal leaf elements inside .polycss-scene.
|
|
277
|
+
* The element is positioned absolutely within the scene root; its
|
|
278
|
+
* transform: matrix3d(...) carries the full world-space placement.
|
|
279
|
+
*/
|
|
280
|
+
.polycss-scene b,
|
|
281
|
+
.polycss-scene i,
|
|
282
|
+
.polycss-scene s,
|
|
283
|
+
.polycss-scene u {
|
|
284
|
+
position: absolute;
|
|
285
|
+
display: block;
|
|
286
|
+
transform-origin: 0 0;
|
|
287
|
+
transform-style: preserve-3d;
|
|
288
|
+
margin: 0;
|
|
289
|
+
padding: 0;
|
|
290
|
+
font: inherit;
|
|
291
|
+
font-weight: normal;
|
|
292
|
+
font-style: normal;
|
|
293
|
+
line-height: 0;
|
|
294
|
+
quotes: none;
|
|
295
|
+
text-decoration: none;
|
|
296
|
+
backface-visibility: hidden;
|
|
297
|
+
background-repeat: no-repeat;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.polycss-scene b {
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.polycss-scene i {
|
|
304
|
+
border-style: solid;
|
|
305
|
+
border-width: 1px;
|
|
306
|
+
border-color: currentColor;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.polycss-scene s {
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.polycss-scene u {
|
|
313
|
+
width: 0px;
|
|
314
|
+
height: 0px;
|
|
315
|
+
background: transparent;
|
|
316
|
+
box-sizing: content-box;
|
|
317
|
+
border: 0 solid transparent;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* \u2500\u2500 Gizmo override \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
321
|
+
|
|
322
|
+
/*
|
|
323
|
+
* <TransformControls> renders 3D arrows using the same polygon pipeline
|
|
324
|
+
* as user content, but the gizmo is a UI affordance \u2014 both faces of
|
|
325
|
+
* every polygon should remain visible regardless of which way the
|
|
326
|
+
* camera is looking. Otherwise the cuboid shafts and pyramid heads end
|
|
327
|
+
* up half-culled (you see only the side faces, not the caps), and the
|
|
328
|
+
* arrow looks like a flat strip instead of a 3D bar.
|
|
329
|
+
*
|
|
330
|
+
* Transitions on border-color and background-color smooth the
|
|
331
|
+
* idle \u2192 hover \u2192 drag alpha changes. Baked-mode arrows render their
|
|
332
|
+
* color via inline border-color, dynamic-mode via background-color
|
|
333
|
+
* (rgb-with-alpha CSS calc); transitioning both covers either path.
|
|
334
|
+
*/
|
|
335
|
+
.polycss-transform-controls i,
|
|
336
|
+
.polycss-transform-controls b,
|
|
337
|
+
.polycss-transform-controls s,
|
|
338
|
+
.polycss-transform-controls u {
|
|
339
|
+
backface-visibility: visible;
|
|
340
|
+
transition: border-color 150ms ease-out, background-color 150ms ease-out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* \u2500\u2500 Dynamic lighting cascade vars (scene root \u2192 polygons) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
344
|
+
|
|
345
|
+
/*
|
|
346
|
+
* Dynamic mode: PolyScene writes the directional + ambient light setup to
|
|
347
|
+
* these custom properties on the scene root. Each polygon leaf bakes its
|
|
348
|
+
* own normal directly into an inline calc() that reads these vars to
|
|
349
|
+
* resolve the Lambert dot product and per-channel tint. Sliding the light
|
|
350
|
+
* only writes these scene-root vars \u2014 no JS, no atlas redraw.
|
|
351
|
+
*
|
|
352
|
+
* Registering with @property forces the browser to parse the values as
|
|
353
|
+
* <number>s instead of opaque token streams; that makes the polygon-level
|
|
354
|
+
* calc() expressions resolve reliably across engines.
|
|
355
|
+
*/
|
|
356
|
+
|
|
357
|
+
@property --plx { syntax: "<number>"; inherits: true; initial-value: 0; }
|
|
358
|
+
@property --ply { syntax: "<number>"; inherits: true; initial-value: 0; }
|
|
359
|
+
@property --plz { syntax: "<number>"; inherits: true; initial-value: 1; }
|
|
360
|
+
@property --plr { syntax: "<number>"; inherits: true; initial-value: 1; }
|
|
361
|
+
@property --plg { syntax: "<number>"; inherits: true; initial-value: 1; }
|
|
362
|
+
@property --plb { syntax: "<number>"; inherits: true; initial-value: 1; }
|
|
363
|
+
@property --pli { syntax: "<number>"; inherits: true; initial-value: 1; }
|
|
364
|
+
@property --par { syntax: "<number>"; inherits: true; initial-value: 1; }
|
|
365
|
+
@property --pag { syntax: "<number>"; inherits: true; initial-value: 1; }
|
|
366
|
+
@property --pab { syntax: "<number>"; inherits: true; initial-value: 1; }
|
|
367
|
+
@property --pai { syntax: "<number>"; inherits: true; initial-value: 0.4; }
|
|
368
|
+
|
|
369
|
+
/* Per-polygon surface normal \u2014 set inline by the renderer. inherits:false
|
|
370
|
+
because each leaf has its own normal (no cascade). */
|
|
371
|
+
@property --pnx { syntax: "<number>"; inherits: false; initial-value: 0; }
|
|
372
|
+
@property --pny { syntax: "<number>"; inherits: false; initial-value: 0; }
|
|
373
|
+
@property --pnz { syntax: "<number>"; inherits: false; initial-value: 1; }
|
|
374
|
+
@property --psr { syntax: "<number>"; inherits: false; initial-value: 1; }
|
|
375
|
+
@property --psg { syntax: "<number>"; inherits: false; initial-value: 1; }
|
|
376
|
+
@property --psb { syntax: "<number>"; inherits: false; initial-value: 1; }
|
|
377
|
+
|
|
378
|
+
/* Calc-driven Lambert + tint, scoped to dynamic-lighting scenes. Lives
|
|
379
|
+
here (not inline per polygon) so each leaf only carries its tiny normal
|
|
380
|
+
declarations \u2014 ~12\xD7 smaller per-polygon style payload on big meshes. */
|
|
381
|
+
.polycss-scene[data-polycss-lighting="dynamic"] s {
|
|
382
|
+
background-color: rgb(
|
|
383
|
+
calc(255 * (var(--par) * var(--pai)
|
|
384
|
+
+ var(--plr) * var(--pli) * max(0,
|
|
385
|
+
var(--pnx) * var(--plx) +
|
|
386
|
+
var(--pny) * var(--ply) +
|
|
387
|
+
var(--pnz) * var(--plz))))
|
|
388
|
+
calc(255 * (var(--pag) * var(--pai)
|
|
389
|
+
+ var(--plg) * var(--pli) * max(0,
|
|
390
|
+
var(--pnx) * var(--plx) +
|
|
391
|
+
var(--pny) * var(--ply) +
|
|
392
|
+
var(--pnz) * var(--plz))))
|
|
393
|
+
calc(255 * (var(--pab) * var(--pai)
|
|
394
|
+
+ var(--plb) * var(--pli) * max(0,
|
|
395
|
+
var(--pnx) * var(--plx) +
|
|
396
|
+
var(--pny) * var(--ply) +
|
|
397
|
+
var(--pnz) * var(--plz))))
|
|
398
|
+
);
|
|
399
|
+
background-blend-mode: multiply;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.polycss-scene[data-polycss-lighting="dynamic"] u {
|
|
403
|
+
border-bottom-color: rgb(
|
|
404
|
+
calc(255 * var(--psr) * (var(--par) * var(--pai)
|
|
405
|
+
+ var(--plr) * var(--pli) * max(0,
|
|
406
|
+
var(--pnx) * var(--plx) +
|
|
407
|
+
var(--pny) * var(--ply) +
|
|
408
|
+
var(--pnz) * var(--plz))))
|
|
409
|
+
calc(255 * var(--psg) * (var(--pag) * var(--pai)
|
|
410
|
+
+ var(--plg) * var(--pli) * max(0,
|
|
411
|
+
var(--pnx) * var(--plx) +
|
|
412
|
+
var(--pny) * var(--ply) +
|
|
413
|
+
var(--pnz) * var(--plz))))
|
|
414
|
+
calc(255 * var(--psb) * (var(--pab) * var(--pai)
|
|
415
|
+
+ var(--plb) * var(--pli) * max(0,
|
|
416
|
+
var(--pnx) * var(--plx) +
|
|
417
|
+
var(--pny) * var(--ply) +
|
|
418
|
+
var(--pnz) * var(--plz))))
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
`;
|
|
422
|
+
|
|
423
|
+
// src/scene/textureAtlas.tsx
|
|
424
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo5, useState } from "react";
|
|
425
|
+
import { parsePureColor } from "@layoutit/polycss-core";
|
|
426
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
427
|
+
var DEFAULT_TILE = 50;
|
|
428
|
+
var DEFAULT_LIGHT_DIR = [0.4, -0.7, 0.59];
|
|
429
|
+
var DEFAULT_LIGHT_COLOR = "#ffffff";
|
|
430
|
+
var DEFAULT_LIGHT_INTENSITY = 1;
|
|
431
|
+
var DEFAULT_AMBIENT_COLOR = "#ffffff";
|
|
432
|
+
var DEFAULT_AMBIENT_INTENSITY = 0.4;
|
|
433
|
+
var ATLAS_MAX_SIZE = 4096;
|
|
434
|
+
var ATLAS_PADDING = 1;
|
|
435
|
+
var MIN_ATLAS_SCALE = 0.1;
|
|
436
|
+
var MAX_ATLAS_SCALE = 1;
|
|
437
|
+
var AUTO_ATLAS_LOW_AREA = ATLAS_MAX_SIZE * ATLAS_MAX_SIZE;
|
|
438
|
+
var AUTO_ATLAS_MEDIUM_AREA = AUTO_ATLAS_LOW_AREA * 3;
|
|
439
|
+
var AUTO_ATLAS_MAX_BITMAP_SIDE = 2048;
|
|
440
|
+
var AUTO_ATLAS_MAX_DECODED_BYTES = 16 * 1024 * 1024;
|
|
441
|
+
var AUTO_ATLAS_SCALE_GUARD = 0.995;
|
|
442
|
+
var DEFAULT_MATRIX_DECIMALS = 3;
|
|
443
|
+
var DEFAULT_BORDER_SHAPE_DECIMALS = 2;
|
|
444
|
+
var BASIS_EPS = 1e-9;
|
|
445
|
+
var SOLID_TRIANGLE_BLEED = 0.45;
|
|
446
|
+
var TEXTURE_IMAGE_CACHE = /* @__PURE__ */ new Map();
|
|
447
|
+
var RECT_EPS = 1e-3;
|
|
448
|
+
var TEXTURE_TRIANGLE_BLEED = 0.75;
|
|
449
|
+
function loadTextureImage(url) {
|
|
450
|
+
let p = TEXTURE_IMAGE_CACHE.get(url);
|
|
451
|
+
if (!p) {
|
|
452
|
+
p = new Promise((resolve, reject) => {
|
|
453
|
+
const img = new Image();
|
|
454
|
+
img.decoding = "async";
|
|
455
|
+
img.onload = () => resolve(img);
|
|
456
|
+
img.onerror = () => reject(new Error(`texture load failed: ${url}`));
|
|
457
|
+
img.src = url;
|
|
458
|
+
});
|
|
459
|
+
TEXTURE_IMAGE_CACHE.set(url, p);
|
|
460
|
+
p.then(
|
|
461
|
+
() => {
|
|
462
|
+
if (TEXTURE_IMAGE_CACHE.get(url) === p) TEXTURE_IMAGE_CACHE.delete(url);
|
|
463
|
+
},
|
|
464
|
+
() => {
|
|
465
|
+
if (TEXTURE_IMAGE_CACHE.get(url) === p) TEXTURE_IMAGE_CACHE.delete(url);
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return p;
|
|
470
|
+
}
|
|
471
|
+
function normalizeAtlasScale(scale) {
|
|
472
|
+
const value = typeof scale === "string" ? Number(scale) : scale;
|
|
473
|
+
if (value === void 0 || !Number.isFinite(value)) return 1;
|
|
474
|
+
return Math.min(MAX_ATLAS_SCALE, Math.max(MIN_ATLAS_SCALE, value));
|
|
475
|
+
}
|
|
476
|
+
function roundDecimal(value, decimals) {
|
|
477
|
+
const next = value.toFixed(decimals).replace(/\.?0+$/, "");
|
|
478
|
+
return Object.is(Number(next), -0) ? "0" : next;
|
|
479
|
+
}
|
|
480
|
+
function formatMatrix3d(matrix, decimals = DEFAULT_MATRIX_DECIMALS) {
|
|
481
|
+
return `matrix3d(${matrix.split(",").map((value) => {
|
|
482
|
+
const parsed = Number(value.trim());
|
|
483
|
+
return Number.isFinite(parsed) ? roundDecimal(parsed, decimals) : value.trim();
|
|
484
|
+
}).join(",")})`;
|
|
485
|
+
}
|
|
486
|
+
function formatPercent(value, decimals = DEFAULT_BORDER_SHAPE_DECIMALS) {
|
|
487
|
+
return `${roundDecimal(value, decimals)}%`;
|
|
488
|
+
}
|
|
489
|
+
function atlasArea(pages) {
|
|
490
|
+
return pages.reduce((sum, page) => sum + page.width * page.height, 0);
|
|
491
|
+
}
|
|
492
|
+
function autoAtlasScaleCap(pages) {
|
|
493
|
+
const area = atlasArea(pages);
|
|
494
|
+
if (area <= 0) return 1;
|
|
495
|
+
const maxSide = Math.max(
|
|
496
|
+
1,
|
|
497
|
+
...pages.map((page) => Math.max(page.width, page.height))
|
|
498
|
+
);
|
|
499
|
+
const sideScale = AUTO_ATLAS_MAX_BITMAP_SIDE / maxSide;
|
|
500
|
+
const memoryScale = Math.sqrt(AUTO_ATLAS_MAX_DECODED_BYTES / (area * 4));
|
|
501
|
+
return normalizeAtlasScale(Math.min(sideScale, memoryScale));
|
|
502
|
+
}
|
|
503
|
+
function autoAtlasScale(pages) {
|
|
504
|
+
const area = atlasArea(pages);
|
|
505
|
+
let atlasScale = 0.5;
|
|
506
|
+
if (area <= AUTO_ATLAS_LOW_AREA) atlasScale = 1;
|
|
507
|
+
else if (area <= AUTO_ATLAS_MEDIUM_AREA) atlasScale = 0.75;
|
|
508
|
+
return normalizeAtlasScale(Math.min(atlasScale, autoAtlasScaleCap(pages)));
|
|
509
|
+
}
|
|
510
|
+
function atlasBitmapMaxSide(pages, atlasScale) {
|
|
511
|
+
return pages.reduce((max, page) => Math.max(
|
|
512
|
+
max,
|
|
513
|
+
Math.ceil(page.width * atlasScale),
|
|
514
|
+
Math.ceil(page.height * atlasScale)
|
|
515
|
+
), 0);
|
|
516
|
+
}
|
|
517
|
+
function atlasDecodedBytes(pages, atlasScale) {
|
|
518
|
+
return pages.reduce(
|
|
519
|
+
(sum, page) => sum + Math.ceil(page.width * atlasScale) * Math.ceil(page.height * atlasScale) * 4,
|
|
520
|
+
0
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
function autoAtlasBudgetFactor(pages, atlasScale) {
|
|
524
|
+
const maxSide = atlasBitmapMaxSide(pages, atlasScale);
|
|
525
|
+
const decodedBytes = atlasDecodedBytes(pages, atlasScale);
|
|
526
|
+
const sideFactor = maxSide > AUTO_ATLAS_MAX_BITMAP_SIDE ? AUTO_ATLAS_MAX_BITMAP_SIDE / maxSide : 1;
|
|
527
|
+
const memoryFactor = decodedBytes > AUTO_ATLAS_MAX_DECODED_BYTES ? Math.sqrt(AUTO_ATLAS_MAX_DECODED_BYTES / decodedBytes) : 1;
|
|
528
|
+
return Math.min(sideFactor, memoryFactor);
|
|
529
|
+
}
|
|
530
|
+
function packTextureAtlasPlansAuto(plans, fullScalePacked) {
|
|
531
|
+
let atlasScale = autoAtlasScale(fullScalePacked.pages);
|
|
532
|
+
let packed = atlasScale === 1 ? fullScalePacked : packTextureAtlasPlans(plans, atlasScale);
|
|
533
|
+
for (let i = 0; i < 4; i++) {
|
|
534
|
+
const factor = autoAtlasBudgetFactor(packed.pages, atlasScale);
|
|
535
|
+
if (factor >= 1) break;
|
|
536
|
+
const nextAtlasScale = normalizeAtlasScale(atlasScale * factor * AUTO_ATLAS_SCALE_GUARD);
|
|
537
|
+
if (nextAtlasScale >= atlasScale) break;
|
|
538
|
+
atlasScale = nextAtlasScale;
|
|
539
|
+
packed = packTextureAtlasPlans(plans, atlasScale);
|
|
540
|
+
}
|
|
541
|
+
return { packed, atlasScale };
|
|
542
|
+
}
|
|
543
|
+
function packTextureAtlasPlansWithScale(plans, atlasScaleInput) {
|
|
544
|
+
if (atlasScaleInput !== void 0 && atlasScaleInput !== "auto") {
|
|
545
|
+
const atlasScale = normalizeAtlasScale(atlasScaleInput);
|
|
546
|
+
return { packed: packTextureAtlasPlans(plans, atlasScale), atlasScale };
|
|
547
|
+
}
|
|
548
|
+
const fullScalePacked = packTextureAtlasPlans(plans, 1);
|
|
549
|
+
return packTextureAtlasPlansAuto(plans, fullScalePacked);
|
|
550
|
+
}
|
|
551
|
+
function atlasPadding(atlasScale) {
|
|
552
|
+
return Math.max(ATLAS_PADDING, Math.ceil(ATLAS_PADDING / atlasScale));
|
|
553
|
+
}
|
|
554
|
+
function setCssTransform(ctx, atlasScale, a = 1, b = 0, c = 0, d = 1, e = 0, f = 0) {
|
|
555
|
+
ctx.setTransform(
|
|
556
|
+
a * atlasScale,
|
|
557
|
+
b * atlasScale,
|
|
558
|
+
c * atlasScale,
|
|
559
|
+
d * atlasScale,
|
|
560
|
+
e * atlasScale,
|
|
561
|
+
f * atlasScale
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
function parseHex(hex) {
|
|
565
|
+
const parsed = parsePureColor(hex);
|
|
566
|
+
if (!parsed) return { r: 255, g: 255, b: 255 };
|
|
567
|
+
return { r: parsed.rgb[0], g: parsed.rgb[1], b: parsed.rgb[2] };
|
|
568
|
+
}
|
|
569
|
+
function parseAlpha(input) {
|
|
570
|
+
return parsePureColor(input)?.alpha ?? 1;
|
|
571
|
+
}
|
|
572
|
+
function isFullRectSolid(entry) {
|
|
573
|
+
if (entry.screenPts.length !== 8) return false;
|
|
574
|
+
const xs = [];
|
|
575
|
+
const ys = [];
|
|
576
|
+
const addUnique = (list, value) => {
|
|
577
|
+
for (const existing of list) {
|
|
578
|
+
if (Math.abs(existing - value) <= RECT_EPS) return;
|
|
579
|
+
}
|
|
580
|
+
list.push(value);
|
|
581
|
+
};
|
|
582
|
+
for (let i = 0; i < entry.screenPts.length; i += 2) {
|
|
583
|
+
addUnique(xs, entry.screenPts[i]);
|
|
584
|
+
addUnique(ys, entry.screenPts[i + 1]);
|
|
585
|
+
}
|
|
586
|
+
if (xs.length !== 2 || ys.length !== 2) return false;
|
|
587
|
+
xs.sort((a, b) => a - b);
|
|
588
|
+
ys.sort((a, b) => a - b);
|
|
589
|
+
if (Math.abs(xs[0]) > RECT_EPS || Math.abs(ys[0]) > RECT_EPS || xs[1] - xs[0] <= RECT_EPS || ys[1] - ys[0] <= RECT_EPS) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
for (let i = 0; i < entry.screenPts.length; i += 2) {
|
|
593
|
+
const x = entry.screenPts[i];
|
|
594
|
+
const y = entry.screenPts[i + 1];
|
|
595
|
+
const onX = Math.abs(x - xs[0]) <= RECT_EPS || Math.abs(x - xs[1]) <= RECT_EPS;
|
|
596
|
+
const onY = Math.abs(y - ys[0]) <= RECT_EPS || Math.abs(y - ys[1]) <= RECT_EPS;
|
|
597
|
+
if (!onX || !onY) return false;
|
|
598
|
+
}
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
function isSolidTrianglePlan(entry) {
|
|
602
|
+
return !entry.texture && entry.polygon.vertices.length === 3;
|
|
603
|
+
}
|
|
604
|
+
function borderShapeSupported() {
|
|
605
|
+
const supportsBorderShape = !!globalThis.CSS?.supports?.(
|
|
606
|
+
"border-shape",
|
|
607
|
+
"polygon(0 0, 100% 0, 0 100%) polygon(50% 50%, 50% 50%, 50% 50%)"
|
|
608
|
+
);
|
|
609
|
+
if (!supportsBorderShape) return false;
|
|
610
|
+
const media = globalThis.matchMedia;
|
|
611
|
+
if (typeof media !== "function") return true;
|
|
612
|
+
return media("(pointer: fine)").matches && media("(hover: hover)").matches;
|
|
613
|
+
}
|
|
614
|
+
function cssPolygonShapeForPlan(entry) {
|
|
615
|
+
const pts = [];
|
|
616
|
+
const width = entry.canvasW || 1;
|
|
617
|
+
const height = entry.canvasH || 1;
|
|
618
|
+
for (let i = 0; i < entry.screenPts.length; i += 2) {
|
|
619
|
+
const x = Math.max(0, Math.min(100, entry.screenPts[i] / width * 100));
|
|
620
|
+
const y = Math.max(0, Math.min(100, entry.screenPts[i + 1] / height * 100));
|
|
621
|
+
pts.push(`${formatPercent(x)} ${formatPercent(y)}`);
|
|
622
|
+
}
|
|
623
|
+
return `polygon(${pts.join(", ")})`;
|
|
624
|
+
}
|
|
625
|
+
function cssCollapsedInnerShapeForPlan(entry) {
|
|
626
|
+
let xSum = 0;
|
|
627
|
+
let ySum = 0;
|
|
628
|
+
const points = Math.max(1, entry.screenPts.length / 2);
|
|
629
|
+
for (let i = 0; i < entry.screenPts.length; i += 2) {
|
|
630
|
+
xSum += entry.screenPts[i];
|
|
631
|
+
ySum += entry.screenPts[i + 1];
|
|
632
|
+
}
|
|
633
|
+
const width = entry.canvasW || 1;
|
|
634
|
+
const height = entry.canvasH || 1;
|
|
635
|
+
const x = formatPercent(Math.max(0, Math.min(100, xSum / points / width * 100)));
|
|
636
|
+
const y = formatPercent(Math.max(0, Math.min(100, ySum / points / height * 100)));
|
|
637
|
+
return `polygon(${Array.from({ length: points }, () => `${x} ${y}`).join(", ")})`;
|
|
638
|
+
}
|
|
639
|
+
function cssBorderShapeForPlan(entry) {
|
|
640
|
+
return `${cssPolygonShapeForPlan(entry)} ${cssCollapsedInnerShapeForPlan(entry)}`;
|
|
641
|
+
}
|
|
642
|
+
function formatMatrix3dValues(values, decimals = DEFAULT_MATRIX_DECIMALS) {
|
|
643
|
+
return values.map((value) => roundDecimal(value, decimals)).join(",");
|
|
644
|
+
}
|
|
645
|
+
function cssPoints(vertices, tile, elev) {
|
|
646
|
+
return vertices.map((v) => [v[1] * tile, v[0] * tile, v[2] * elev]);
|
|
647
|
+
}
|
|
648
|
+
function computeSurfaceNormal(pts) {
|
|
649
|
+
if (pts.length < 3) return null;
|
|
650
|
+
const p0 = pts[0], p1 = pts[1], p2 = pts[2];
|
|
651
|
+
const e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
|
|
652
|
+
const e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
|
|
653
|
+
const normal = [
|
|
654
|
+
-(e1[1] * e2[2] - e1[2] * e2[1]),
|
|
655
|
+
-(e1[2] * e2[0] - e1[0] * e2[2]),
|
|
656
|
+
-(e1[0] * e2[1] - e1[1] * e2[0])
|
|
657
|
+
];
|
|
658
|
+
const len = Math.hypot(normal[0], normal[1], normal[2]);
|
|
659
|
+
if (len <= BASIS_EPS) return null;
|
|
660
|
+
return [normal[0] / len, normal[1] / len, normal[2] / len];
|
|
661
|
+
}
|
|
662
|
+
function dotVec(a, b) {
|
|
663
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
664
|
+
}
|
|
665
|
+
function crossVec(a, b) {
|
|
666
|
+
return [
|
|
667
|
+
a[1] * b[2] - a[2] * b[1],
|
|
668
|
+
a[2] * b[0] - a[0] * b[2],
|
|
669
|
+
a[0] * b[1] - a[1] * b[0]
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
function solidTriangleStyle(entry, textureLighting, pointerEvents) {
|
|
673
|
+
if (!isSolidTrianglePlan(entry)) return null;
|
|
674
|
+
const pts = cssPoints(entry.polygon.vertices, entry.tileSize, entry.layerElevation);
|
|
675
|
+
const normal = computeSurfaceNormal(pts);
|
|
676
|
+
if (!normal) return null;
|
|
677
|
+
const edges = [
|
|
678
|
+
{ a: 0, b: 1, c: 2 },
|
|
679
|
+
{ a: 1, b: 2, c: 0 },
|
|
680
|
+
{ a: 2, b: 0, c: 1 }
|
|
681
|
+
].map((edge) => {
|
|
682
|
+
const av2 = pts[edge.a];
|
|
683
|
+
const bv2 = pts[edge.b];
|
|
684
|
+
return {
|
|
685
|
+
...edge,
|
|
686
|
+
length: Math.hypot(bv2[0] - av2[0], bv2[1] - av2[1], bv2[2] - av2[2])
|
|
687
|
+
};
|
|
688
|
+
}).sort((a2, b2) => b2.length - a2.length);
|
|
689
|
+
let a = edges[0].a;
|
|
690
|
+
let b = edges[0].b;
|
|
691
|
+
const c = edges[0].c;
|
|
692
|
+
let av = pts[a];
|
|
693
|
+
let bv = pts[b];
|
|
694
|
+
const cv = pts[c];
|
|
695
|
+
let baseLength = edges[0].length;
|
|
696
|
+
if (baseLength <= BASIS_EPS) return null;
|
|
697
|
+
let xAxis = [
|
|
698
|
+
(bv[0] - av[0]) / baseLength,
|
|
699
|
+
(bv[1] - av[1]) / baseLength,
|
|
700
|
+
(bv[2] - av[2]) / baseLength
|
|
701
|
+
];
|
|
702
|
+
const ac = [cv[0] - av[0], cv[1] - av[1], cv[2] - av[2]];
|
|
703
|
+
let apexX = dotVec(ac, xAxis);
|
|
704
|
+
let foot = [
|
|
705
|
+
av[0] + xAxis[0] * apexX,
|
|
706
|
+
av[1] + xAxis[1] * apexX,
|
|
707
|
+
av[2] + xAxis[2] * apexX
|
|
708
|
+
];
|
|
709
|
+
let yAxisRaw = [
|
|
710
|
+
foot[0] - cv[0],
|
|
711
|
+
foot[1] - cv[1],
|
|
712
|
+
foot[2] - cv[2]
|
|
713
|
+
];
|
|
714
|
+
const height = Math.hypot(yAxisRaw[0], yAxisRaw[1], yAxisRaw[2]);
|
|
715
|
+
if (height <= BASIS_EPS) return null;
|
|
716
|
+
let yAxis = [
|
|
717
|
+
yAxisRaw[0] / height,
|
|
718
|
+
yAxisRaw[1] / height,
|
|
719
|
+
yAxisRaw[2] / height
|
|
720
|
+
];
|
|
721
|
+
if (dotVec(crossVec(xAxis, yAxis), normal) < 0) {
|
|
722
|
+
const nextA = b;
|
|
723
|
+
b = a;
|
|
724
|
+
a = nextA;
|
|
725
|
+
av = pts[a];
|
|
726
|
+
bv = pts[b];
|
|
727
|
+
baseLength = Math.hypot(bv[0] - av[0], bv[1] - av[1], bv[2] - av[2]);
|
|
728
|
+
if (baseLength <= BASIS_EPS) return null;
|
|
729
|
+
xAxis = [
|
|
730
|
+
(bv[0] - av[0]) / baseLength,
|
|
731
|
+
(bv[1] - av[1]) / baseLength,
|
|
732
|
+
(bv[2] - av[2]) / baseLength
|
|
733
|
+
];
|
|
734
|
+
const nextAc = [cv[0] - av[0], cv[1] - av[1], cv[2] - av[2]];
|
|
735
|
+
apexX = dotVec(nextAc, xAxis);
|
|
736
|
+
foot = [
|
|
737
|
+
av[0] + xAxis[0] * apexX,
|
|
738
|
+
av[1] + xAxis[1] * apexX,
|
|
739
|
+
av[2] + xAxis[2] * apexX
|
|
740
|
+
];
|
|
741
|
+
yAxisRaw = [
|
|
742
|
+
foot[0] - cv[0],
|
|
743
|
+
foot[1] - cv[1],
|
|
744
|
+
foot[2] - cv[2]
|
|
745
|
+
];
|
|
746
|
+
const nextHeight = Math.hypot(yAxisRaw[0], yAxisRaw[1], yAxisRaw[2]);
|
|
747
|
+
if (nextHeight <= BASIS_EPS) return null;
|
|
748
|
+
yAxis = [
|
|
749
|
+
yAxisRaw[0] / nextHeight,
|
|
750
|
+
yAxisRaw[1] / nextHeight,
|
|
751
|
+
yAxisRaw[2] / nextHeight
|
|
752
|
+
];
|
|
753
|
+
}
|
|
754
|
+
const left = Math.max(0, Math.min(baseLength, apexX));
|
|
755
|
+
const right = Math.max(0, baseLength - left);
|
|
756
|
+
const bleed = SOLID_TRIANGLE_BLEED;
|
|
757
|
+
const leftPx = left + bleed;
|
|
758
|
+
const rightPx = right + bleed;
|
|
759
|
+
const heightPx = height + bleed * 2;
|
|
760
|
+
const tx = cv[0] - leftPx * xAxis[0] - bleed * yAxis[0];
|
|
761
|
+
const ty = cv[1] - leftPx * xAxis[1] - bleed * yAxis[1];
|
|
762
|
+
const tz = cv[2] - leftPx * xAxis[2] - bleed * yAxis[2];
|
|
763
|
+
const matrix = formatMatrix3dValues([
|
|
764
|
+
xAxis[0],
|
|
765
|
+
xAxis[1],
|
|
766
|
+
xAxis[2],
|
|
767
|
+
0,
|
|
768
|
+
yAxis[0],
|
|
769
|
+
yAxis[1],
|
|
770
|
+
yAxis[2],
|
|
771
|
+
0,
|
|
772
|
+
normal[0],
|
|
773
|
+
normal[1],
|
|
774
|
+
normal[2],
|
|
775
|
+
0,
|
|
776
|
+
tx,
|
|
777
|
+
ty,
|
|
778
|
+
tz,
|
|
779
|
+
1
|
|
780
|
+
]);
|
|
781
|
+
const dynamic = textureLighting === "dynamic";
|
|
782
|
+
const base = parseHex(entry.polygon.color ?? "#cccccc");
|
|
783
|
+
return {
|
|
784
|
+
transform: `matrix3d(${matrix})`,
|
|
785
|
+
borderWidth: `0 ${rightPx}px ${heightPx}px ${leftPx}px`,
|
|
786
|
+
borderBottomColor: dynamic ? void 0 : entry.shadedColor,
|
|
787
|
+
pointerEvents: pointerEvents === "none" ? "none" : void 0,
|
|
788
|
+
...dynamic ? {
|
|
789
|
+
["--pnx"]: normal[0].toFixed(4),
|
|
790
|
+
["--pny"]: normal[1].toFixed(4),
|
|
791
|
+
["--pnz"]: normal[2].toFixed(4),
|
|
792
|
+
["--psr"]: (base.r / 255).toFixed(4),
|
|
793
|
+
["--psg"]: (base.g / 255).toFixed(4),
|
|
794
|
+
["--psb"]: (base.b / 255).toFixed(4)
|
|
795
|
+
} : null
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function rgbToHex({ r, g, b }) {
|
|
799
|
+
const f = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
|
|
800
|
+
return `#${f(r)}${f(g)}${f(b)}`;
|
|
801
|
+
}
|
|
802
|
+
function shadePolygon(baseColor, directScale, lightColor, ambientColor, ambientIntensity) {
|
|
803
|
+
const base = parseHex(baseColor);
|
|
804
|
+
const light = parseHex(lightColor);
|
|
805
|
+
const amb = parseHex(ambientColor);
|
|
806
|
+
const tintR = amb.r / 255 * ambientIntensity + light.r / 255 * directScale;
|
|
807
|
+
const tintG = amb.g / 255 * ambientIntensity + light.g / 255 * directScale;
|
|
808
|
+
const tintB = amb.b / 255 * ambientIntensity + light.b / 255 * directScale;
|
|
809
|
+
const r = Math.max(0, Math.min(255, Math.round(base.r * tintR)));
|
|
810
|
+
const g = Math.max(0, Math.min(255, Math.round(base.g * tintG)));
|
|
811
|
+
const b = Math.max(0, Math.min(255, Math.round(base.b * tintB)));
|
|
812
|
+
const alpha = parseAlpha(baseColor);
|
|
813
|
+
return alpha < 1 ? `rgba(${r}, ${g}, ${b}, ${alpha})` : rgbToHex({ r, g, b });
|
|
814
|
+
}
|
|
815
|
+
function textureTintFactors(directScale, lightColor, ambientColor, ambientIntensity) {
|
|
816
|
+
const light = parseHex(lightColor);
|
|
817
|
+
const amb = parseHex(ambientColor);
|
|
818
|
+
return {
|
|
819
|
+
r: amb.r / 255 * ambientIntensity + light.r / 255 * directScale,
|
|
820
|
+
g: amb.g / 255 * ambientIntensity + light.g / 255 * directScale,
|
|
821
|
+
b: amb.b / 255 * ambientIntensity + light.b / 255 * directScale
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function tintToCss({ r, g, b }) {
|
|
825
|
+
const f = (n) => Math.round(Math.max(0, Math.min(1, n)) * 255);
|
|
826
|
+
return `rgb(${f(r)} ${f(g)} ${f(b)})`;
|
|
827
|
+
}
|
|
828
|
+
function applyTextureTint(ctx, x, y, width, height, tint, atlasScale) {
|
|
829
|
+
if (Math.abs(tint.r - 1) < 1e-3 && Math.abs(tint.g - 1) < 1e-3 && Math.abs(tint.b - 1) < 1e-3) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
ctx.save();
|
|
833
|
+
setCssTransform(ctx, atlasScale);
|
|
834
|
+
ctx.globalCompositeOperation = "multiply";
|
|
835
|
+
ctx.fillStyle = tintToCss(tint);
|
|
836
|
+
ctx.fillRect(x, y, width, height);
|
|
837
|
+
ctx.restore();
|
|
838
|
+
}
|
|
839
|
+
function drawImageCover(ctx, img, x, y, width, height, atlasScale) {
|
|
840
|
+
const srcW = img.naturalWidth || img.width || 1;
|
|
841
|
+
const srcH = img.naturalHeight || img.height || 1;
|
|
842
|
+
const scale = Math.max(width / srcW, height / srcH);
|
|
843
|
+
const drawW = srcW * scale;
|
|
844
|
+
const drawH = srcH * scale;
|
|
845
|
+
setCssTransform(ctx, atlasScale);
|
|
846
|
+
ctx.drawImage(img, x + (width - drawW) / 2, y + (height - drawH) / 2, drawW, drawH);
|
|
847
|
+
}
|
|
848
|
+
function computeUvAffine(points, uvs) {
|
|
849
|
+
if (points.length < 3 || uvs.length < 3) return null;
|
|
850
|
+
const [p0, p1, p2] = points;
|
|
851
|
+
const [uv0, uv1, uv2] = uvs;
|
|
852
|
+
const sx0 = p0[0], sy0 = p0[1];
|
|
853
|
+
const sx1 = p1[0], sy1 = p1[1];
|
|
854
|
+
const sx2 = p2[0], sy2 = p2[1];
|
|
855
|
+
const u0 = uv0[0], V0 = 1 - uv0[1];
|
|
856
|
+
const u1 = uv1[0], V1 = 1 - uv1[1];
|
|
857
|
+
const u2 = uv2[0], V2 = 1 - uv2[1];
|
|
858
|
+
const du1 = u1 - u0, dV1 = V1 - V0;
|
|
859
|
+
const du2 = u2 - u0, dV2 = V2 - V0;
|
|
860
|
+
const det = du1 * dV2 - du2 * dV1;
|
|
861
|
+
if (Math.abs(det) <= 1e-9) return null;
|
|
862
|
+
const dx1 = sx1 - sx0, dx2 = sx2 - sx0;
|
|
863
|
+
const dy1 = sy1 - sy0, dy2 = sy2 - sy0;
|
|
864
|
+
const affine = {
|
|
865
|
+
a: (dx1 * dV2 - dx2 * dV1) / det,
|
|
866
|
+
b: (du1 * dx2 - du2 * dx1) / det,
|
|
867
|
+
c: (dy1 * dV2 - dy2 * dV1) / det,
|
|
868
|
+
d: (du1 * dy2 - du2 * dy1) / det,
|
|
869
|
+
e: 0,
|
|
870
|
+
f: 0
|
|
871
|
+
};
|
|
872
|
+
affine.e = sx0 - affine.a * u0 - affine.b * V0;
|
|
873
|
+
affine.f = sy0 - affine.c * u0 - affine.d * V0;
|
|
874
|
+
return affine;
|
|
875
|
+
}
|
|
876
|
+
function computeUvSampleRect(uvs) {
|
|
877
|
+
if (uvs.length === 0) return null;
|
|
878
|
+
let minU = Infinity;
|
|
879
|
+
let minV = Infinity;
|
|
880
|
+
let maxU = -Infinity;
|
|
881
|
+
let maxV = -Infinity;
|
|
882
|
+
for (const uv of uvs) {
|
|
883
|
+
const u = uv[0];
|
|
884
|
+
const v = 1 - uv[1];
|
|
885
|
+
if (!Number.isFinite(u) || !Number.isFinite(v)) return null;
|
|
886
|
+
minU = Math.min(minU, u);
|
|
887
|
+
maxU = Math.max(maxU, u);
|
|
888
|
+
minV = Math.min(minV, v);
|
|
889
|
+
maxV = Math.max(maxV, v);
|
|
890
|
+
}
|
|
891
|
+
return { minU, minV, maxU, maxV };
|
|
892
|
+
}
|
|
893
|
+
function projectTextureTriangle(triangle, tile, elev, origin, xAxis, yAxis, shiftX, shiftY) {
|
|
894
|
+
const points = triangle.vertices.map((vertex) => {
|
|
895
|
+
const point = [
|
|
896
|
+
vertex[1] * tile,
|
|
897
|
+
vertex[0] * tile,
|
|
898
|
+
vertex[2] * elev
|
|
899
|
+
];
|
|
900
|
+
const dx = point[0] - origin[0];
|
|
901
|
+
const dy = point[1] - origin[1];
|
|
902
|
+
const dz = point[2] - origin[2];
|
|
903
|
+
return [
|
|
904
|
+
dx * xAxis[0] + dy * xAxis[1] + dz * xAxis[2] + shiftX,
|
|
905
|
+
dx * yAxis[0] + dy * yAxis[1] + dz * yAxis[2] + shiftY
|
|
906
|
+
];
|
|
907
|
+
});
|
|
908
|
+
const uvAffine = computeUvAffine(points, triangle.uvs);
|
|
909
|
+
const uvSampleRect = computeUvSampleRect(triangle.uvs);
|
|
910
|
+
if (!uvAffine && !uvSampleRect) return null;
|
|
911
|
+
return {
|
|
912
|
+
screenPts: points.flatMap(([x, y]) => [x, y]),
|
|
913
|
+
uvAffine,
|
|
914
|
+
uvSampleRect
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function expandClipPoints(points, amount) {
|
|
918
|
+
if (points.length < 6 || amount <= 0) return points;
|
|
919
|
+
let cx = 0;
|
|
920
|
+
let cy = 0;
|
|
921
|
+
const count = points.length / 2;
|
|
922
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
923
|
+
cx += points[i];
|
|
924
|
+
cy += points[i + 1];
|
|
925
|
+
}
|
|
926
|
+
cx /= count;
|
|
927
|
+
cy /= count;
|
|
928
|
+
const expanded = points.slice();
|
|
929
|
+
for (let i = 0; i < expanded.length; i += 2) {
|
|
930
|
+
const dx = expanded[i] - cx;
|
|
931
|
+
const dy = expanded[i + 1] - cy;
|
|
932
|
+
const len = Math.hypot(dx, dy);
|
|
933
|
+
if (len <= RECT_EPS) continue;
|
|
934
|
+
expanded[i] += dx / len * amount;
|
|
935
|
+
expanded[i + 1] += dy / len * amount;
|
|
936
|
+
}
|
|
937
|
+
return expanded;
|
|
938
|
+
}
|
|
939
|
+
function canvasToUrl(canvas) {
|
|
940
|
+
if (typeof canvas.toBlob === "function") {
|
|
941
|
+
return new Promise((resolve) => {
|
|
942
|
+
canvas.toBlob((blob) => {
|
|
943
|
+
resolve(blob ? URL.createObjectURL(blob) : null);
|
|
944
|
+
}, "image/png");
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
return Promise.resolve(canvas.toDataURL("image/png"));
|
|
949
|
+
} catch {
|
|
950
|
+
return Promise.resolve(null);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function clampSourceCoord(value, max) {
|
|
954
|
+
return Math.max(0, Math.min(max, value));
|
|
955
|
+
}
|
|
956
|
+
function drawImageUvSample(ctx, img, rect, x, y, width, height, atlasScale) {
|
|
957
|
+
const imgW = img.naturalWidth || img.width || 1;
|
|
958
|
+
const imgH = img.naturalHeight || img.height || 1;
|
|
959
|
+
const rawX0 = clampSourceCoord(Math.min(rect.minU, rect.maxU) * imgW, imgW);
|
|
960
|
+
const rawX1 = clampSourceCoord(Math.max(rect.minU, rect.maxU) * imgW, imgW);
|
|
961
|
+
const rawY0 = clampSourceCoord(Math.min(rect.minV, rect.maxV) * imgH, imgH);
|
|
962
|
+
const rawY1 = clampSourceCoord(Math.max(rect.minV, rect.maxV) * imgH, imgH);
|
|
963
|
+
let sx = Math.floor(rawX0);
|
|
964
|
+
let sy = Math.floor(rawY0);
|
|
965
|
+
let sw = Math.ceil(rawX1) - sx;
|
|
966
|
+
let sh = Math.ceil(rawY1) - sy;
|
|
967
|
+
if (sw < 1) {
|
|
968
|
+
sx = Math.floor(clampSourceCoord((rect.minU + rect.maxU) / 2 * imgW, imgW - 1));
|
|
969
|
+
sw = 1;
|
|
970
|
+
}
|
|
971
|
+
if (sh < 1) {
|
|
972
|
+
sy = Math.floor(clampSourceCoord((rect.minV + rect.maxV) / 2 * imgH, imgH - 1));
|
|
973
|
+
sh = 1;
|
|
974
|
+
}
|
|
975
|
+
sx = Math.max(0, Math.min(imgW - 1, sx));
|
|
976
|
+
sy = Math.max(0, Math.min(imgH - 1, sy));
|
|
977
|
+
sw = Math.max(1, Math.min(imgW - sx, sw));
|
|
978
|
+
sh = Math.max(1, Math.min(imgH - sy, sh));
|
|
979
|
+
setCssTransform(ctx, atlasScale);
|
|
980
|
+
ctx.drawImage(img, sx, sy, sw, sh, x, y, width, height);
|
|
981
|
+
}
|
|
982
|
+
function computeTextureAtlasPlan(polygon, index, options = {}) {
|
|
983
|
+
const { vertices, texture, uvs } = polygon;
|
|
984
|
+
if (!vertices || vertices.length < 3) return null;
|
|
985
|
+
const tile = options.tileSize ?? DEFAULT_TILE;
|
|
986
|
+
const elev = options.layerElevation ?? tile;
|
|
987
|
+
const toCss = (v) => [
|
|
988
|
+
v[1] * tile,
|
|
989
|
+
v[0] * tile,
|
|
990
|
+
v[2] * elev
|
|
991
|
+
];
|
|
992
|
+
const pts = vertices.map(toCss);
|
|
993
|
+
const p0 = pts[0];
|
|
994
|
+
const p1 = pts[1];
|
|
995
|
+
const p2 = pts[2];
|
|
996
|
+
const e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
|
|
997
|
+
const e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
|
|
998
|
+
const l01 = Math.hypot(e1[0], e1[1], e1[2]);
|
|
999
|
+
if (l01 === 0) return null;
|
|
1000
|
+
const xAxis = [e1[0] / l01, e1[1] / l01, e1[2] / l01];
|
|
1001
|
+
let nx = -(e1[1] * e2[2] - e1[2] * e2[1]);
|
|
1002
|
+
let ny = -(e1[2] * e2[0] - e1[0] * e2[2]);
|
|
1003
|
+
let nz = -(e1[0] * e2[1] - e1[1] * e2[0]);
|
|
1004
|
+
const nLen = Math.hypot(nx, ny, nz);
|
|
1005
|
+
if (nLen === 0) return null;
|
|
1006
|
+
nx /= nLen;
|
|
1007
|
+
ny /= nLen;
|
|
1008
|
+
nz /= nLen;
|
|
1009
|
+
const yAxis = [
|
|
1010
|
+
ny * xAxis[2] - nz * xAxis[1],
|
|
1011
|
+
nz * xAxis[0] - nx * xAxis[2],
|
|
1012
|
+
nx * xAxis[1] - ny * xAxis[0]
|
|
1013
|
+
];
|
|
1014
|
+
const local2D = pts.map((p) => {
|
|
1015
|
+
const dx = p[0] - p0[0], dy = p[1] - p0[1], dz = p[2] - p0[2];
|
|
1016
|
+
return [
|
|
1017
|
+
dx * xAxis[0] + dy * xAxis[1] + dz * xAxis[2],
|
|
1018
|
+
dx * yAxis[0] + dy * yAxis[1] + dz * yAxis[2]
|
|
1019
|
+
];
|
|
1020
|
+
});
|
|
1021
|
+
let xMin = Infinity, yMin = Infinity, xMax = -Infinity, yMax = -Infinity;
|
|
1022
|
+
for (const [x, y] of local2D) {
|
|
1023
|
+
if (x < xMin) xMin = x;
|
|
1024
|
+
if (x > xMax) xMax = x;
|
|
1025
|
+
if (y < yMin) yMin = y;
|
|
1026
|
+
if (y > yMax) yMax = y;
|
|
1027
|
+
}
|
|
1028
|
+
const shiftX = -xMin;
|
|
1029
|
+
const shiftY = -yMin;
|
|
1030
|
+
const w = xMax - xMin;
|
|
1031
|
+
const h = yMax - yMin;
|
|
1032
|
+
if (!Number.isFinite(w) || !Number.isFinite(h)) return null;
|
|
1033
|
+
const screenPts = [];
|
|
1034
|
+
for (let i = 0; i < local2D.length; i++) {
|
|
1035
|
+
const [x, y] = local2D[i];
|
|
1036
|
+
const sx = x + shiftX;
|
|
1037
|
+
const sy = y + shiftY;
|
|
1038
|
+
screenPts.push(sx, sy);
|
|
1039
|
+
}
|
|
1040
|
+
const tx = p0[0] - shiftX * xAxis[0] - shiftY * yAxis[0];
|
|
1041
|
+
const ty = p0[1] - shiftX * xAxis[1] - shiftY * yAxis[1];
|
|
1042
|
+
const tz = p0[2] - shiftX * xAxis[2] - shiftY * yAxis[2];
|
|
1043
|
+
const matrix = [
|
|
1044
|
+
xAxis[0],
|
|
1045
|
+
xAxis[1],
|
|
1046
|
+
xAxis[2],
|
|
1047
|
+
0,
|
|
1048
|
+
yAxis[0],
|
|
1049
|
+
yAxis[1],
|
|
1050
|
+
yAxis[2],
|
|
1051
|
+
0,
|
|
1052
|
+
nx,
|
|
1053
|
+
ny,
|
|
1054
|
+
nz,
|
|
1055
|
+
0,
|
|
1056
|
+
tx,
|
|
1057
|
+
ty,
|
|
1058
|
+
tz,
|
|
1059
|
+
1
|
|
1060
|
+
].join(",");
|
|
1061
|
+
const directionalCfg = options.directionalLight;
|
|
1062
|
+
const ambientCfg = options.ambientLight;
|
|
1063
|
+
const lightDir = directionalCfg?.direction ?? DEFAULT_LIGHT_DIR;
|
|
1064
|
+
const lightColor = directionalCfg?.color ?? DEFAULT_LIGHT_COLOR;
|
|
1065
|
+
const lightIntensity = Math.max(0, directionalCfg?.intensity ?? DEFAULT_LIGHT_INTENSITY);
|
|
1066
|
+
const ambientColor = ambientCfg?.color ?? DEFAULT_AMBIENT_COLOR;
|
|
1067
|
+
const ambientIntensity = Math.max(0, ambientCfg?.intensity ?? DEFAULT_AMBIENT_INTENSITY);
|
|
1068
|
+
const lLen = Math.hypot(lightDir[0], lightDir[1], lightDir[2]) || 1;
|
|
1069
|
+
const lx = lightDir[0] / lLen, ly = lightDir[1] / lLen, lz = lightDir[2] / lLen;
|
|
1070
|
+
const directScale = lightIntensity * Math.max(0, nx * lx + ny * ly + nz * lz);
|
|
1071
|
+
const textureTint = textureTintFactors(directScale, lightColor, ambientColor, ambientIntensity);
|
|
1072
|
+
const shadedColor = shadePolygon(polygon.color ?? "#cccccc", directScale, lightColor, ambientColor, ambientIntensity);
|
|
1073
|
+
let uvAffine = null;
|
|
1074
|
+
let uvSampleRect = null;
|
|
1075
|
+
if (texture && uvs && uvs.length >= 3 && uvs.length === vertices.length) {
|
|
1076
|
+
uvSampleRect = computeUvSampleRect(uvs);
|
|
1077
|
+
uvAffine = computeUvAffine(
|
|
1078
|
+
local2D.map(([x, y]) => [x + shiftX, y + shiftY]),
|
|
1079
|
+
uvs
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
const textureTriangles = texture && polygon.textureTriangles?.length ? polygon.textureTriangles.map(
|
|
1083
|
+
(triangle) => projectTextureTriangle(triangle, tile, elev, p0, xAxis, yAxis, shiftX, shiftY)
|
|
1084
|
+
).filter((triangle) => !!triangle) : null;
|
|
1085
|
+
return {
|
|
1086
|
+
index,
|
|
1087
|
+
polygon,
|
|
1088
|
+
texture,
|
|
1089
|
+
tileSize: tile,
|
|
1090
|
+
layerElevation: elev,
|
|
1091
|
+
matrix,
|
|
1092
|
+
canvasW: Math.max(1, Math.ceil(w)),
|
|
1093
|
+
canvasH: Math.max(1, Math.ceil(h)),
|
|
1094
|
+
screenPts,
|
|
1095
|
+
uvAffine,
|
|
1096
|
+
uvSampleRect,
|
|
1097
|
+
textureTriangles,
|
|
1098
|
+
normal: [nx, ny, nz],
|
|
1099
|
+
textureTint,
|
|
1100
|
+
shadedColor
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function packTextureAtlasPlans(plans, atlasScale = 1) {
|
|
1104
|
+
const entries = Array(plans.length).fill(null);
|
|
1105
|
+
const pages = [];
|
|
1106
|
+
const padding = atlasPadding(atlasScale);
|
|
1107
|
+
const sortedPlans = plans.filter((plan) => !!plan).sort(
|
|
1108
|
+
(a, b) => b.canvasH - a.canvasH || b.canvasW - a.canvasW || a.index - b.index
|
|
1109
|
+
);
|
|
1110
|
+
const createPage = () => ({
|
|
1111
|
+
width: padding,
|
|
1112
|
+
height: padding,
|
|
1113
|
+
entries: [],
|
|
1114
|
+
shelves: []
|
|
1115
|
+
});
|
|
1116
|
+
const placeOnPage = (page, plan, pageIndex) => {
|
|
1117
|
+
if (page.sealed) return null;
|
|
1118
|
+
for (const shelf of page.shelves) {
|
|
1119
|
+
if (plan.canvasH <= shelf.height && shelf.x + plan.canvasW + padding <= ATLAS_MAX_SIZE) {
|
|
1120
|
+
const entry2 = { ...plan, pageIndex, x: shelf.x, y: shelf.y };
|
|
1121
|
+
shelf.x += plan.canvasW + padding;
|
|
1122
|
+
page.entries.push(entry2);
|
|
1123
|
+
page.width = Math.max(page.width, entry2.x + plan.canvasW + padding);
|
|
1124
|
+
return entry2;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const shelfY = page.shelves.length === 0 ? padding : page.height;
|
|
1128
|
+
if (shelfY + plan.canvasH + padding > ATLAS_MAX_SIZE) return null;
|
|
1129
|
+
const entry = { ...plan, pageIndex, x: padding, y: shelfY };
|
|
1130
|
+
page.shelves.push({
|
|
1131
|
+
x: padding + plan.canvasW + padding,
|
|
1132
|
+
y: shelfY,
|
|
1133
|
+
height: plan.canvasH
|
|
1134
|
+
});
|
|
1135
|
+
page.entries.push(entry);
|
|
1136
|
+
page.width = Math.max(page.width, entry.x + plan.canvasW + padding);
|
|
1137
|
+
page.height = Math.max(page.height, shelfY + plan.canvasH + padding);
|
|
1138
|
+
return entry;
|
|
1139
|
+
};
|
|
1140
|
+
for (const plan of sortedPlans) {
|
|
1141
|
+
const tooLarge = plan.canvasW + padding * 2 > ATLAS_MAX_SIZE || plan.canvasH + padding * 2 > ATLAS_MAX_SIZE;
|
|
1142
|
+
if (tooLarge) {
|
|
1143
|
+
const pageIndex = pages.length;
|
|
1144
|
+
const entry = {
|
|
1145
|
+
...plan,
|
|
1146
|
+
pageIndex,
|
|
1147
|
+
x: padding,
|
|
1148
|
+
y: padding
|
|
1149
|
+
};
|
|
1150
|
+
entries[plan.index] = entry;
|
|
1151
|
+
pages.push({
|
|
1152
|
+
width: plan.canvasW + padding * 2,
|
|
1153
|
+
height: plan.canvasH + padding * 2,
|
|
1154
|
+
entries: [entry],
|
|
1155
|
+
shelves: [],
|
|
1156
|
+
sealed: true
|
|
1157
|
+
});
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
let placed = null;
|
|
1161
|
+
for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
|
|
1162
|
+
placed = placeOnPage(pages[pageIndex], plan, pageIndex);
|
|
1163
|
+
if (placed) break;
|
|
1164
|
+
}
|
|
1165
|
+
if (!placed) {
|
|
1166
|
+
const page = createPage();
|
|
1167
|
+
const pageIndex = pages.length;
|
|
1168
|
+
pages.push(page);
|
|
1169
|
+
placed = placeOnPage(page, plan, pageIndex);
|
|
1170
|
+
}
|
|
1171
|
+
if (placed) entries[plan.index] = placed;
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
entries,
|
|
1175
|
+
pages: pages.map(({ width, height, entries: entries2 }) => ({ width, height, entries: entries2 }))
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
async function buildAtlasPage(page, textureLighting, doc, atlasScale) {
|
|
1179
|
+
const canvas = doc.createElement("canvas");
|
|
1180
|
+
canvas.width = Math.max(1, Math.ceil(page.width * atlasScale));
|
|
1181
|
+
canvas.height = Math.max(1, Math.ceil(page.height * atlasScale));
|
|
1182
|
+
const ctx = canvas.getContext("2d");
|
|
1183
|
+
if (!ctx) return { width: page.width, height: page.height, url: null };
|
|
1184
|
+
const uniqueTextures = Array.from(new Set(
|
|
1185
|
+
page.entries.flatMap((entry) => entry.texture ? [entry.texture] : [])
|
|
1186
|
+
));
|
|
1187
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
1188
|
+
await Promise.all(uniqueTextures.map(async (url2) => {
|
|
1189
|
+
loaded.set(url2, await loadTextureImage(url2));
|
|
1190
|
+
}));
|
|
1191
|
+
for (const entry of page.entries) {
|
|
1192
|
+
const srcImg = entry.texture ? loaded.get(entry.texture) : null;
|
|
1193
|
+
ctx.save();
|
|
1194
|
+
setCssTransform(ctx, atlasScale);
|
|
1195
|
+
ctx.beginPath();
|
|
1196
|
+
for (let i = 0; i < entry.screenPts.length; i += 2) {
|
|
1197
|
+
const px = entry.x + entry.screenPts[i];
|
|
1198
|
+
const py = entry.y + entry.screenPts[i + 1];
|
|
1199
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
1200
|
+
else ctx.lineTo(px, py);
|
|
1201
|
+
}
|
|
1202
|
+
ctx.closePath();
|
|
1203
|
+
ctx.clip();
|
|
1204
|
+
if (!entry.texture) {
|
|
1205
|
+
setCssTransform(ctx, atlasScale);
|
|
1206
|
+
ctx.fillStyle = textureLighting === "dynamic" ? entry.polygon.color ?? "#cccccc" : entry.shadedColor;
|
|
1207
|
+
ctx.fillRect(entry.x, entry.y, entry.canvasW, entry.canvasH);
|
|
1208
|
+
} else {
|
|
1209
|
+
if (srcImg && entry.textureTriangles?.length) {
|
|
1210
|
+
const imgW = srcImg.naturalWidth || srcImg.width || 1;
|
|
1211
|
+
const imgH = srcImg.naturalHeight || srcImg.height || 1;
|
|
1212
|
+
for (const triangle of entry.textureTriangles) {
|
|
1213
|
+
const clipPts = expandClipPoints(triangle.screenPts, TEXTURE_TRIANGLE_BLEED);
|
|
1214
|
+
ctx.save();
|
|
1215
|
+
setCssTransform(ctx, atlasScale);
|
|
1216
|
+
ctx.beginPath();
|
|
1217
|
+
for (let i = 0; i < clipPts.length; i += 2) {
|
|
1218
|
+
const px = entry.x + clipPts[i];
|
|
1219
|
+
const py = entry.y + clipPts[i + 1];
|
|
1220
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
1221
|
+
else ctx.lineTo(px, py);
|
|
1222
|
+
}
|
|
1223
|
+
ctx.closePath();
|
|
1224
|
+
ctx.clip();
|
|
1225
|
+
if (triangle.uvAffine) {
|
|
1226
|
+
setCssTransform(
|
|
1227
|
+
ctx,
|
|
1228
|
+
atlasScale,
|
|
1229
|
+
triangle.uvAffine.a / imgW,
|
|
1230
|
+
triangle.uvAffine.c / imgW,
|
|
1231
|
+
triangle.uvAffine.b / imgH,
|
|
1232
|
+
triangle.uvAffine.d / imgH,
|
|
1233
|
+
entry.x + triangle.uvAffine.e,
|
|
1234
|
+
entry.y + triangle.uvAffine.f
|
|
1235
|
+
);
|
|
1236
|
+
ctx.drawImage(srcImg, 0, 0);
|
|
1237
|
+
} else if (triangle.uvSampleRect) {
|
|
1238
|
+
drawImageUvSample(
|
|
1239
|
+
ctx,
|
|
1240
|
+
srcImg,
|
|
1241
|
+
triangle.uvSampleRect,
|
|
1242
|
+
entry.x,
|
|
1243
|
+
entry.y,
|
|
1244
|
+
entry.canvasW,
|
|
1245
|
+
entry.canvasH,
|
|
1246
|
+
atlasScale
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
ctx.restore();
|
|
1250
|
+
}
|
|
1251
|
+
} else if (srcImg && entry.uvAffine) {
|
|
1252
|
+
const imgW = srcImg.naturalWidth || srcImg.width || 1;
|
|
1253
|
+
const imgH = srcImg.naturalHeight || srcImg.height || 1;
|
|
1254
|
+
setCssTransform(
|
|
1255
|
+
ctx,
|
|
1256
|
+
atlasScale,
|
|
1257
|
+
entry.uvAffine.a / imgW,
|
|
1258
|
+
entry.uvAffine.c / imgW,
|
|
1259
|
+
entry.uvAffine.b / imgH,
|
|
1260
|
+
entry.uvAffine.d / imgH,
|
|
1261
|
+
entry.x + entry.uvAffine.e,
|
|
1262
|
+
entry.y + entry.uvAffine.f
|
|
1263
|
+
);
|
|
1264
|
+
ctx.drawImage(srcImg, 0, 0);
|
|
1265
|
+
} else if (srcImg && entry.uvSampleRect) {
|
|
1266
|
+
drawImageUvSample(
|
|
1267
|
+
ctx,
|
|
1268
|
+
srcImg,
|
|
1269
|
+
entry.uvSampleRect,
|
|
1270
|
+
entry.x,
|
|
1271
|
+
entry.y,
|
|
1272
|
+
entry.canvasW,
|
|
1273
|
+
entry.canvasH,
|
|
1274
|
+
atlasScale
|
|
1275
|
+
);
|
|
1276
|
+
} else if (srcImg) {
|
|
1277
|
+
drawImageCover(ctx, srcImg, entry.x, entry.y, entry.canvasW, entry.canvasH, atlasScale);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (entry.texture && textureLighting === "baked") {
|
|
1281
|
+
applyTextureTint(ctx, entry.x, entry.y, entry.canvasW, entry.canvasH, entry.textureTint, atlasScale);
|
|
1282
|
+
}
|
|
1283
|
+
ctx.restore();
|
|
1284
|
+
}
|
|
1285
|
+
const url = await canvasToUrl(canvas);
|
|
1286
|
+
canvas.width = 1;
|
|
1287
|
+
canvas.height = 1;
|
|
1288
|
+
return {
|
|
1289
|
+
width: page.width,
|
|
1290
|
+
height: page.height,
|
|
1291
|
+
url
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
async function buildAtlasPages(pages, textureLighting, doc, atlasScale, isCancelled) {
|
|
1295
|
+
const built = [];
|
|
1296
|
+
for (const page of pages) {
|
|
1297
|
+
if (isCancelled()) break;
|
|
1298
|
+
built.push(await buildAtlasPage(page, textureLighting, doc, atlasScale));
|
|
1299
|
+
}
|
|
1300
|
+
return built;
|
|
1301
|
+
}
|
|
1302
|
+
function useTextureAtlas(plans, textureLighting, atlasScaleInput) {
|
|
1303
|
+
const useBorderShape = textureLighting !== "dynamic" && borderShapeSupported();
|
|
1304
|
+
const atlasPlans = useMemo5(
|
|
1305
|
+
() => plans.map((plan) => {
|
|
1306
|
+
if (!plan) return plan;
|
|
1307
|
+
if (plan.texture) return plan;
|
|
1308
|
+
if (isSolidTrianglePlan(plan)) return null;
|
|
1309
|
+
if (textureLighting !== "dynamic" && (useBorderShape || isFullRectSolid(plan))) return null;
|
|
1310
|
+
return plan;
|
|
1311
|
+
}),
|
|
1312
|
+
[plans, textureLighting, useBorderShape]
|
|
1313
|
+
);
|
|
1314
|
+
const { packed, atlasScale } = useMemo5(
|
|
1315
|
+
() => packTextureAtlasPlansWithScale(atlasPlans, atlasScaleInput),
|
|
1316
|
+
[atlasPlans, atlasScaleInput]
|
|
1317
|
+
);
|
|
1318
|
+
const [pages, setPages] = useState(
|
|
1319
|
+
() => packed.pages.map((page) => ({ width: page.width, height: page.height, url: null }))
|
|
1320
|
+
);
|
|
1321
|
+
useEffect2(() => {
|
|
1322
|
+
let cancelled = false;
|
|
1323
|
+
let urls = [];
|
|
1324
|
+
setPages(packed.pages.map((page) => ({ width: page.width, height: page.height, url: null })));
|
|
1325
|
+
if (packed.pages.length === 0 || typeof document === "undefined") {
|
|
1326
|
+
return () => {
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
buildAtlasPages(packed.pages, textureLighting, document, atlasScale, () => cancelled).then((nextPages) => {
|
|
1330
|
+
if (cancelled) {
|
|
1331
|
+
for (const page of nextPages) {
|
|
1332
|
+
if (page.url?.startsWith("blob:")) URL.revokeObjectURL(page.url);
|
|
1333
|
+
}
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
urls = nextPages.flatMap((page) => page.url?.startsWith("blob:") ? [page.url] : []);
|
|
1337
|
+
setPages(nextPages);
|
|
1338
|
+
}).catch(() => {
|
|
1339
|
+
if (!cancelled) {
|
|
1340
|
+
setPages(packed.pages.map((page) => ({ width: page.width, height: page.height, url: null })));
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
return () => {
|
|
1344
|
+
cancelled = true;
|
|
1345
|
+
for (const url of urls) URL.revokeObjectURL(url);
|
|
1346
|
+
};
|
|
1347
|
+
}, [packed, textureLighting, atlasScale]);
|
|
1348
|
+
return {
|
|
1349
|
+
entries: packed.entries,
|
|
1350
|
+
pages,
|
|
1351
|
+
ready: pages.length === 0 || pages.every((page) => !!page.url)
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function TextureBorderShapePoly({
|
|
1355
|
+
entry,
|
|
1356
|
+
className,
|
|
1357
|
+
style: styleProp,
|
|
1358
|
+
domAttrs,
|
|
1359
|
+
domEventHandlers,
|
|
1360
|
+
pointerEvents = "auto"
|
|
1361
|
+
}) {
|
|
1362
|
+
const fullRect = isFullRectSolid(entry);
|
|
1363
|
+
const borderShape = fullRect ? null : cssBorderShapeForPlan(entry);
|
|
1364
|
+
const setElementRef = useCallback3((el) => {
|
|
1365
|
+
if (!el) return;
|
|
1366
|
+
if (borderShape) el.style.setProperty("border-shape", borderShape);
|
|
1367
|
+
else el.style.removeProperty("border-shape");
|
|
1368
|
+
}, [borderShape]);
|
|
1369
|
+
const style = fullRect ? {
|
|
1370
|
+
width: entry.canvasW,
|
|
1371
|
+
height: entry.canvasH,
|
|
1372
|
+
transform: formatMatrix3d(entry.matrix),
|
|
1373
|
+
background: entry.shadedColor,
|
|
1374
|
+
pointerEvents: pointerEvents === "none" ? "none" : void 0,
|
|
1375
|
+
...styleProp
|
|
1376
|
+
} : {
|
|
1377
|
+
width: entry.canvasW,
|
|
1378
|
+
height: entry.canvasH,
|
|
1379
|
+
transform: formatMatrix3d(entry.matrix),
|
|
1380
|
+
color: entry.shadedColor,
|
|
1381
|
+
pointerEvents: pointerEvents === "none" ? "none" : void 0,
|
|
1382
|
+
...styleProp
|
|
1383
|
+
};
|
|
1384
|
+
const dataAttrs = entry.polygon.data ? Object.fromEntries(
|
|
1385
|
+
Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
|
|
1386
|
+
) : {};
|
|
1387
|
+
const elementClassName = className?.trim() || void 0;
|
|
1388
|
+
if (fullRect) {
|
|
1389
|
+
return /* @__PURE__ */ jsx3(
|
|
1390
|
+
"b",
|
|
1391
|
+
{
|
|
1392
|
+
className: elementClassName,
|
|
1393
|
+
style,
|
|
1394
|
+
...domEventHandlers,
|
|
1395
|
+
...dataAttrs,
|
|
1396
|
+
...domAttrs
|
|
1397
|
+
}
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
return /* @__PURE__ */ jsx3(
|
|
1401
|
+
"i",
|
|
1402
|
+
{
|
|
1403
|
+
ref: setElementRef,
|
|
1404
|
+
className: elementClassName,
|
|
1405
|
+
style,
|
|
1406
|
+
...domEventHandlers,
|
|
1407
|
+
...dataAttrs,
|
|
1408
|
+
...domAttrs
|
|
1409
|
+
}
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
function TextureTrianglePoly({
|
|
1413
|
+
entry,
|
|
1414
|
+
textureLighting,
|
|
1415
|
+
className,
|
|
1416
|
+
style: styleProp,
|
|
1417
|
+
domAttrs,
|
|
1418
|
+
domEventHandlers,
|
|
1419
|
+
pointerEvents = "auto"
|
|
1420
|
+
}) {
|
|
1421
|
+
const triangleStyle = solidTriangleStyle(entry, textureLighting, pointerEvents);
|
|
1422
|
+
if (!triangleStyle) return null;
|
|
1423
|
+
const dataAttrs = entry.polygon.data ? Object.fromEntries(
|
|
1424
|
+
Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
|
|
1425
|
+
) : {};
|
|
1426
|
+
const elementClassName = className?.trim() || void 0;
|
|
1427
|
+
return /* @__PURE__ */ jsx3(
|
|
1428
|
+
"u",
|
|
1429
|
+
{
|
|
1430
|
+
className: elementClassName,
|
|
1431
|
+
style: { ...triangleStyle, ...styleProp },
|
|
1432
|
+
...domEventHandlers,
|
|
1433
|
+
...dataAttrs,
|
|
1434
|
+
...domAttrs
|
|
1435
|
+
}
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
function TextureAtlasPoly({
|
|
1439
|
+
entry,
|
|
1440
|
+
page,
|
|
1441
|
+
textureLighting,
|
|
1442
|
+
className,
|
|
1443
|
+
style: styleProp,
|
|
1444
|
+
domAttrs,
|
|
1445
|
+
domEventHandlers,
|
|
1446
|
+
pointerEvents = "auto"
|
|
1447
|
+
}) {
|
|
1448
|
+
const dynamic = textureLighting === "dynamic";
|
|
1449
|
+
const dynamicMask = dynamic && page?.url ? `url(${page.url})` : void 0;
|
|
1450
|
+
const background = !dynamic && page?.url ? `url(${page.url}) -${entry.x}px -${entry.y}px / ${page.width}px ${page.height}px no-repeat` : void 0;
|
|
1451
|
+
const style = {
|
|
1452
|
+
width: entry.canvasW,
|
|
1453
|
+
height: entry.canvasH,
|
|
1454
|
+
transform: formatMatrix3d(entry.matrix),
|
|
1455
|
+
background,
|
|
1456
|
+
backgroundImage: dynamic && page?.url ? `url(${page.url})` : void 0,
|
|
1457
|
+
backgroundPosition: dynamic ? `-${entry.x}px -${entry.y}px` : void 0,
|
|
1458
|
+
backgroundSize: dynamic && page ? `${page.width}px ${page.height}px` : void 0,
|
|
1459
|
+
...dynamic ? {
|
|
1460
|
+
["--pnx"]: entry.normal[0].toFixed(4),
|
|
1461
|
+
["--pny"]: entry.normal[1].toFixed(4),
|
|
1462
|
+
["--pnz"]: entry.normal[2].toFixed(4)
|
|
1463
|
+
} : null,
|
|
1464
|
+
...dynamic && dynamicMask ? {
|
|
1465
|
+
// Use the atlas as an alpha mask so transparent regions outside
|
|
1466
|
+
// the polygon don't get painted with the tint.
|
|
1467
|
+
maskImage: dynamicMask,
|
|
1468
|
+
maskMode: "alpha",
|
|
1469
|
+
maskPosition: `-${entry.x}px -${entry.y}px`,
|
|
1470
|
+
maskSize: page ? `${page.width}px ${page.height}px` : void 0,
|
|
1471
|
+
maskRepeat: "no-repeat",
|
|
1472
|
+
WebkitMaskImage: dynamicMask,
|
|
1473
|
+
WebkitMaskPosition: `-${entry.x}px -${entry.y}px`,
|
|
1474
|
+
WebkitMaskSize: page ? `${page.width}px ${page.height}px` : void 0,
|
|
1475
|
+
WebkitMaskRepeat: "no-repeat"
|
|
1476
|
+
} : null,
|
|
1477
|
+
opacity: page?.url ? void 0 : 0,
|
|
1478
|
+
pointerEvents: pointerEvents === "none" ? "none" : void 0,
|
|
1479
|
+
...styleProp
|
|
1480
|
+
};
|
|
1481
|
+
const dataAttrs = entry.polygon.data ? Object.fromEntries(
|
|
1482
|
+
Object.entries(entry.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
|
|
1483
|
+
) : {};
|
|
1484
|
+
const elementClassName = className?.trim() || void 0;
|
|
1485
|
+
return /* @__PURE__ */ jsx3(
|
|
1486
|
+
"s",
|
|
1487
|
+
{
|
|
1488
|
+
className: elementClassName,
|
|
1489
|
+
style,
|
|
1490
|
+
...domEventHandlers,
|
|
1491
|
+
...dataAttrs,
|
|
1492
|
+
...domAttrs
|
|
1493
|
+
}
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/scene/sceneContext.ts
|
|
1498
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
1499
|
+
var PolySceneContext = createContext2(null);
|
|
1500
|
+
function usePolySceneContext2() {
|
|
1501
|
+
return useContext2(PolySceneContext);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/scene/PolyScene.tsx
|
|
1505
|
+
import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
1506
|
+
function PolySceneInner({
|
|
1507
|
+
polygons: polygonsProp,
|
|
1508
|
+
centerPolygons: centerPolygonsProp,
|
|
1509
|
+
perspective: _perspective,
|
|
1510
|
+
rotX: _rotX,
|
|
1511
|
+
rotY: _rotY,
|
|
1512
|
+
zoom: _zoom,
|
|
1513
|
+
directionalLight,
|
|
1514
|
+
ambientLight,
|
|
1515
|
+
textureLighting = "baked",
|
|
1516
|
+
atlasScale,
|
|
1517
|
+
autoCenter = false,
|
|
1518
|
+
className,
|
|
1519
|
+
style,
|
|
1520
|
+
children,
|
|
1521
|
+
position: _position,
|
|
1522
|
+
scale: _scale,
|
|
1523
|
+
rotation: _rotation,
|
|
1524
|
+
debugShowLabels: _debugShowLabels,
|
|
1525
|
+
debugShowBackfaces
|
|
1526
|
+
}) {
|
|
1527
|
+
const { store, sceneElRef } = useCameraContext();
|
|
1528
|
+
const cameraState = store.getState().cameraState;
|
|
1529
|
+
const localSceneRef = useCallback4(
|
|
1530
|
+
(el) => {
|
|
1531
|
+
sceneElRef.current = el;
|
|
1532
|
+
},
|
|
1533
|
+
[sceneElRef]
|
|
1534
|
+
);
|
|
1535
|
+
useEffect3(() => {
|
|
1536
|
+
const el = sceneElRef.current;
|
|
1537
|
+
if (!el) return;
|
|
1538
|
+
el.classList.toggle("polycss-debug-show-backfaces", !!debugShowBackfaces);
|
|
1539
|
+
}, [debugShowBackfaces, sceneElRef]);
|
|
1540
|
+
const injectedRef = useRef3(false);
|
|
1541
|
+
useEffect3(() => {
|
|
1542
|
+
if (injectedRef.current) return;
|
|
1543
|
+
if (typeof document !== "undefined") {
|
|
1544
|
+
injectPolyBaseStyles(document);
|
|
1545
|
+
injectedRef.current = true;
|
|
1546
|
+
}
|
|
1547
|
+
}, []);
|
|
1548
|
+
const inputPolygons = useMemo6(() => polygonsProp ?? [], [polygonsProp]);
|
|
1549
|
+
const centerInputPolygons = useMemo6(
|
|
1550
|
+
() => centerPolygonsProp ?? null,
|
|
1551
|
+
[centerPolygonsProp]
|
|
1552
|
+
);
|
|
1553
|
+
const { polygons, sceneBbox: renderSceneBbox } = usePolySceneContext(inputPolygons, {
|
|
1554
|
+
directionalLight
|
|
1555
|
+
});
|
|
1556
|
+
const { sceneBbox: centerSceneBbox } = usePolySceneContext(
|
|
1557
|
+
centerInputPolygons ?? inputPolygons,
|
|
1558
|
+
{ directionalLight }
|
|
1559
|
+
);
|
|
1560
|
+
const sceneBbox = centerInputPolygons ? centerSceneBbox : renderSceneBbox;
|
|
1561
|
+
const sceneStyle = useMemo6(() => {
|
|
1562
|
+
const handle = createIsometricCamera2(cameraState);
|
|
1563
|
+
const cameraStyle = handle.getStyle();
|
|
1564
|
+
return {
|
|
1565
|
+
["--scene-transform"]: cameraStyle.transform
|
|
1566
|
+
};
|
|
1567
|
+
}, [cameraState]);
|
|
1568
|
+
const computedClassName = `polycss-scene${className ? ` ${className}` : ""}`;
|
|
1569
|
+
const directionalForAtlas = textureLighting === "dynamic" ? void 0 : directionalLight;
|
|
1570
|
+
const ambientForAtlas = textureLighting === "dynamic" ? void 0 : ambientLight;
|
|
1571
|
+
const polyContext = useMemo6(() => {
|
|
1572
|
+
const tileSize = 50;
|
|
1573
|
+
return {
|
|
1574
|
+
tileSize,
|
|
1575
|
+
layerElevation: tileSize,
|
|
1576
|
+
directionalLight: directionalForAtlas,
|
|
1577
|
+
ambientLight: ambientForAtlas,
|
|
1578
|
+
textureLighting
|
|
1579
|
+
};
|
|
1580
|
+
}, [directionalForAtlas, ambientForAtlas, textureLighting]);
|
|
1581
|
+
const textureAtlasPlans = useMemo6(
|
|
1582
|
+
() => polygons.map((p, i) => computeTextureAtlasPlan(p, i, polyContext)),
|
|
1583
|
+
[polygons, polyContext]
|
|
1584
|
+
);
|
|
1585
|
+
const textureAtlas = useTextureAtlas(textureAtlasPlans, textureLighting, atlasScale);
|
|
1586
|
+
const dynamicLightVars = useMemo6(() => {
|
|
1587
|
+
if (textureLighting !== "dynamic") return null;
|
|
1588
|
+
const dir = directionalLight?.direction ?? [0.4, -0.7, 0.59];
|
|
1589
|
+
const len = Math.hypot(dir[0], dir[1], dir[2]) || 1;
|
|
1590
|
+
const lx = dir[0] / len, ly = dir[1] / len, lz = dir[2] / len;
|
|
1591
|
+
const lightRgb = parseHexColor(directionalLight?.color ?? "#ffffff")?.rgb ?? [255, 255, 255];
|
|
1592
|
+
const ambRgb = parseHexColor(ambientLight?.color ?? "#ffffff")?.rgb ?? [255, 255, 255];
|
|
1593
|
+
const lightIntensity = directionalLight?.intensity ?? 1;
|
|
1594
|
+
const ambientIntensity = ambientLight?.intensity ?? 0.4;
|
|
1595
|
+
const ch = (n) => (n / 255).toFixed(4);
|
|
1596
|
+
return {
|
|
1597
|
+
["--plx"]: lx.toFixed(4),
|
|
1598
|
+
["--ply"]: ly.toFixed(4),
|
|
1599
|
+
["--plz"]: lz.toFixed(4),
|
|
1600
|
+
["--plr"]: ch(lightRgb[0]),
|
|
1601
|
+
["--plg"]: ch(lightRgb[1]),
|
|
1602
|
+
["--plb"]: ch(lightRgb[2]),
|
|
1603
|
+
["--pli"]: lightIntensity.toFixed(4),
|
|
1604
|
+
["--par"]: ch(ambRgb[0]),
|
|
1605
|
+
["--pag"]: ch(ambRgb[1]),
|
|
1606
|
+
["--pab"]: ch(ambRgb[2]),
|
|
1607
|
+
["--pai"]: ambientIntensity.toFixed(4)
|
|
1608
|
+
};
|
|
1609
|
+
}, [textureLighting, directionalLight, ambientLight]);
|
|
1610
|
+
const autoCenterTransform = useMemo6(() => {
|
|
1611
|
+
if (!autoCenter) return void 0;
|
|
1612
|
+
const cssX = (sceneBbox.min[1] + sceneBbox.max[1]) / 2 * polyContext.tileSize;
|
|
1613
|
+
const cssY = (sceneBbox.min[0] + sceneBbox.max[0]) / 2 * polyContext.tileSize;
|
|
1614
|
+
const cssZ = (sceneBbox.min[2] + sceneBbox.max[2]) / 2 * polyContext.layerElevation;
|
|
1615
|
+
return `translate3d(${-cssX}px, ${-cssY}px, ${-cssZ}px)`;
|
|
1616
|
+
}, [autoCenter, sceneBbox, polyContext.tileSize, polyContext.layerElevation]);
|
|
1617
|
+
const polyChildren = textureAtlas.entries.map((entry, index) => {
|
|
1618
|
+
if (entry) {
|
|
1619
|
+
return /* @__PURE__ */ jsx4(
|
|
1620
|
+
TextureAtlasPoly,
|
|
1621
|
+
{
|
|
1622
|
+
entry,
|
|
1623
|
+
page: textureAtlas.pages[entry.pageIndex],
|
|
1624
|
+
textureLighting
|
|
1625
|
+
},
|
|
1626
|
+
entry.index
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
const plan = textureAtlasPlans[index];
|
|
1630
|
+
if (!plan || plan.texture) return null;
|
|
1631
|
+
return isSolidTrianglePlan(plan) ? /* @__PURE__ */ jsx4(TextureTrianglePoly, { entry: plan, textureLighting }, plan.index) : /* @__PURE__ */ jsx4(TextureBorderShapePoly, { entry: plan }, plan.index);
|
|
1632
|
+
});
|
|
1633
|
+
const sceneCtxValue = useMemo6(
|
|
1634
|
+
() => ({ textureLighting, directionalLight, ambientLight }),
|
|
1635
|
+
[textureLighting, directionalLight, ambientLight]
|
|
1636
|
+
);
|
|
1637
|
+
return /* @__PURE__ */ jsx4(PolySceneContext.Provider, { value: sceneCtxValue, children: /* @__PURE__ */ jsx4(
|
|
1638
|
+
"div",
|
|
1639
|
+
{
|
|
1640
|
+
ref: localSceneRef,
|
|
1641
|
+
className: computedClassName,
|
|
1642
|
+
"data-polycss-lighting": textureLighting,
|
|
1643
|
+
"aria-hidden": "true",
|
|
1644
|
+
style: {
|
|
1645
|
+
...sceneStyle,
|
|
1646
|
+
...dynamicLightVars ?? null,
|
|
1647
|
+
...style
|
|
1648
|
+
// No more --polycss-rows / --polycss-cols — CSS Grid was dropped
|
|
1649
|
+
// in Phase 4 (per §Design.4a).
|
|
1650
|
+
},
|
|
1651
|
+
children: autoCenterTransform ? /* @__PURE__ */ jsxs("div", { className: "polycss-offset", style: { ["--offset-transform"]: autoCenterTransform }, children: [
|
|
1652
|
+
polyChildren,
|
|
1653
|
+
children
|
|
1654
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1655
|
+
polyChildren,
|
|
1656
|
+
children
|
|
1657
|
+
] })
|
|
1658
|
+
}
|
|
1659
|
+
) });
|
|
1660
|
+
}
|
|
1661
|
+
var PolyScene = memo3(PolySceneInner);
|
|
1662
|
+
|
|
1663
|
+
// src/scene/PolyMesh.tsx
|
|
1664
|
+
import {
|
|
1665
|
+
forwardRef,
|
|
1666
|
+
useCallback as useCallback6,
|
|
1667
|
+
useContext as useContext3,
|
|
1668
|
+
useEffect as useEffect5,
|
|
1669
|
+
useImperativeHandle,
|
|
1670
|
+
useMemo as useMemo7,
|
|
1671
|
+
useRef as useRef5,
|
|
1672
|
+
useState as useState3
|
|
1673
|
+
} from "react";
|
|
1674
|
+
import { computeSceneBbox, inverseRotateVec3 } from "@layoutit/polycss-core";
|
|
1675
|
+
|
|
1676
|
+
// src/scene/useMesh.ts
|
|
1677
|
+
import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef4, useState as useState2 } from "react";
|
|
1678
|
+
import { loadMesh } from "@layoutit/polycss-core";
|
|
1679
|
+
var EMPTY_POLYGONS = [];
|
|
1680
|
+
var EMPTY_WARNINGS = [];
|
|
1681
|
+
function usePolyMesh(src, options) {
|
|
1682
|
+
const [state, setState] = useState2({
|
|
1683
|
+
polygons: EMPTY_POLYGONS,
|
|
1684
|
+
loading: !!src,
|
|
1685
|
+
error: null,
|
|
1686
|
+
warnings: EMPTY_WARNINGS
|
|
1687
|
+
});
|
|
1688
|
+
const activeResultRef = useRef4(null);
|
|
1689
|
+
const dispose = useCallback5(() => {
|
|
1690
|
+
const r = activeResultRef.current;
|
|
1691
|
+
if (r) {
|
|
1692
|
+
try {
|
|
1693
|
+
r.dispose();
|
|
1694
|
+
} catch {
|
|
1695
|
+
}
|
|
1696
|
+
activeResultRef.current = null;
|
|
1697
|
+
}
|
|
1698
|
+
}, []);
|
|
1699
|
+
useEffect4(() => {
|
|
1700
|
+
if (!src) {
|
|
1701
|
+
dispose();
|
|
1702
|
+
setState({
|
|
1703
|
+
polygons: EMPTY_POLYGONS,
|
|
1704
|
+
loading: false,
|
|
1705
|
+
error: null,
|
|
1706
|
+
warnings: EMPTY_WARNINGS
|
|
1707
|
+
});
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
let cancelled = false;
|
|
1711
|
+
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
1712
|
+
const prevResult = activeResultRef.current;
|
|
1713
|
+
loadMesh(src, options).then((result) => {
|
|
1714
|
+
if (cancelled) {
|
|
1715
|
+
try {
|
|
1716
|
+
result.dispose();
|
|
1717
|
+
} catch {
|
|
1718
|
+
}
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (prevResult) {
|
|
1722
|
+
try {
|
|
1723
|
+
prevResult.dispose();
|
|
1724
|
+
} catch {
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
activeResultRef.current = result;
|
|
1728
|
+
setState({
|
|
1729
|
+
polygons: result.polygons,
|
|
1730
|
+
loading: false,
|
|
1731
|
+
error: null,
|
|
1732
|
+
warnings: result.warnings ?? EMPTY_WARNINGS
|
|
1733
|
+
});
|
|
1734
|
+
}).catch((err) => {
|
|
1735
|
+
if (cancelled) return;
|
|
1736
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1737
|
+
setState((prev) => ({
|
|
1738
|
+
polygons: prev.polygons,
|
|
1739
|
+
loading: false,
|
|
1740
|
+
error,
|
|
1741
|
+
warnings: prev.warnings
|
|
1742
|
+
}));
|
|
1743
|
+
});
|
|
1744
|
+
return () => {
|
|
1745
|
+
cancelled = true;
|
|
1746
|
+
};
|
|
1747
|
+
}, [src, dispose]);
|
|
1748
|
+
useEffect4(() => {
|
|
1749
|
+
return () => dispose();
|
|
1750
|
+
}, [dispose]);
|
|
1751
|
+
return {
|
|
1752
|
+
polygons: state.polygons,
|
|
1753
|
+
loading: state.loading,
|
|
1754
|
+
error: state.error,
|
|
1755
|
+
warnings: state.warnings,
|
|
1756
|
+
dispose
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/scene/events.ts
|
|
1761
|
+
var MESH_REGISTRY = /* @__PURE__ */ new WeakMap();
|
|
1762
|
+
function registerMeshElement(el, handle) {
|
|
1763
|
+
MESH_REGISTRY.set(el, handle);
|
|
1764
|
+
}
|
|
1765
|
+
function unregisterMeshElement(el) {
|
|
1766
|
+
MESH_REGISTRY.delete(el);
|
|
1767
|
+
}
|
|
1768
|
+
function findPolyMeshHandle(el) {
|
|
1769
|
+
let cur = el;
|
|
1770
|
+
while (cur) {
|
|
1771
|
+
if (cur instanceof HTMLElement) {
|
|
1772
|
+
const h = MESH_REGISTRY.get(cur);
|
|
1773
|
+
if (h) return h;
|
|
1774
|
+
}
|
|
1775
|
+
cur = cur.parentElement;
|
|
1776
|
+
}
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
function pointInMeshElement(meshEl, clientX, clientY) {
|
|
1780
|
+
const polys = Array.from(meshEl.querySelectorAll("i,b,s,u"));
|
|
1781
|
+
for (const p of polys) {
|
|
1782
|
+
const r = p.getBoundingClientRect();
|
|
1783
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
1784
|
+
if (clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom) {
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return false;
|
|
1789
|
+
}
|
|
1790
|
+
function findMeshUnderPoint(clientX, clientY, filter) {
|
|
1791
|
+
if (typeof document === "undefined") return null;
|
|
1792
|
+
const meshEls = Array.from(
|
|
1793
|
+
document.querySelectorAll(".polycss-mesh")
|
|
1794
|
+
);
|
|
1795
|
+
for (const meshEl of meshEls) {
|
|
1796
|
+
if (filter && !filter(meshEl)) continue;
|
|
1797
|
+
const handle = findPolyMeshHandle(meshEl);
|
|
1798
|
+
if (!handle) continue;
|
|
1799
|
+
if (pointInMeshElement(meshEl, clientX, clientY)) return handle;
|
|
1800
|
+
}
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/scene/PolyMesh.tsx
|
|
1805
|
+
import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
|
|
1806
|
+
function buildTransform(position, scale, rotation) {
|
|
1807
|
+
const parts = [];
|
|
1808
|
+
if (position) {
|
|
1809
|
+
parts.push(`translate3d(${position[0]}px, ${position[1]}px, ${position[2]}px)`);
|
|
1810
|
+
}
|
|
1811
|
+
if (scale !== void 0) {
|
|
1812
|
+
if (typeof scale === "number") {
|
|
1813
|
+
if (scale !== 1) parts.push(`scale3d(${scale}, ${scale}, ${scale})`);
|
|
1814
|
+
} else {
|
|
1815
|
+
parts.push(`scale3d(${scale[0]}, ${scale[1]}, ${scale[2]})`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (rotation) {
|
|
1819
|
+
if (rotation[0]) parts.push(`rotateX(${rotation[0]}deg)`);
|
|
1820
|
+
if (rotation[1]) parts.push(`rotateY(${rotation[1]}deg)`);
|
|
1821
|
+
if (rotation[2]) parts.push(`rotateZ(${rotation[2]}deg)`);
|
|
1822
|
+
}
|
|
1823
|
+
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
1824
|
+
}
|
|
1825
|
+
function recenterPolygons(polygons) {
|
|
1826
|
+
if (polygons.length === 0) return polygons;
|
|
1827
|
+
const bbox = computeSceneBbox(polygons);
|
|
1828
|
+
const cx = (bbox.min[0] + bbox.max[0]) / 2;
|
|
1829
|
+
const cy = (bbox.min[1] + bbox.max[1]) / 2;
|
|
1830
|
+
const cz = (bbox.min[2] + bbox.max[2]) / 2;
|
|
1831
|
+
if (cx === 0 && cy === 0 && cz === 0) return polygons;
|
|
1832
|
+
const shift = (v) => [v[0] - cx, v[1] - cy, v[2] - cz];
|
|
1833
|
+
return polygons.map((p) => ({
|
|
1834
|
+
...p,
|
|
1835
|
+
vertices: p.vertices.map(shift),
|
|
1836
|
+
...p.textureTriangles?.length ? {
|
|
1837
|
+
textureTriangles: p.textureTriangles.map((triangle) => ({
|
|
1838
|
+
...triangle,
|
|
1839
|
+
vertices: triangle.vertices.map(shift)
|
|
1840
|
+
}))
|
|
1841
|
+
} : null
|
|
1842
|
+
}));
|
|
1843
|
+
}
|
|
1844
|
+
var PolyMesh = forwardRef(function PolyMesh2({
|
|
1845
|
+
id,
|
|
1846
|
+
src,
|
|
1847
|
+
mtl,
|
|
1848
|
+
polygons: polygonsProp,
|
|
1849
|
+
autoCenter,
|
|
1850
|
+
textureLighting,
|
|
1851
|
+
atlasScale,
|
|
1852
|
+
children,
|
|
1853
|
+
fallback,
|
|
1854
|
+
errorFallback,
|
|
1855
|
+
parseOptions,
|
|
1856
|
+
position,
|
|
1857
|
+
scale,
|
|
1858
|
+
rotation,
|
|
1859
|
+
className,
|
|
1860
|
+
style,
|
|
1861
|
+
onClick,
|
|
1862
|
+
onContextMenu,
|
|
1863
|
+
onDoubleClick,
|
|
1864
|
+
onWheel,
|
|
1865
|
+
onPointerDown,
|
|
1866
|
+
onPointerUp,
|
|
1867
|
+
onPointerMove,
|
|
1868
|
+
onPointerOver,
|
|
1869
|
+
onPointerOut,
|
|
1870
|
+
onPointerEnter,
|
|
1871
|
+
onPointerLeave,
|
|
1872
|
+
onPointerCancel
|
|
1873
|
+
}, forwardedRef) {
|
|
1874
|
+
const mergedOptions = useMemo7(() => {
|
|
1875
|
+
if (!mtl && !parseOptions) return void 0;
|
|
1876
|
+
return { ...parseOptions ?? {}, ...mtl ? { mtlUrl: mtl } : {} };
|
|
1877
|
+
}, [mtl, parseOptions]);
|
|
1878
|
+
const fetched = usePolyMesh(src ?? "", mergedOptions);
|
|
1879
|
+
const sourcePolygons = src ? fetched.polygons : polygonsProp ?? [];
|
|
1880
|
+
const polygons = useMemo7(
|
|
1881
|
+
() => autoCenter ? recenterPolygons(sourcePolygons) : sourcePolygons,
|
|
1882
|
+
[sourcePolygons, autoCenter]
|
|
1883
|
+
);
|
|
1884
|
+
const transform = buildTransform(position, scale, rotation);
|
|
1885
|
+
const wrapperRef = useRef5(null);
|
|
1886
|
+
const propsRef = useRef5({ position, scale, rotation });
|
|
1887
|
+
propsRef.current = { position, scale, rotation };
|
|
1888
|
+
const polygonsRef = useRef5(polygons);
|
|
1889
|
+
polygonsRef.current = polygons;
|
|
1890
|
+
const [bakedRotation, setBakedRotation] = useState3(rotation);
|
|
1891
|
+
const handle = useMemo7(() => ({
|
|
1892
|
+
get element() {
|
|
1893
|
+
return wrapperRef.current;
|
|
1894
|
+
},
|
|
1895
|
+
id,
|
|
1896
|
+
getPosition: () => propsRef.current.position,
|
|
1897
|
+
getRotation: () => propsRef.current.rotation,
|
|
1898
|
+
getScale: () => propsRef.current.scale,
|
|
1899
|
+
getPolygons: () => polygonsRef.current,
|
|
1900
|
+
rebakeAtlas: () => setBakedRotation(propsRef.current.rotation)
|
|
1901
|
+
}), [id]);
|
|
1902
|
+
useImperativeHandle(forwardedRef, () => handle, [handle]);
|
|
1903
|
+
useEffect5(() => {
|
|
1904
|
+
const el = wrapperRef.current;
|
|
1905
|
+
if (!el) return;
|
|
1906
|
+
registerMeshElement(el, handle);
|
|
1907
|
+
return () => unregisterMeshElement(el);
|
|
1908
|
+
}, [handle]);
|
|
1909
|
+
const cameraCtx = useContext3(PolyCameraContext);
|
|
1910
|
+
const cameraElRef = cameraCtx?.cameraElRef ?? null;
|
|
1911
|
+
const pointerDownAtRef = useRef5(null);
|
|
1912
|
+
const makeEvent = useCallback6(
|
|
1913
|
+
function makeEvent2(nativeEvent, clientX, clientY) {
|
|
1914
|
+
const intersections = [];
|
|
1915
|
+
if (typeof document !== "undefined" && typeof document.elementsFromPoint === "function") {
|
|
1916
|
+
const stacked = document.elementsFromPoint(clientX, clientY);
|
|
1917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1918
|
+
for (const el of stacked) {
|
|
1919
|
+
const h = findPolyMeshHandle(el);
|
|
1920
|
+
if (h && !seen.has(h)) {
|
|
1921
|
+
seen.add(h);
|
|
1922
|
+
intersections.push({ object: h });
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
let nx = 0;
|
|
1927
|
+
let ny = 0;
|
|
1928
|
+
const camEl = cameraElRef?.current;
|
|
1929
|
+
if (camEl) {
|
|
1930
|
+
const r = camEl.getBoundingClientRect();
|
|
1931
|
+
if (r.width > 0 && r.height > 0) {
|
|
1932
|
+
nx = (clientX - r.left) / r.width * 2 - 1;
|
|
1933
|
+
ny = -((clientY - r.top) / r.height * 2 - 1);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
let delta = 0;
|
|
1937
|
+
const pd = pointerDownAtRef.current;
|
|
1938
|
+
if (pd) delta = Math.hypot(clientX - pd.x, clientY - pd.y);
|
|
1939
|
+
return {
|
|
1940
|
+
object: intersections[0]?.object ?? handle,
|
|
1941
|
+
eventObject: handle,
|
|
1942
|
+
intersections,
|
|
1943
|
+
pointer: { x: nx, y: ny },
|
|
1944
|
+
delta,
|
|
1945
|
+
nativeEvent,
|
|
1946
|
+
stopPropagation: () => nativeEvent.stopPropagation()
|
|
1947
|
+
};
|
|
1948
|
+
},
|
|
1949
|
+
[cameraElRef, handle]
|
|
1950
|
+
);
|
|
1951
|
+
const wrapperHandlers = useMemo7(() => {
|
|
1952
|
+
const dispatch = (polyHandler, reactEvent, nativeEvent, clientX, clientY) => {
|
|
1953
|
+
if (!polyHandler) return;
|
|
1954
|
+
const polyEvent = makeEvent(nativeEvent, clientX, clientY);
|
|
1955
|
+
const originalStop = polyEvent.stopPropagation;
|
|
1956
|
+
polyEvent.stopPropagation = () => {
|
|
1957
|
+
originalStop();
|
|
1958
|
+
reactEvent.stopPropagation();
|
|
1959
|
+
};
|
|
1960
|
+
polyHandler(polyEvent);
|
|
1961
|
+
};
|
|
1962
|
+
const out = {};
|
|
1963
|
+
if (onClick) {
|
|
1964
|
+
out.onClick = (e) => dispatch(onClick, e, e.nativeEvent, e.clientX, e.clientY);
|
|
1965
|
+
}
|
|
1966
|
+
if (onContextMenu) {
|
|
1967
|
+
out.onContextMenu = (e) => dispatch(onContextMenu, e, e.nativeEvent, e.clientX, e.clientY);
|
|
1968
|
+
}
|
|
1969
|
+
if (onDoubleClick) {
|
|
1970
|
+
out.onDoubleClick = (e) => dispatch(onDoubleClick, e, e.nativeEvent, e.clientX, e.clientY);
|
|
1971
|
+
}
|
|
1972
|
+
if (onWheel) {
|
|
1973
|
+
out.onWheel = (e) => dispatch(onWheel, e, e.nativeEvent, e.clientX, e.clientY);
|
|
1974
|
+
}
|
|
1975
|
+
if (onPointerDown) {
|
|
1976
|
+
out.onPointerDown = (e) => {
|
|
1977
|
+
pointerDownAtRef.current = { x: e.clientX, y: e.clientY };
|
|
1978
|
+
dispatch(onPointerDown, e, e.nativeEvent, e.clientX, e.clientY);
|
|
1979
|
+
};
|
|
1980
|
+
} else {
|
|
1981
|
+
out.onPointerDown = (e) => {
|
|
1982
|
+
pointerDownAtRef.current = { x: e.clientX, y: e.clientY };
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
if (onPointerUp) {
|
|
1986
|
+
out.onPointerUp = (e) => {
|
|
1987
|
+
dispatch(onPointerUp, e, e.nativeEvent, e.clientX, e.clientY);
|
|
1988
|
+
pointerDownAtRef.current = null;
|
|
1989
|
+
};
|
|
1990
|
+
} else {
|
|
1991
|
+
out.onPointerUp = () => {
|
|
1992
|
+
pointerDownAtRef.current = null;
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
if (onPointerMove) {
|
|
1996
|
+
out.onPointerMove = (e) => dispatch(onPointerMove, e, e.nativeEvent, e.clientX, e.clientY);
|
|
1997
|
+
}
|
|
1998
|
+
if (onPointerOver || onPointerEnter) {
|
|
1999
|
+
out.onPointerEnter = (e) => {
|
|
2000
|
+
if (onPointerOver) dispatch(onPointerOver, e, e.nativeEvent, e.clientX, e.clientY);
|
|
2001
|
+
if (onPointerEnter) dispatch(onPointerEnter, e, e.nativeEvent, e.clientX, e.clientY);
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
if (onPointerOut || onPointerLeave) {
|
|
2005
|
+
out.onPointerLeave = (e) => {
|
|
2006
|
+
if (onPointerOut) dispatch(onPointerOut, e, e.nativeEvent, e.clientX, e.clientY);
|
|
2007
|
+
if (onPointerLeave) dispatch(onPointerLeave, e, e.nativeEvent, e.clientX, e.clientY);
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
if (onPointerCancel) {
|
|
2011
|
+
out.onPointerCancel = (e) => {
|
|
2012
|
+
dispatch(onPointerCancel, e, e.nativeEvent, e.clientX, e.clientY);
|
|
2013
|
+
pointerDownAtRef.current = null;
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
return out;
|
|
2017
|
+
}, [
|
|
2018
|
+
makeEvent,
|
|
2019
|
+
onClick,
|
|
2020
|
+
onContextMenu,
|
|
2021
|
+
onDoubleClick,
|
|
2022
|
+
onWheel,
|
|
2023
|
+
onPointerDown,
|
|
2024
|
+
onPointerUp,
|
|
2025
|
+
onPointerMove,
|
|
2026
|
+
onPointerOver,
|
|
2027
|
+
onPointerOut,
|
|
2028
|
+
onPointerEnter,
|
|
2029
|
+
onPointerLeave,
|
|
2030
|
+
onPointerCancel
|
|
2031
|
+
]);
|
|
2032
|
+
const sceneCtx = usePolySceneContext2();
|
|
2033
|
+
const effectiveTextureLighting = textureLighting ?? sceneCtx?.textureLighting ?? "baked";
|
|
2034
|
+
const effectiveDirectional = effectiveTextureLighting === "dynamic" ? void 0 : sceneCtx?.directionalLight;
|
|
2035
|
+
const effectiveAmbient = effectiveTextureLighting === "dynamic" ? void 0 : sceneCtx?.ambientLight;
|
|
2036
|
+
const sceneDirectionalLight = sceneCtx?.directionalLight;
|
|
2037
|
+
const dynamicLightOverride = useMemo7(() => {
|
|
2038
|
+
if (effectiveTextureLighting !== "dynamic") return null;
|
|
2039
|
+
if (!rotation || rotation[0] === 0 && rotation[1] === 0 && rotation[2] === 0) return null;
|
|
2040
|
+
if (!sceneDirectionalLight) return null;
|
|
2041
|
+
const dir = sceneDirectionalLight.direction;
|
|
2042
|
+
const localDir = inverseRotateVec3(dir, rotation);
|
|
2043
|
+
const len = Math.hypot(localDir[0], localDir[1], localDir[2]) || 1;
|
|
2044
|
+
return {
|
|
2045
|
+
["--plx"]: (localDir[0] / len).toFixed(4),
|
|
2046
|
+
["--ply"]: (localDir[1] / len).toFixed(4),
|
|
2047
|
+
["--plz"]: (localDir[2] / len).toFixed(4)
|
|
2048
|
+
};
|
|
2049
|
+
}, [effectiveTextureLighting, rotation, sceneDirectionalLight]);
|
|
2050
|
+
const wrapperStyle = {
|
|
2051
|
+
transform,
|
|
2052
|
+
...dynamicLightOverride,
|
|
2053
|
+
...style
|
|
2054
|
+
};
|
|
2055
|
+
const bakedDirectional = useMemo7(() => {
|
|
2056
|
+
if (!effectiveDirectional) return effectiveDirectional;
|
|
2057
|
+
const rot = bakedRotation ?? [0, 0, 0];
|
|
2058
|
+
if (rot[0] === 0 && rot[1] === 0 && rot[2] === 0) return effectiveDirectional;
|
|
2059
|
+
return {
|
|
2060
|
+
...effectiveDirectional,
|
|
2061
|
+
direction: inverseRotateVec3(effectiveDirectional.direction, rot)
|
|
2062
|
+
};
|
|
2063
|
+
}, [effectiveDirectional, bakedRotation]);
|
|
2064
|
+
const atlasPlans = useMemo7(
|
|
2065
|
+
() => !children ? polygons.map((p, i) => computeTextureAtlasPlan(p, i, {
|
|
2066
|
+
directionalLight: bakedDirectional,
|
|
2067
|
+
ambientLight: effectiveAmbient
|
|
2068
|
+
})) : [],
|
|
2069
|
+
[children, polygons, bakedDirectional, effectiveAmbient]
|
|
2070
|
+
);
|
|
2071
|
+
const textureAtlas = useTextureAtlas(
|
|
2072
|
+
atlasPlans,
|
|
2073
|
+
effectiveTextureLighting,
|
|
2074
|
+
atlasScale
|
|
2075
|
+
);
|
|
2076
|
+
const renderedPolygons = children ? polygons.map((p, i) => (
|
|
2077
|
+
// Render-prop: caller controls how each polygon renders. We still
|
|
2078
|
+
// wrap in a fragment with key so React reconciliation works.
|
|
2079
|
+
/* @__PURE__ */ jsx5(RenderPropPolygon, { polygon: p, index: i, children }, i)
|
|
2080
|
+
)) : textureAtlas.entries.map((entry, index) => {
|
|
2081
|
+
if (entry) {
|
|
2082
|
+
return /* @__PURE__ */ jsx5(
|
|
2083
|
+
TextureAtlasPoly,
|
|
2084
|
+
{
|
|
2085
|
+
entry,
|
|
2086
|
+
page: textureAtlas.pages[entry.pageIndex],
|
|
2087
|
+
textureLighting: effectiveTextureLighting
|
|
2088
|
+
},
|
|
2089
|
+
entry.index
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
const plan = atlasPlans[index];
|
|
2093
|
+
if (!plan || plan.texture) return null;
|
|
2094
|
+
return isSolidTrianglePlan(plan) ? /* @__PURE__ */ jsx5(TextureTrianglePoly, { entry: plan, textureLighting: effectiveTextureLighting }, plan.index) : /* @__PURE__ */ jsx5(TextureBorderShapePoly, { entry: plan }, plan.index);
|
|
2095
|
+
});
|
|
2096
|
+
if (src) {
|
|
2097
|
+
if (fetched.loading && fetched.polygons.length === 0) {
|
|
2098
|
+
return /* @__PURE__ */ jsx5(
|
|
2099
|
+
"div",
|
|
2100
|
+
{
|
|
2101
|
+
ref: wrapperRef,
|
|
2102
|
+
"data-poly-mesh-id": id,
|
|
2103
|
+
className: `polycss-mesh polycss-mesh-loading${className ? ` ${className}` : ""}`,
|
|
2104
|
+
style: wrapperStyle,
|
|
2105
|
+
...wrapperHandlers,
|
|
2106
|
+
children: fallback ?? null
|
|
2107
|
+
}
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
if (fetched.error && fetched.polygons.length === 0) {
|
|
2111
|
+
return /* @__PURE__ */ jsx5(
|
|
2112
|
+
"div",
|
|
2113
|
+
{
|
|
2114
|
+
ref: wrapperRef,
|
|
2115
|
+
"data-poly-mesh-id": id,
|
|
2116
|
+
className: `polycss-mesh polycss-mesh-error${className ? ` ${className}` : ""}`,
|
|
2117
|
+
style: wrapperStyle,
|
|
2118
|
+
...wrapperHandlers,
|
|
2119
|
+
children: errorFallback ? errorFallback(fetched.error) : null
|
|
2120
|
+
}
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return /* @__PURE__ */ jsx5(
|
|
2125
|
+
"div",
|
|
2126
|
+
{
|
|
2127
|
+
ref: wrapperRef,
|
|
2128
|
+
"data-poly-mesh-id": id,
|
|
2129
|
+
className: `polycss-mesh${className ? ` ${className}` : ""}`,
|
|
2130
|
+
style: wrapperStyle,
|
|
2131
|
+
...wrapperHandlers,
|
|
2132
|
+
children: renderedPolygons
|
|
2133
|
+
}
|
|
2134
|
+
);
|
|
2135
|
+
});
|
|
2136
|
+
function RenderPropPolygon({
|
|
2137
|
+
polygon,
|
|
2138
|
+
index,
|
|
2139
|
+
children
|
|
2140
|
+
}) {
|
|
2141
|
+
return /* @__PURE__ */ jsx5(Fragment2, { children: children(polygon, index) });
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/scene/usePolyMaterial.ts
|
|
2145
|
+
import { useMemo as useMemo8 } from "react";
|
|
2146
|
+
function usePolyMaterial(options) {
|
|
2147
|
+
return useMemo8(
|
|
2148
|
+
() => ({ texture: options.texture, key: options.key }),
|
|
2149
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2150
|
+
[options.texture, options.key]
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// src/shapes/Poly.tsx
|
|
2155
|
+
import { memo as memo4, useMemo as useMemo9 } from "react";
|
|
2156
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
2157
|
+
function isAxisAlignedRectUVs(uvs) {
|
|
2158
|
+
if (uvs.length !== 4) return null;
|
|
2159
|
+
const us = [...new Set(uvs.map((uv) => uv[0]))].sort((a, b) => a - b);
|
|
2160
|
+
const vs = [...new Set(uvs.map((uv) => uv[1]))].sort((a, b) => a - b);
|
|
2161
|
+
if (us.length !== 2 || vs.length !== 2) return null;
|
|
2162
|
+
const corners = /* @__PURE__ */ new Set([
|
|
2163
|
+
`${us[0]},${vs[0]}`,
|
|
2164
|
+
`${us[0]},${vs[1]}`,
|
|
2165
|
+
`${us[1]},${vs[0]}`,
|
|
2166
|
+
`${us[1]},${vs[1]}`
|
|
2167
|
+
]);
|
|
2168
|
+
for (const uv of uvs) {
|
|
2169
|
+
if (!corners.has(`${uv[0]},${uv[1]}`)) return null;
|
|
2170
|
+
}
|
|
2171
|
+
return { u0: us[0], u1: us[1], v0: vs[0], v1: vs[1] };
|
|
2172
|
+
}
|
|
2173
|
+
function MaterialDirectPoly({
|
|
2174
|
+
plan,
|
|
2175
|
+
material,
|
|
2176
|
+
uvRect,
|
|
2177
|
+
className,
|
|
2178
|
+
style: styleProp,
|
|
2179
|
+
domAttrs,
|
|
2180
|
+
domEventHandlers,
|
|
2181
|
+
pointerEvents = "auto"
|
|
2182
|
+
}) {
|
|
2183
|
+
const { u0, u1, v0, v1 } = uvRect;
|
|
2184
|
+
const du = u1 - u0;
|
|
2185
|
+
const dv = v1 - v0;
|
|
2186
|
+
const sourceW = plan.canvasW / du;
|
|
2187
|
+
const sourceH = plan.canvasH / dv;
|
|
2188
|
+
const vMax = Math.max(v0, v1);
|
|
2189
|
+
const offsetX = u0 * sourceW;
|
|
2190
|
+
const offsetY = (1 - vMax) * sourceH;
|
|
2191
|
+
const style = {
|
|
2192
|
+
width: plan.canvasW,
|
|
2193
|
+
height: plan.canvasH,
|
|
2194
|
+
transform: `matrix3d(${plan.matrix})`,
|
|
2195
|
+
backgroundImage: `url(${material.texture})`,
|
|
2196
|
+
backgroundSize: `${sourceW}px ${sourceH}px`,
|
|
2197
|
+
backgroundPosition: `-${offsetX}px -${offsetY}px`,
|
|
2198
|
+
pointerEvents: pointerEvents === "none" ? "none" : void 0,
|
|
2199
|
+
...styleProp
|
|
2200
|
+
};
|
|
2201
|
+
const dataAttrs = plan.polygon.data ? Object.fromEntries(
|
|
2202
|
+
Object.entries(plan.polygon.data).map(([k, v]) => [`data-${k}`, String(v)])
|
|
2203
|
+
) : {};
|
|
2204
|
+
const elementClassName = className?.trim() || void 0;
|
|
2205
|
+
return /* @__PURE__ */ jsx6(
|
|
2206
|
+
"i",
|
|
2207
|
+
{
|
|
2208
|
+
className: elementClassName,
|
|
2209
|
+
style,
|
|
2210
|
+
...domEventHandlers,
|
|
2211
|
+
...dataAttrs,
|
|
2212
|
+
...domAttrs
|
|
2213
|
+
}
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
function PolyInner({
|
|
2217
|
+
vertices,
|
|
2218
|
+
color,
|
|
2219
|
+
texture,
|
|
2220
|
+
uvs,
|
|
2221
|
+
data,
|
|
2222
|
+
material,
|
|
2223
|
+
position,
|
|
2224
|
+
scale,
|
|
2225
|
+
rotation,
|
|
2226
|
+
className,
|
|
2227
|
+
style: styleProp,
|
|
2228
|
+
id,
|
|
2229
|
+
onClick,
|
|
2230
|
+
onDoubleClick,
|
|
2231
|
+
onMouseEnter,
|
|
2232
|
+
onMouseLeave,
|
|
2233
|
+
onMouseMove,
|
|
2234
|
+
onPointerDown,
|
|
2235
|
+
onPointerUp,
|
|
2236
|
+
onPointerEnter,
|
|
2237
|
+
onPointerLeave,
|
|
2238
|
+
onFocus,
|
|
2239
|
+
onBlur,
|
|
2240
|
+
onKeyDown,
|
|
2241
|
+
tabIndex,
|
|
2242
|
+
role,
|
|
2243
|
+
"aria-label": ariaLabel,
|
|
2244
|
+
"aria-hidden": ariaHidden,
|
|
2245
|
+
pointerEvents: pointerEventsProp,
|
|
2246
|
+
context,
|
|
2247
|
+
textureLighting: textureLightingProp,
|
|
2248
|
+
atlasScale: atlasScaleProp,
|
|
2249
|
+
baseColor: baseColorProp,
|
|
2250
|
+
...dataAttrs
|
|
2251
|
+
}) {
|
|
2252
|
+
const tileSize = context?.tileSize ?? 50;
|
|
2253
|
+
const layerElevation = context?.layerElevation ?? tileSize;
|
|
2254
|
+
const textureLighting = textureLightingProp ?? context?.textureLighting ?? "baked";
|
|
2255
|
+
const atlasScale = atlasScaleProp ?? context?.atlasScale;
|
|
2256
|
+
const polygonColor = baseColorProp ?? color;
|
|
2257
|
+
const effectiveTexture = material?.texture ?? texture;
|
|
2258
|
+
const atlasPlan = useMemo9(
|
|
2259
|
+
() => computeTextureAtlasPlan(
|
|
2260
|
+
{ vertices, color: polygonColor, texture: effectiveTexture, uvs, data },
|
|
2261
|
+
0,
|
|
2262
|
+
{
|
|
2263
|
+
tileSize,
|
|
2264
|
+
layerElevation,
|
|
2265
|
+
directionalLight: context?.directionalLight
|
|
2266
|
+
}
|
|
2267
|
+
),
|
|
2268
|
+
[
|
|
2269
|
+
vertices,
|
|
2270
|
+
polygonColor,
|
|
2271
|
+
effectiveTexture,
|
|
2272
|
+
uvs,
|
|
2273
|
+
data,
|
|
2274
|
+
tileSize,
|
|
2275
|
+
layerElevation,
|
|
2276
|
+
context?.directionalLight
|
|
2277
|
+
]
|
|
2278
|
+
);
|
|
2279
|
+
const materialUvRect = useMemo9(
|
|
2280
|
+
() => material && uvs ? isAxisAlignedRectUVs(uvs) : null,
|
|
2281
|
+
[material, uvs]
|
|
2282
|
+
);
|
|
2283
|
+
const atlasPlans = useMemo9(
|
|
2284
|
+
() => materialUvRect ? [] : [atlasPlan],
|
|
2285
|
+
[materialUvRect, atlasPlan]
|
|
2286
|
+
);
|
|
2287
|
+
const textureAtlas = useTextureAtlas(atlasPlans, textureLighting, atlasScale);
|
|
2288
|
+
const domEventHandlers = {
|
|
2289
|
+
onClick,
|
|
2290
|
+
onDoubleClick,
|
|
2291
|
+
onMouseEnter,
|
|
2292
|
+
onMouseLeave,
|
|
2293
|
+
onMouseMove,
|
|
2294
|
+
onPointerDown,
|
|
2295
|
+
onPointerUp,
|
|
2296
|
+
onPointerEnter,
|
|
2297
|
+
onPointerLeave,
|
|
2298
|
+
onFocus,
|
|
2299
|
+
onBlur,
|
|
2300
|
+
onKeyDown
|
|
2301
|
+
};
|
|
2302
|
+
const domAttrs = {
|
|
2303
|
+
id,
|
|
2304
|
+
tabIndex,
|
|
2305
|
+
role,
|
|
2306
|
+
"aria-label": ariaLabel,
|
|
2307
|
+
"aria-hidden": ariaHidden,
|
|
2308
|
+
...Object.fromEntries(
|
|
2309
|
+
Object.entries(dataAttrs).filter(([k]) => k.startsWith("data-"))
|
|
2310
|
+
),
|
|
2311
|
+
...data ? Object.fromEntries(
|
|
2312
|
+
Object.entries(data).map(([k, v]) => [`data-${k}`, String(v)])
|
|
2313
|
+
) : {}
|
|
2314
|
+
};
|
|
2315
|
+
const transformParts = [];
|
|
2316
|
+
if (position) {
|
|
2317
|
+
transformParts.push(
|
|
2318
|
+
`translate3d(${position[0]}px, ${position[1]}px, ${position[2]}px)`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
if (scale !== void 0) {
|
|
2322
|
+
if (typeof scale === "number") {
|
|
2323
|
+
if (scale !== 1) transformParts.push(`scale3d(${scale}, ${scale}, ${scale})`);
|
|
2324
|
+
} else {
|
|
2325
|
+
transformParts.push(`scale3d(${scale[0]}, ${scale[1]}, ${scale[2]})`);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
if (rotation) {
|
|
2329
|
+
if (rotation[0]) transformParts.push(`rotateX(${rotation[0]}deg)`);
|
|
2330
|
+
if (rotation[1]) transformParts.push(`rotateY(${rotation[1]}deg)`);
|
|
2331
|
+
if (rotation[2]) transformParts.push(`rotateZ(${rotation[2]}deg)`);
|
|
2332
|
+
}
|
|
2333
|
+
const wrapperTransform = transformParts.length > 0 ? transformParts.join(" ") : void 0;
|
|
2334
|
+
let front = null;
|
|
2335
|
+
if (materialUvRect && material && atlasPlan) {
|
|
2336
|
+
front = /* @__PURE__ */ jsx6(
|
|
2337
|
+
MaterialDirectPoly,
|
|
2338
|
+
{
|
|
2339
|
+
plan: atlasPlan,
|
|
2340
|
+
material,
|
|
2341
|
+
uvRect: materialUvRect,
|
|
2342
|
+
className,
|
|
2343
|
+
style: styleProp,
|
|
2344
|
+
domAttrs,
|
|
2345
|
+
domEventHandlers,
|
|
2346
|
+
pointerEvents: pointerEventsProp ?? "auto"
|
|
2347
|
+
}
|
|
2348
|
+
);
|
|
2349
|
+
} else {
|
|
2350
|
+
const atlasEntry = textureAtlas.entries[0];
|
|
2351
|
+
if (atlasEntry) {
|
|
2352
|
+
front = /* @__PURE__ */ jsx6(
|
|
2353
|
+
TextureAtlasPoly,
|
|
2354
|
+
{
|
|
2355
|
+
entry: atlasEntry,
|
|
2356
|
+
page: textureAtlas.pages[atlasEntry.pageIndex],
|
|
2357
|
+
textureLighting,
|
|
2358
|
+
className,
|
|
2359
|
+
style: styleProp,
|
|
2360
|
+
domAttrs,
|
|
2361
|
+
domEventHandlers,
|
|
2362
|
+
pointerEvents: pointerEventsProp ?? "auto"
|
|
2363
|
+
}
|
|
2364
|
+
);
|
|
2365
|
+
} else if (atlasPlan && !atlasPlan.texture) {
|
|
2366
|
+
front = isSolidTrianglePlan(atlasPlan) ? /* @__PURE__ */ jsx6(
|
|
2367
|
+
TextureTrianglePoly,
|
|
2368
|
+
{
|
|
2369
|
+
entry: atlasPlan,
|
|
2370
|
+
textureLighting,
|
|
2371
|
+
className,
|
|
2372
|
+
style: styleProp,
|
|
2373
|
+
domAttrs,
|
|
2374
|
+
domEventHandlers,
|
|
2375
|
+
pointerEvents: pointerEventsProp ?? "auto"
|
|
2376
|
+
}
|
|
2377
|
+
) : /* @__PURE__ */ jsx6(
|
|
2378
|
+
TextureBorderShapePoly,
|
|
2379
|
+
{
|
|
2380
|
+
entry: atlasPlan,
|
|
2381
|
+
className,
|
|
2382
|
+
style: styleProp,
|
|
2383
|
+
domAttrs,
|
|
2384
|
+
domEventHandlers,
|
|
2385
|
+
pointerEvents: pointerEventsProp ?? "auto"
|
|
2386
|
+
}
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
if (!front) return null;
|
|
2391
|
+
if (!wrapperTransform) return front;
|
|
2392
|
+
return /* @__PURE__ */ jsx6(
|
|
2393
|
+
"div",
|
|
2394
|
+
{
|
|
2395
|
+
className: "polycss-poly-wrapper",
|
|
2396
|
+
style: {
|
|
2397
|
+
position: "absolute",
|
|
2398
|
+
transformStyle: "preserve-3d",
|
|
2399
|
+
transform: wrapperTransform
|
|
2400
|
+
},
|
|
2401
|
+
children: front
|
|
2402
|
+
}
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
var Poly = memo4(PolyInner);
|
|
2406
|
+
|
|
2407
|
+
// src/controls/PolyOrbitControls.tsx
|
|
2408
|
+
import { useEffect as useEffect6, useRef as useRef6 } from "react";
|
|
2409
|
+
|
|
2410
|
+
// src/controls/sharedControls.ts
|
|
2411
|
+
import { BASE_TILE as BASE_TILE2 } from "@layoutit/polycss-core";
|
|
2412
|
+
var POINTER_DRAG_SPEED = 4;
|
|
2413
|
+
function invertFactor(invert) {
|
|
2414
|
+
if (invert === true) return -1;
|
|
2415
|
+
if (invert === void 0 || invert === false) return 1;
|
|
2416
|
+
return invert;
|
|
2417
|
+
}
|
|
2418
|
+
function applyOrbit(dx, dy, s, handle, invert) {
|
|
2419
|
+
const f = invertFactor(invert);
|
|
2420
|
+
const dX = dx / POINTER_DRAG_SPEED * f;
|
|
2421
|
+
const dY = dy / POINTER_DRAG_SPEED * f;
|
|
2422
|
+
const rotX = Math.max(0, Math.min(100, s.rotX - dY));
|
|
2423
|
+
const rotY = ((s.rotY - dX) % 360 + 360) % 360;
|
|
2424
|
+
handle.update({ rotX, rotY });
|
|
2425
|
+
}
|
|
2426
|
+
function applyPan(dx, dy, s, handle, _invert) {
|
|
2427
|
+
const z = Math.max(0.01, s.zoom);
|
|
2428
|
+
const cosRotXRaw = Math.cos(s.rotX * Math.PI / 180);
|
|
2429
|
+
const cosRotX = cosRotXRaw >= 0 ? Math.max(0.1, cosRotXRaw) : Math.min(-0.1, cosRotXRaw);
|
|
2430
|
+
const cZ = Math.cos(s.rotY * Math.PI / 180);
|
|
2431
|
+
const sZ = Math.sin(s.rotY * Math.PI / 180);
|
|
2432
|
+
const k = z * BASE_TILE2;
|
|
2433
|
+
const targetD0 = (dx * sZ - dy * cZ / cosRotX) / k;
|
|
2434
|
+
const targetD1 = -(dx * cZ + dy * sZ / cosRotX) / k;
|
|
2435
|
+
const t = s.target;
|
|
2436
|
+
handle.update({ target: [t[0] + targetD0, t[1] + targetD1, t[2]] });
|
|
2437
|
+
}
|
|
2438
|
+
var buildOrbitControls = {
|
|
2439
|
+
applyOrbit,
|
|
2440
|
+
applyPan,
|
|
2441
|
+
invertFactor
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
// src/controls/controlsEffects.ts
|
|
2445
|
+
var WHEEL_IDLE_END_MS = 150;
|
|
2446
|
+
var ZOOM_STEP = 513e-6;
|
|
2447
|
+
var PINCH_AMP = 10;
|
|
2448
|
+
var SCROLL_AMP = 3;
|
|
2449
|
+
var ANIM_FRAME_MS = 16.67;
|
|
2450
|
+
var ANIM_DT_CLAMP_MS = 50;
|
|
2451
|
+
var DEFAULT_ANIMATE_SPEED = 0.3;
|
|
2452
|
+
var DOLLY_STEP = 0.05;
|
|
2453
|
+
function makeWheelEffect({
|
|
2454
|
+
wheel,
|
|
2455
|
+
dollyRef,
|
|
2456
|
+
wheelRef,
|
|
2457
|
+
zoomMinRef,
|
|
2458
|
+
zoomMaxRef,
|
|
2459
|
+
distanceMinRef,
|
|
2460
|
+
distanceMaxRef,
|
|
2461
|
+
cameraElRef,
|
|
2462
|
+
cameraRef,
|
|
2463
|
+
applyTransformDirect,
|
|
2464
|
+
store,
|
|
2465
|
+
fireStart,
|
|
2466
|
+
fireChange,
|
|
2467
|
+
fireEnd
|
|
2468
|
+
}) {
|
|
2469
|
+
if (!wheel) return;
|
|
2470
|
+
const el = cameraElRef.current;
|
|
2471
|
+
if (!el) return;
|
|
2472
|
+
let wheelActive = false;
|
|
2473
|
+
let wheelIdleTimer = null;
|
|
2474
|
+
const onWheel = (e) => {
|
|
2475
|
+
if (!wheelRef.current) return;
|
|
2476
|
+
e.preventDefault();
|
|
2477
|
+
const lineFactor = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1;
|
|
2478
|
+
let delta = e.deltaY * lineFactor;
|
|
2479
|
+
if (e.ctrlKey) delta *= PINCH_AMP;
|
|
2480
|
+
else delta *= SCROLL_AMP;
|
|
2481
|
+
const handle = cameraRef.current;
|
|
2482
|
+
if (dollyRef.current) {
|
|
2483
|
+
const nextDist = Math.max(
|
|
2484
|
+
distanceMinRef.current,
|
|
2485
|
+
Math.min(distanceMaxRef.current, handle.state.distance + delta * DOLLY_STEP)
|
|
2486
|
+
);
|
|
2487
|
+
handle.update({ distance: nextDist });
|
|
2488
|
+
} else {
|
|
2489
|
+
const factor = Math.exp(-delta * ZOOM_STEP);
|
|
2490
|
+
const next = Math.max(
|
|
2491
|
+
zoomMinRef.current,
|
|
2492
|
+
Math.min(zoomMaxRef.current, handle.state.zoom * factor)
|
|
2493
|
+
);
|
|
2494
|
+
handle.update({ zoom: next });
|
|
2495
|
+
}
|
|
2496
|
+
applyTransformDirect();
|
|
2497
|
+
store.updateCameraFromRef(handle);
|
|
2498
|
+
if (!wheelActive) {
|
|
2499
|
+
wheelActive = true;
|
|
2500
|
+
fireStart();
|
|
2501
|
+
}
|
|
2502
|
+
fireChange();
|
|
2503
|
+
if (wheelIdleTimer !== null) clearTimeout(wheelIdleTimer);
|
|
2504
|
+
wheelIdleTimer = setTimeout(() => {
|
|
2505
|
+
wheelIdleTimer = null;
|
|
2506
|
+
wheelActive = false;
|
|
2507
|
+
fireEnd();
|
|
2508
|
+
}, WHEEL_IDLE_END_MS);
|
|
2509
|
+
};
|
|
2510
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
2511
|
+
return () => {
|
|
2512
|
+
el.removeEventListener("wheel", onWheel);
|
|
2513
|
+
if (wheelIdleTimer !== null) clearTimeout(wheelIdleTimer);
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
function makeAnimateEffect({
|
|
2517
|
+
animateOn,
|
|
2518
|
+
animateRef,
|
|
2519
|
+
animationPausedShared,
|
|
2520
|
+
applyTransformDirect,
|
|
2521
|
+
cameraRef,
|
|
2522
|
+
store,
|
|
2523
|
+
fireChange
|
|
2524
|
+
}) {
|
|
2525
|
+
if (!animateOn) return;
|
|
2526
|
+
let rafId = null;
|
|
2527
|
+
let stopped = false;
|
|
2528
|
+
let lastTime = 0;
|
|
2529
|
+
const tick = (now) => {
|
|
2530
|
+
if (stopped) return;
|
|
2531
|
+
const a = animateRef.current;
|
|
2532
|
+
if (!a) {
|
|
2533
|
+
rafId = requestAnimationFrame(tick);
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
if (!animationPausedShared.value) {
|
|
2537
|
+
const dt = Math.min(ANIM_DT_CLAMP_MS, lastTime ? now - lastTime : ANIM_FRAME_MS);
|
|
2538
|
+
lastTime = now;
|
|
2539
|
+
const speed = a.speed ?? DEFAULT_ANIMATE_SPEED;
|
|
2540
|
+
const delta = speed * (dt / ANIM_FRAME_MS);
|
|
2541
|
+
const handle = cameraRef.current;
|
|
2542
|
+
const s = handle.state;
|
|
2543
|
+
if (a.axis === "x") {
|
|
2544
|
+
const rotX = ((s.rotX + delta) % 360 + 360) % 360;
|
|
2545
|
+
handle.update({ rotX });
|
|
2546
|
+
} else {
|
|
2547
|
+
const rotY = ((s.rotY + delta) % 360 + 360) % 360;
|
|
2548
|
+
handle.update({ rotY });
|
|
2549
|
+
}
|
|
2550
|
+
applyTransformDirect();
|
|
2551
|
+
store.updateCameraFromRef(handle);
|
|
2552
|
+
fireChange();
|
|
2553
|
+
} else {
|
|
2554
|
+
lastTime = now;
|
|
2555
|
+
}
|
|
2556
|
+
rafId = requestAnimationFrame(tick);
|
|
2557
|
+
};
|
|
2558
|
+
rafId = requestAnimationFrame(tick);
|
|
2559
|
+
return () => {
|
|
2560
|
+
stopped = true;
|
|
2561
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// src/controls/PolyOrbitControls.tsx
|
|
2566
|
+
function PolyOrbitControls({
|
|
2567
|
+
drag = true,
|
|
2568
|
+
wheel = true,
|
|
2569
|
+
dolly = false,
|
|
2570
|
+
invert = false,
|
|
2571
|
+
minZoom = 0.1,
|
|
2572
|
+
maxZoom = 10,
|
|
2573
|
+
minDistance = 0,
|
|
2574
|
+
maxDistance = 5e3,
|
|
2575
|
+
animate = false,
|
|
2576
|
+
onChange,
|
|
2577
|
+
onInteractionStart,
|
|
2578
|
+
onInteractionEnd
|
|
2579
|
+
}) {
|
|
2580
|
+
const { store, cameraRef, cameraElRef, applyTransformDirect } = useCameraContext();
|
|
2581
|
+
const dragRef = useRef6(drag);
|
|
2582
|
+
const wheelRef = useRef6(wheel);
|
|
2583
|
+
const dollyRef = useRef6(dolly);
|
|
2584
|
+
const invertRef = useRef6(invert);
|
|
2585
|
+
const zoomMinRef = useRef6(minZoom);
|
|
2586
|
+
const zoomMaxRef = useRef6(maxZoom);
|
|
2587
|
+
const distanceMinRef = useRef6(minDistance);
|
|
2588
|
+
const distanceMaxRef = useRef6(maxDistance);
|
|
2589
|
+
const animateRef = useRef6(animate);
|
|
2590
|
+
const onChangeRef = useRef6(onChange);
|
|
2591
|
+
const onInteractionStartRef = useRef6(onInteractionStart);
|
|
2592
|
+
const onInteractionEndRef = useRef6(onInteractionEnd);
|
|
2593
|
+
useEffect6(() => {
|
|
2594
|
+
dragRef.current = drag;
|
|
2595
|
+
wheelRef.current = wheel;
|
|
2596
|
+
dollyRef.current = dolly;
|
|
2597
|
+
invertRef.current = invert;
|
|
2598
|
+
zoomMinRef.current = minZoom;
|
|
2599
|
+
zoomMaxRef.current = maxZoom;
|
|
2600
|
+
distanceMinRef.current = minDistance;
|
|
2601
|
+
distanceMaxRef.current = maxDistance;
|
|
2602
|
+
animateRef.current = animate;
|
|
2603
|
+
onChangeRef.current = onChange;
|
|
2604
|
+
onInteractionStartRef.current = onInteractionStart;
|
|
2605
|
+
onInteractionEndRef.current = onInteractionEnd;
|
|
2606
|
+
});
|
|
2607
|
+
const cameraSnapshot = () => {
|
|
2608
|
+
const s = cameraRef.current.state;
|
|
2609
|
+
return { rotX: s.rotX, rotY: s.rotY, zoom: s.zoom, target: s.target, distance: s.distance };
|
|
2610
|
+
};
|
|
2611
|
+
const fireChange = () => {
|
|
2612
|
+
const fn = onChangeRef.current;
|
|
2613
|
+
if (!fn) return;
|
|
2614
|
+
try {
|
|
2615
|
+
fn(cameraSnapshot());
|
|
2616
|
+
} catch (err) {
|
|
2617
|
+
console.error("[polycss/react] PolyOrbitControls onChange threw:", err);
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
const fireStart = () => {
|
|
2621
|
+
const fn = onInteractionStartRef.current;
|
|
2622
|
+
if (!fn) return;
|
|
2623
|
+
try {
|
|
2624
|
+
fn(cameraSnapshot());
|
|
2625
|
+
} catch (err) {
|
|
2626
|
+
console.error("[polycss/react] PolyOrbitControls onInteractionStart threw:", err);
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
const fireEnd = () => {
|
|
2630
|
+
const fn = onInteractionEndRef.current;
|
|
2631
|
+
if (!fn) return;
|
|
2632
|
+
try {
|
|
2633
|
+
fn(cameraSnapshot());
|
|
2634
|
+
} catch (err) {
|
|
2635
|
+
console.error("[polycss/react] PolyOrbitControls onInteractionEnd threw:", err);
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
const animationPausedShared = useRef6({ value: false }).current;
|
|
2639
|
+
useEffect6(() => {
|
|
2640
|
+
if (!drag) return;
|
|
2641
|
+
const el = cameraElRef.current;
|
|
2642
|
+
if (!el) return;
|
|
2643
|
+
let activePointerId = null;
|
|
2644
|
+
let pointer = { x: 0, y: 0 };
|
|
2645
|
+
let animationPaused = false;
|
|
2646
|
+
let rightDragActive = false;
|
|
2647
|
+
let rightPointer = { x: 0, y: 0 };
|
|
2648
|
+
const onDown = (e) => {
|
|
2649
|
+
if (!dragRef.current) return;
|
|
2650
|
+
if (activePointerId !== null) return;
|
|
2651
|
+
if (e.isPrimary === false) return;
|
|
2652
|
+
e.preventDefault();
|
|
2653
|
+
activePointerId = e.pointerId;
|
|
2654
|
+
pointer = { x: e.clientX, y: e.clientY };
|
|
2655
|
+
el.style.cursor = "grabbing";
|
|
2656
|
+
try {
|
|
2657
|
+
e.target.setPointerCapture(e.pointerId);
|
|
2658
|
+
} catch {
|
|
2659
|
+
}
|
|
2660
|
+
const a = animateRef.current;
|
|
2661
|
+
if (a && a.pauseOnInteraction !== false) {
|
|
2662
|
+
animationPaused = true;
|
|
2663
|
+
animationPausedShared.value = true;
|
|
2664
|
+
}
|
|
2665
|
+
fireStart();
|
|
2666
|
+
};
|
|
2667
|
+
const onMove = (e) => {
|
|
2668
|
+
if (activePointerId === null || e.pointerId !== activePointerId) return;
|
|
2669
|
+
if (!dragRef.current) return;
|
|
2670
|
+
e.preventDefault();
|
|
2671
|
+
const dx = e.clientX - pointer.x;
|
|
2672
|
+
const dy = e.clientY - pointer.y;
|
|
2673
|
+
pointer = { x: e.clientX, y: e.clientY };
|
|
2674
|
+
const handle = cameraRef.current;
|
|
2675
|
+
if (e.shiftKey) {
|
|
2676
|
+
buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
|
|
2677
|
+
} else {
|
|
2678
|
+
buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
|
|
2679
|
+
}
|
|
2680
|
+
applyTransformDirect();
|
|
2681
|
+
store.updateCameraFromRef(handle);
|
|
2682
|
+
fireChange();
|
|
2683
|
+
};
|
|
2684
|
+
const onUp = (e) => {
|
|
2685
|
+
if (activePointerId !== e.pointerId) return;
|
|
2686
|
+
activePointerId = null;
|
|
2687
|
+
el.style.cursor = dragRef.current ? "grab" : "";
|
|
2688
|
+
try {
|
|
2689
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
2690
|
+
} catch {
|
|
2691
|
+
}
|
|
2692
|
+
if (animationPaused) {
|
|
2693
|
+
animationPaused = false;
|
|
2694
|
+
animationPausedShared.value = false;
|
|
2695
|
+
}
|
|
2696
|
+
fireEnd();
|
|
2697
|
+
};
|
|
2698
|
+
const onContextMenu = (e) => {
|
|
2699
|
+
e.preventDefault();
|
|
2700
|
+
};
|
|
2701
|
+
const onMouseDown = (e) => {
|
|
2702
|
+
if (e.button !== 2) return;
|
|
2703
|
+
rightDragActive = true;
|
|
2704
|
+
rightPointer = { x: e.clientX, y: e.clientY };
|
|
2705
|
+
};
|
|
2706
|
+
const onMouseMove = (e) => {
|
|
2707
|
+
if (!rightDragActive || !dragRef.current) return;
|
|
2708
|
+
const dx = e.clientX - rightPointer.x;
|
|
2709
|
+
const dy = e.clientY - rightPointer.y;
|
|
2710
|
+
rightPointer = { x: e.clientX, y: e.clientY };
|
|
2711
|
+
const handle = cameraRef.current;
|
|
2712
|
+
buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
|
|
2713
|
+
applyTransformDirect();
|
|
2714
|
+
store.updateCameraFromRef(handle);
|
|
2715
|
+
fireChange();
|
|
2716
|
+
};
|
|
2717
|
+
const onMouseUp = (e) => {
|
|
2718
|
+
if (e.button !== 2) return;
|
|
2719
|
+
if (rightDragActive) {
|
|
2720
|
+
rightDragActive = false;
|
|
2721
|
+
fireEnd();
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
el.style.cursor = "grab";
|
|
2725
|
+
el.style.touchAction = "none";
|
|
2726
|
+
el.style.userSelect = "none";
|
|
2727
|
+
el.addEventListener("pointerdown", onDown);
|
|
2728
|
+
el.addEventListener("pointermove", onMove);
|
|
2729
|
+
el.addEventListener("pointerup", onUp);
|
|
2730
|
+
el.addEventListener("pointercancel", onUp);
|
|
2731
|
+
el.addEventListener("contextmenu", onContextMenu);
|
|
2732
|
+
el.addEventListener("mousedown", onMouseDown);
|
|
2733
|
+
el.addEventListener("mousemove", onMouseMove);
|
|
2734
|
+
el.addEventListener("mouseup", onMouseUp);
|
|
2735
|
+
return () => {
|
|
2736
|
+
el.removeEventListener("pointerdown", onDown);
|
|
2737
|
+
el.removeEventListener("pointermove", onMove);
|
|
2738
|
+
el.removeEventListener("pointerup", onUp);
|
|
2739
|
+
el.removeEventListener("pointercancel", onUp);
|
|
2740
|
+
el.removeEventListener("contextmenu", onContextMenu);
|
|
2741
|
+
el.removeEventListener("mousedown", onMouseDown);
|
|
2742
|
+
el.removeEventListener("mousemove", onMouseMove);
|
|
2743
|
+
el.removeEventListener("mouseup", onMouseUp);
|
|
2744
|
+
el.style.cursor = "";
|
|
2745
|
+
el.style.touchAction = "";
|
|
2746
|
+
el.style.userSelect = "";
|
|
2747
|
+
};
|
|
2748
|
+
}, [drag, applyTransformDirect, cameraElRef, cameraRef, store]);
|
|
2749
|
+
useEffect6(
|
|
2750
|
+
() => makeWheelEffect({ wheel, dollyRef, wheelRef, zoomMinRef, zoomMaxRef, distanceMinRef, distanceMaxRef, cameraElRef, cameraRef, applyTransformDirect, store, fireStart, fireChange, fireEnd }),
|
|
2751
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2752
|
+
[wheel, applyTransformDirect, cameraElRef, cameraRef, store]
|
|
2753
|
+
);
|
|
2754
|
+
useEffect6(
|
|
2755
|
+
() => makeAnimateEffect({ animateOn: !!animate, animateRef, animationPausedShared, applyTransformDirect, cameraRef, store, fireChange }),
|
|
2756
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2757
|
+
[!!animate, animationPausedShared, applyTransformDirect, cameraRef, store]
|
|
2758
|
+
);
|
|
2759
|
+
return null;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// src/controls/PolyMapControls.tsx
|
|
2763
|
+
import { useEffect as useEffect7, useRef as useRef7 } from "react";
|
|
2764
|
+
function PolyMapControls({
|
|
2765
|
+
drag = true,
|
|
2766
|
+
wheel = true,
|
|
2767
|
+
dolly = false,
|
|
2768
|
+
invert = false,
|
|
2769
|
+
minZoom = 0.1,
|
|
2770
|
+
maxZoom = 10,
|
|
2771
|
+
minDistance = 0,
|
|
2772
|
+
maxDistance = 5e3,
|
|
2773
|
+
animate = false,
|
|
2774
|
+
onChange,
|
|
2775
|
+
onInteractionStart,
|
|
2776
|
+
onInteractionEnd
|
|
2777
|
+
}) {
|
|
2778
|
+
const { store, cameraRef, cameraElRef, applyTransformDirect } = useCameraContext();
|
|
2779
|
+
const dragRef = useRef7(drag);
|
|
2780
|
+
const wheelRef = useRef7(wheel);
|
|
2781
|
+
const dollyRef = useRef7(dolly);
|
|
2782
|
+
const invertRef = useRef7(invert);
|
|
2783
|
+
const zoomMinRef = useRef7(minZoom);
|
|
2784
|
+
const zoomMaxRef = useRef7(maxZoom);
|
|
2785
|
+
const distanceMinRef = useRef7(minDistance);
|
|
2786
|
+
const distanceMaxRef = useRef7(maxDistance);
|
|
2787
|
+
const animateRef = useRef7(animate);
|
|
2788
|
+
const onChangeRef = useRef7(onChange);
|
|
2789
|
+
const onInteractionStartRef = useRef7(onInteractionStart);
|
|
2790
|
+
const onInteractionEndRef = useRef7(onInteractionEnd);
|
|
2791
|
+
useEffect7(() => {
|
|
2792
|
+
dragRef.current = drag;
|
|
2793
|
+
wheelRef.current = wheel;
|
|
2794
|
+
dollyRef.current = dolly;
|
|
2795
|
+
invertRef.current = invert;
|
|
2796
|
+
zoomMinRef.current = minZoom;
|
|
2797
|
+
zoomMaxRef.current = maxZoom;
|
|
2798
|
+
distanceMinRef.current = minDistance;
|
|
2799
|
+
distanceMaxRef.current = maxDistance;
|
|
2800
|
+
animateRef.current = animate;
|
|
2801
|
+
onChangeRef.current = onChange;
|
|
2802
|
+
onInteractionStartRef.current = onInteractionStart;
|
|
2803
|
+
onInteractionEndRef.current = onInteractionEnd;
|
|
2804
|
+
});
|
|
2805
|
+
const cameraSnapshot = () => {
|
|
2806
|
+
const s = cameraRef.current.state;
|
|
2807
|
+
return { rotX: s.rotX, rotY: s.rotY, zoom: s.zoom, target: s.target, distance: s.distance };
|
|
2808
|
+
};
|
|
2809
|
+
const fireChange = () => {
|
|
2810
|
+
const fn = onChangeRef.current;
|
|
2811
|
+
if (!fn) return;
|
|
2812
|
+
try {
|
|
2813
|
+
fn(cameraSnapshot());
|
|
2814
|
+
} catch (err) {
|
|
2815
|
+
console.error("[polycss/react] PolyMapControls onChange threw:", err);
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
const fireStart = () => {
|
|
2819
|
+
const fn = onInteractionStartRef.current;
|
|
2820
|
+
if (!fn) return;
|
|
2821
|
+
try {
|
|
2822
|
+
fn(cameraSnapshot());
|
|
2823
|
+
} catch (err) {
|
|
2824
|
+
console.error("[polycss/react] PolyMapControls onInteractionStart threw:", err);
|
|
2825
|
+
}
|
|
2826
|
+
};
|
|
2827
|
+
const fireEnd = () => {
|
|
2828
|
+
const fn = onInteractionEndRef.current;
|
|
2829
|
+
if (!fn) return;
|
|
2830
|
+
try {
|
|
2831
|
+
fn(cameraSnapshot());
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
console.error("[polycss/react] PolyMapControls onInteractionEnd threw:", err);
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
const animationPausedShared = useRef7({ value: false }).current;
|
|
2837
|
+
useEffect7(() => {
|
|
2838
|
+
if (!drag) return;
|
|
2839
|
+
const el = cameraElRef.current;
|
|
2840
|
+
if (!el) return;
|
|
2841
|
+
let activePointerId = null;
|
|
2842
|
+
let pointer = { x: 0, y: 0 };
|
|
2843
|
+
let animationPaused = false;
|
|
2844
|
+
let rightDragActive = false;
|
|
2845
|
+
let rightPointer = { x: 0, y: 0 };
|
|
2846
|
+
const onDown = (e) => {
|
|
2847
|
+
if (!dragRef.current) return;
|
|
2848
|
+
if (activePointerId !== null) return;
|
|
2849
|
+
if (e.isPrimary === false) return;
|
|
2850
|
+
e.preventDefault();
|
|
2851
|
+
activePointerId = e.pointerId;
|
|
2852
|
+
pointer = { x: e.clientX, y: e.clientY };
|
|
2853
|
+
el.style.cursor = "grabbing";
|
|
2854
|
+
try {
|
|
2855
|
+
e.target.setPointerCapture(e.pointerId);
|
|
2856
|
+
} catch {
|
|
2857
|
+
}
|
|
2858
|
+
const a = animateRef.current;
|
|
2859
|
+
if (a && a.pauseOnInteraction !== false) {
|
|
2860
|
+
animationPaused = true;
|
|
2861
|
+
animationPausedShared.value = true;
|
|
2862
|
+
}
|
|
2863
|
+
fireStart();
|
|
2864
|
+
};
|
|
2865
|
+
const onMove = (e) => {
|
|
2866
|
+
if (activePointerId === null || e.pointerId !== activePointerId) return;
|
|
2867
|
+
if (!dragRef.current) return;
|
|
2868
|
+
e.preventDefault();
|
|
2869
|
+
const dx = e.clientX - pointer.x;
|
|
2870
|
+
const dy = e.clientY - pointer.y;
|
|
2871
|
+
pointer = { x: e.clientX, y: e.clientY };
|
|
2872
|
+
const handle = cameraRef.current;
|
|
2873
|
+
if (e.shiftKey) {
|
|
2874
|
+
buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
|
|
2875
|
+
} else {
|
|
2876
|
+
buildOrbitControls.applyPan(dx, dy, handle.state, handle, invertRef.current);
|
|
2877
|
+
}
|
|
2878
|
+
applyTransformDirect();
|
|
2879
|
+
store.updateCameraFromRef(handle);
|
|
2880
|
+
fireChange();
|
|
2881
|
+
};
|
|
2882
|
+
const onUp = (e) => {
|
|
2883
|
+
if (activePointerId !== e.pointerId) return;
|
|
2884
|
+
activePointerId = null;
|
|
2885
|
+
el.style.cursor = dragRef.current ? "grab" : "";
|
|
2886
|
+
try {
|
|
2887
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
2888
|
+
} catch {
|
|
2889
|
+
}
|
|
2890
|
+
if (animationPaused) {
|
|
2891
|
+
animationPaused = false;
|
|
2892
|
+
animationPausedShared.value = false;
|
|
2893
|
+
}
|
|
2894
|
+
fireEnd();
|
|
2895
|
+
};
|
|
2896
|
+
const onContextMenu = (e) => {
|
|
2897
|
+
e.preventDefault();
|
|
2898
|
+
};
|
|
2899
|
+
const onMouseDown = (e) => {
|
|
2900
|
+
if (e.button !== 2) return;
|
|
2901
|
+
rightDragActive = true;
|
|
2902
|
+
rightPointer = { x: e.clientX, y: e.clientY };
|
|
2903
|
+
};
|
|
2904
|
+
const onMouseMove = (e) => {
|
|
2905
|
+
if (!rightDragActive || !dragRef.current) return;
|
|
2906
|
+
const dx = e.clientX - rightPointer.x;
|
|
2907
|
+
const dy = e.clientY - rightPointer.y;
|
|
2908
|
+
rightPointer = { x: e.clientX, y: e.clientY };
|
|
2909
|
+
const handle = cameraRef.current;
|
|
2910
|
+
buildOrbitControls.applyOrbit(dx, dy, handle.state, handle, invertRef.current);
|
|
2911
|
+
applyTransformDirect();
|
|
2912
|
+
store.updateCameraFromRef(handle);
|
|
2913
|
+
fireChange();
|
|
2914
|
+
};
|
|
2915
|
+
const onMouseUp = (e) => {
|
|
2916
|
+
if (e.button !== 2) return;
|
|
2917
|
+
if (rightDragActive) {
|
|
2918
|
+
rightDragActive = false;
|
|
2919
|
+
fireEnd();
|
|
2920
|
+
}
|
|
2921
|
+
};
|
|
2922
|
+
el.style.cursor = "grab";
|
|
2923
|
+
el.style.touchAction = "none";
|
|
2924
|
+
el.style.userSelect = "none";
|
|
2925
|
+
el.addEventListener("pointerdown", onDown);
|
|
2926
|
+
el.addEventListener("pointermove", onMove);
|
|
2927
|
+
el.addEventListener("pointerup", onUp);
|
|
2928
|
+
el.addEventListener("pointercancel", onUp);
|
|
2929
|
+
el.addEventListener("contextmenu", onContextMenu);
|
|
2930
|
+
el.addEventListener("mousedown", onMouseDown);
|
|
2931
|
+
el.addEventListener("mousemove", onMouseMove);
|
|
2932
|
+
el.addEventListener("mouseup", onMouseUp);
|
|
2933
|
+
return () => {
|
|
2934
|
+
el.removeEventListener("pointerdown", onDown);
|
|
2935
|
+
el.removeEventListener("pointermove", onMove);
|
|
2936
|
+
el.removeEventListener("pointerup", onUp);
|
|
2937
|
+
el.removeEventListener("pointercancel", onUp);
|
|
2938
|
+
el.removeEventListener("contextmenu", onContextMenu);
|
|
2939
|
+
el.removeEventListener("mousedown", onMouseDown);
|
|
2940
|
+
el.removeEventListener("mousemove", onMouseMove);
|
|
2941
|
+
el.removeEventListener("mouseup", onMouseUp);
|
|
2942
|
+
el.style.cursor = "";
|
|
2943
|
+
el.style.touchAction = "";
|
|
2944
|
+
el.style.userSelect = "";
|
|
2945
|
+
};
|
|
2946
|
+
}, [drag, applyTransformDirect, cameraElRef, cameraRef, store]);
|
|
2947
|
+
useEffect7(
|
|
2948
|
+
() => makeWheelEffect({ wheel, dollyRef, wheelRef, zoomMinRef, zoomMaxRef, distanceMinRef, distanceMaxRef, cameraElRef, cameraRef, applyTransformDirect, store, fireStart, fireChange, fireEnd }),
|
|
2949
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2950
|
+
[wheel, applyTransformDirect, cameraElRef, cameraRef, store]
|
|
2951
|
+
);
|
|
2952
|
+
useEffect7(
|
|
2953
|
+
() => makeAnimateEffect({ animateOn: !!animate, animateRef, animationPausedShared, applyTransformDirect, cameraRef, store, fireChange }),
|
|
2954
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2955
|
+
[!!animate, animationPausedShared, applyTransformDirect, cameraRef, store]
|
|
2956
|
+
);
|
|
2957
|
+
return null;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
// src/controls/TransformControls.tsx
|
|
2961
|
+
import {
|
|
2962
|
+
useCallback as useCallback7,
|
|
2963
|
+
useContext as useContext4,
|
|
2964
|
+
useEffect as useEffect8,
|
|
2965
|
+
useMemo as useMemo10,
|
|
2966
|
+
useRef as useRef8,
|
|
2967
|
+
useState as useState4
|
|
2968
|
+
} from "react";
|
|
2969
|
+
import { arrowPolygons, ringPolygons } from "@layoutit/polycss-core";
|
|
2970
|
+
import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2971
|
+
var COLOR_X = "#ff3653";
|
|
2972
|
+
var COLOR_Y = "#8adb00";
|
|
2973
|
+
var COLOR_Z = "#2c8fff";
|
|
2974
|
+
var ALPHA_IDLE = 0.6;
|
|
2975
|
+
var ALPHA_HOVER = 0.8;
|
|
2976
|
+
var ALPHA_DRAGGING = 1;
|
|
2977
|
+
function withAlpha(hex, alpha) {
|
|
2978
|
+
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
|
2979
|
+
if (!m) return hex;
|
|
2980
|
+
const r = parseInt(m[1], 16);
|
|
2981
|
+
const g = parseInt(m[2], 16);
|
|
2982
|
+
const b = parseInt(m[3], 16);
|
|
2983
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
2984
|
+
}
|
|
2985
|
+
var SCENE_TILE_SIZE = 50;
|
|
2986
|
+
var FALLBACK_SHAFT_LENGTH = 60;
|
|
2987
|
+
var SHAFT_LENGTH_RATIO = 0.6;
|
|
2988
|
+
var SHAFT_HALF_THICKNESS_RATIO = 0.0125;
|
|
2989
|
+
var HEAD_LENGTH_RATIO = 0.15;
|
|
2990
|
+
var HEAD_HALF_THICKNESS_RATIO = 0.04;
|
|
2991
|
+
var RING_RADIUS_RATIO = 1;
|
|
2992
|
+
var RING_HALF_THICKNESS_RATIO = 0.012;
|
|
2993
|
+
var RING_SEGMENTS = 64;
|
|
2994
|
+
var SCREEN_AXIS_DEAD_ZONE_SQ = 1e-4;
|
|
2995
|
+
function gizmoLengthForMesh(polygons) {
|
|
2996
|
+
if (polygons.length === 0) return FALLBACK_SHAFT_LENGTH;
|
|
2997
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
2998
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
2999
|
+
for (const poly of polygons) {
|
|
3000
|
+
for (const v of poly.vertices) {
|
|
3001
|
+
if (v[0] < minX) minX = v[0];
|
|
3002
|
+
if (v[0] > maxX) maxX = v[0];
|
|
3003
|
+
if (v[1] < minY) minY = v[1];
|
|
3004
|
+
if (v[1] > maxY) maxY = v[1];
|
|
3005
|
+
if (v[2] < minZ) minZ = v[2];
|
|
3006
|
+
if (v[2] > maxZ) maxZ = v[2];
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
if (!Number.isFinite(minX)) return FALLBACK_SHAFT_LENGTH;
|
|
3010
|
+
const extent = Math.max(maxX - minX, maxY - minY, maxZ - minZ);
|
|
3011
|
+
return extent * SCENE_TILE_SIZE * SHAFT_LENGTH_RATIO;
|
|
3012
|
+
}
|
|
3013
|
+
function gizmoCenterForMesh(polygons) {
|
|
3014
|
+
if (polygons.length === 0) return [0, 0, 0];
|
|
3015
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
3016
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
3017
|
+
for (const poly of polygons) {
|
|
3018
|
+
for (const v of poly.vertices) {
|
|
3019
|
+
if (v[0] < minX) minX = v[0];
|
|
3020
|
+
if (v[0] > maxX) maxX = v[0];
|
|
3021
|
+
if (v[1] < minY) minY = v[1];
|
|
3022
|
+
if (v[1] > maxY) maxY = v[1];
|
|
3023
|
+
if (v[2] < minZ) minZ = v[2];
|
|
3024
|
+
if (v[2] > maxZ) maxZ = v[2];
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
if (!Number.isFinite(minX)) return [0, 0, 0];
|
|
3028
|
+
return [
|
|
3029
|
+
(minY + maxY) / 2 * SCENE_TILE_SIZE,
|
|
3030
|
+
(minX + maxX) / 2 * SCENE_TILE_SIZE,
|
|
3031
|
+
(minZ + maxZ) / 2 * SCENE_TILE_SIZE
|
|
3032
|
+
];
|
|
3033
|
+
}
|
|
3034
|
+
function resolveObject(o) {
|
|
3035
|
+
if (o == null) return null;
|
|
3036
|
+
if (typeof o === "object" && "current" in o) return o.current ?? null;
|
|
3037
|
+
return o;
|
|
3038
|
+
}
|
|
3039
|
+
function snap(value, step) {
|
|
3040
|
+
if (!step || step <= 0) return value;
|
|
3041
|
+
return Math.round(value / step) * step;
|
|
3042
|
+
}
|
|
3043
|
+
var WORLD_AXIS_FOR_CSS = { 0: 1, 1: 0, 2: 2 };
|
|
3044
|
+
var ARROW_SPECS = [
|
|
3045
|
+
{ cssAxis: 0, sign: 1, key: "x", color: COLOR_X },
|
|
3046
|
+
{ cssAxis: 0, sign: -1, key: "-x", color: COLOR_X },
|
|
3047
|
+
{ cssAxis: 1, sign: 1, key: "y", color: COLOR_Y },
|
|
3048
|
+
{ cssAxis: 1, sign: -1, key: "-y", color: COLOR_Y },
|
|
3049
|
+
{ cssAxis: 2, sign: 1, key: "z", color: COLOR_Z },
|
|
3050
|
+
{ cssAxis: 2, sign: -1, key: "-z", color: COLOR_Z }
|
|
3051
|
+
];
|
|
3052
|
+
var RING_SPECS = [
|
|
3053
|
+
{ cssAxis: 0, key: "x", color: COLOR_X },
|
|
3054
|
+
{ cssAxis: 1, key: "y", color: COLOR_Y },
|
|
3055
|
+
{ cssAxis: 2, key: "z", color: COLOR_Z }
|
|
3056
|
+
];
|
|
3057
|
+
function userAxisLetterOf(key) {
|
|
3058
|
+
const last = key.replace("-", "")[0];
|
|
3059
|
+
return last;
|
|
3060
|
+
}
|
|
3061
|
+
function startAxisDrag(opts) {
|
|
3062
|
+
const {
|
|
3063
|
+
cssAxis,
|
|
3064
|
+
sign,
|
|
3065
|
+
shaftLengthCss,
|
|
3066
|
+
wrapper,
|
|
3067
|
+
target,
|
|
3068
|
+
startClientX,
|
|
3069
|
+
startClientY,
|
|
3070
|
+
translationSnap,
|
|
3071
|
+
onChange,
|
|
3072
|
+
onObjectChange,
|
|
3073
|
+
onMouseDown,
|
|
3074
|
+
onMouseUp,
|
|
3075
|
+
onDraggingChanged
|
|
3076
|
+
} = opts;
|
|
3077
|
+
const probeDistance = shaftLengthCss;
|
|
3078
|
+
const axisVec = [0, 0, 0];
|
|
3079
|
+
axisVec[cssAxis] = sign;
|
|
3080
|
+
const probe = document.createElement("div");
|
|
3081
|
+
probe.style.position = "absolute";
|
|
3082
|
+
probe.style.left = "0";
|
|
3083
|
+
probe.style.top = "0";
|
|
3084
|
+
probe.style.width = "0";
|
|
3085
|
+
probe.style.height = "0";
|
|
3086
|
+
probe.style.transform = `translate3d(${axisVec[0] * probeDistance}px, ${axisVec[1] * probeDistance}px, ${axisVec[2] * probeDistance}px)`;
|
|
3087
|
+
wrapper.appendChild(probe);
|
|
3088
|
+
const wRect = wrapper.getBoundingClientRect();
|
|
3089
|
+
const pRect = probe.getBoundingClientRect();
|
|
3090
|
+
wrapper.removeChild(probe);
|
|
3091
|
+
const screenAxisX = (pRect.left - wRect.left) / probeDistance;
|
|
3092
|
+
const screenAxisY = (pRect.top - wRect.top) / probeDistance;
|
|
3093
|
+
const screenAxisLenSq = screenAxisX * screenAxisX + screenAxisY * screenAxisY;
|
|
3094
|
+
if (screenAxisLenSq < SCREEN_AXIS_DEAD_ZONE_SQ) return;
|
|
3095
|
+
const startPos = target.getPosition() ?? [0, 0, 0];
|
|
3096
|
+
onMouseDown?.();
|
|
3097
|
+
onDraggingChanged?.(true);
|
|
3098
|
+
const handleMove = (ev) => {
|
|
3099
|
+
const dx = ev.clientX - startClientX;
|
|
3100
|
+
const dy = ev.clientY - startClientY;
|
|
3101
|
+
let t = (dx * screenAxisX + dy * screenAxisY) / screenAxisLenSq;
|
|
3102
|
+
t = snap(t, translationSnap);
|
|
3103
|
+
const newPos = [
|
|
3104
|
+
startPos[0] + t * axisVec[0],
|
|
3105
|
+
startPos[1] + t * axisVec[1],
|
|
3106
|
+
startPos[2] + t * axisVec[2]
|
|
3107
|
+
];
|
|
3108
|
+
onObjectChange?.({ object: target, position: newPos });
|
|
3109
|
+
onChange?.();
|
|
3110
|
+
};
|
|
3111
|
+
const handleUp = () => {
|
|
3112
|
+
window.removeEventListener("pointermove", handleMove);
|
|
3113
|
+
window.removeEventListener("pointerup", handleUp);
|
|
3114
|
+
window.removeEventListener("pointercancel", handleUp);
|
|
3115
|
+
const swallow = (e) => {
|
|
3116
|
+
e.stopPropagation();
|
|
3117
|
+
e.stopImmediatePropagation();
|
|
3118
|
+
};
|
|
3119
|
+
window.addEventListener("click", swallow, { capture: true, once: true });
|
|
3120
|
+
setTimeout(() => window.removeEventListener("click", swallow, true), 0);
|
|
3121
|
+
onMouseUp?.();
|
|
3122
|
+
onDraggingChanged?.(false);
|
|
3123
|
+
};
|
|
3124
|
+
window.addEventListener("pointermove", handleMove);
|
|
3125
|
+
window.addEventListener("pointerup", handleUp);
|
|
3126
|
+
window.addEventListener("pointercancel", handleUp);
|
|
3127
|
+
}
|
|
3128
|
+
function startRingDrag(opts) {
|
|
3129
|
+
const {
|
|
3130
|
+
cssAxis,
|
|
3131
|
+
wrapper,
|
|
3132
|
+
target,
|
|
3133
|
+
startClientX,
|
|
3134
|
+
startClientY,
|
|
3135
|
+
rotationSnap,
|
|
3136
|
+
onChange,
|
|
3137
|
+
onObjectChange,
|
|
3138
|
+
onMouseDown,
|
|
3139
|
+
onMouseUp,
|
|
3140
|
+
onDraggingChanged
|
|
3141
|
+
} = opts;
|
|
3142
|
+
const wRect = wrapper.getBoundingClientRect();
|
|
3143
|
+
const centerX = wRect.left;
|
|
3144
|
+
const centerY = wRect.top;
|
|
3145
|
+
let lastAngle = Math.atan2(startClientY - centerY, startClientX - centerX);
|
|
3146
|
+
let cumulative = 0;
|
|
3147
|
+
const startRotation = target.getRotation() ?? [0, 0, 0];
|
|
3148
|
+
onMouseDown?.();
|
|
3149
|
+
onDraggingChanged?.(true);
|
|
3150
|
+
const handleMove = (ev) => {
|
|
3151
|
+
const a = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
|
|
3152
|
+
let d = a - lastAngle;
|
|
3153
|
+
if (d > Math.PI) d -= 2 * Math.PI;
|
|
3154
|
+
else if (d < -Math.PI) d += 2 * Math.PI;
|
|
3155
|
+
cumulative += d;
|
|
3156
|
+
lastAngle = a;
|
|
3157
|
+
let degrees = cumulative * 180 / Math.PI;
|
|
3158
|
+
degrees = snap(degrees, rotationSnap);
|
|
3159
|
+
const newRotation = [
|
|
3160
|
+
startRotation[0],
|
|
3161
|
+
startRotation[1],
|
|
3162
|
+
startRotation[2]
|
|
3163
|
+
];
|
|
3164
|
+
newRotation[cssAxis] = startRotation[cssAxis] + degrees;
|
|
3165
|
+
onObjectChange?.({ object: target, rotation: newRotation });
|
|
3166
|
+
onChange?.();
|
|
3167
|
+
};
|
|
3168
|
+
const handleUp = () => {
|
|
3169
|
+
window.removeEventListener("pointermove", handleMove);
|
|
3170
|
+
window.removeEventListener("pointerup", handleUp);
|
|
3171
|
+
window.removeEventListener("pointercancel", handleUp);
|
|
3172
|
+
const swallow = (e) => {
|
|
3173
|
+
e.stopPropagation();
|
|
3174
|
+
e.stopImmediatePropagation();
|
|
3175
|
+
};
|
|
3176
|
+
window.addEventListener("click", swallow, { capture: true, once: true });
|
|
3177
|
+
setTimeout(() => window.removeEventListener("click", swallow, true), 0);
|
|
3178
|
+
onMouseUp?.();
|
|
3179
|
+
onDraggingChanged?.(false);
|
|
3180
|
+
target.rebakeAtlas();
|
|
3181
|
+
};
|
|
3182
|
+
window.addEventListener("pointermove", handleMove);
|
|
3183
|
+
window.addEventListener("pointerup", handleUp);
|
|
3184
|
+
window.addEventListener("pointercancel", handleUp);
|
|
3185
|
+
}
|
|
3186
|
+
function PolyTransformControls({
|
|
3187
|
+
object,
|
|
3188
|
+
mode = "translate",
|
|
3189
|
+
space = "world",
|
|
3190
|
+
size = 1,
|
|
3191
|
+
showX = true,
|
|
3192
|
+
showY = true,
|
|
3193
|
+
showZ = true,
|
|
3194
|
+
translationSnap = null,
|
|
3195
|
+
rotationSnap = null,
|
|
3196
|
+
enabled = true,
|
|
3197
|
+
onChange,
|
|
3198
|
+
onObjectChange,
|
|
3199
|
+
onMouseDown,
|
|
3200
|
+
onMouseUp,
|
|
3201
|
+
onDraggingChanged
|
|
3202
|
+
}) {
|
|
3203
|
+
const [, forceRender] = useState4(0);
|
|
3204
|
+
useEffect8(() => {
|
|
3205
|
+
forceRender((n) => n + 1);
|
|
3206
|
+
}, [object]);
|
|
3207
|
+
const [hoveredKey, setHoveredKey] = useState4(null);
|
|
3208
|
+
const [draggingKey, setDraggingKey] = useState4(null);
|
|
3209
|
+
const cameraCtx = useContext4(PolyCameraContext);
|
|
3210
|
+
const cameraElRef = cameraCtx?.cameraElRef;
|
|
3211
|
+
const dragRef = useRef8({
|
|
3212
|
+
target: null,
|
|
3213
|
+
mode: "translate",
|
|
3214
|
+
shaftLengthCss: 0,
|
|
3215
|
+
enabled: true,
|
|
3216
|
+
show: { x: true, y: true, z: true },
|
|
3217
|
+
translationSnap: null,
|
|
3218
|
+
rotationSnap: null
|
|
3219
|
+
});
|
|
3220
|
+
useEffect8(() => {
|
|
3221
|
+
const cameraEl = cameraElRef?.current;
|
|
3222
|
+
if (!cameraEl) return;
|
|
3223
|
+
const onPointerDown = (event) => {
|
|
3224
|
+
const state = dragRef.current;
|
|
3225
|
+
if (!state.target || !state.enabled) return;
|
|
3226
|
+
const targetEl = event.target;
|
|
3227
|
+
if (targetEl?.closest(".polycss-transform-gizmo")) return;
|
|
3228
|
+
if (state.mode === "translate") {
|
|
3229
|
+
for (const spec of ARROW_SPECS) {
|
|
3230
|
+
if (!state.show[userAxisLetterOf(spec.key)]) continue;
|
|
3231
|
+
const arrowEl = document.querySelector(
|
|
3232
|
+
`.polycss-transform-arrow--${spec.key}`
|
|
3233
|
+
);
|
|
3234
|
+
if (!arrowEl) continue;
|
|
3235
|
+
if (!pointInMeshElement(arrowEl, event.clientX, event.clientY)) continue;
|
|
3236
|
+
event.preventDefault();
|
|
3237
|
+
event.stopPropagation();
|
|
3238
|
+
const wrapper = arrowEl.closest(
|
|
3239
|
+
"[data-poly-transform-controls]"
|
|
3240
|
+
);
|
|
3241
|
+
if (!wrapper) return;
|
|
3242
|
+
setDraggingKey(spec.key);
|
|
3243
|
+
startAxisDrag({
|
|
3244
|
+
cssAxis: spec.cssAxis,
|
|
3245
|
+
sign: spec.sign,
|
|
3246
|
+
shaftLengthCss: state.shaftLengthCss,
|
|
3247
|
+
wrapper,
|
|
3248
|
+
target: state.target,
|
|
3249
|
+
startClientX: event.clientX,
|
|
3250
|
+
startClientY: event.clientY,
|
|
3251
|
+
translationSnap: state.translationSnap,
|
|
3252
|
+
onChange: state.onChange,
|
|
3253
|
+
onObjectChange: state.onObjectChange,
|
|
3254
|
+
onMouseDown: state.onMouseDown,
|
|
3255
|
+
onMouseUp: state.onMouseUp,
|
|
3256
|
+
onDraggingChanged: (d) => {
|
|
3257
|
+
if (!d) setDraggingKey(null);
|
|
3258
|
+
state.onDraggingChanged?.(d);
|
|
3259
|
+
}
|
|
3260
|
+
});
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
} else if (state.mode === "rotate") {
|
|
3264
|
+
for (const spec of RING_SPECS) {
|
|
3265
|
+
if (!state.show[spec.key]) continue;
|
|
3266
|
+
const ringEl = document.querySelector(
|
|
3267
|
+
`.polycss-transform-ring--${spec.key}`
|
|
3268
|
+
);
|
|
3269
|
+
if (!ringEl) continue;
|
|
3270
|
+
if (!pointInMeshElement(ringEl, event.clientX, event.clientY)) continue;
|
|
3271
|
+
event.preventDefault();
|
|
3272
|
+
event.stopPropagation();
|
|
3273
|
+
const wrapper = ringEl.closest(
|
|
3274
|
+
"[data-poly-transform-controls]"
|
|
3275
|
+
);
|
|
3276
|
+
if (!wrapper) return;
|
|
3277
|
+
setDraggingKey(spec.key);
|
|
3278
|
+
startRingDrag({
|
|
3279
|
+
cssAxis: spec.cssAxis,
|
|
3280
|
+
wrapper,
|
|
3281
|
+
target: state.target,
|
|
3282
|
+
startClientX: event.clientX,
|
|
3283
|
+
startClientY: event.clientY,
|
|
3284
|
+
rotationSnap: state.rotationSnap,
|
|
3285
|
+
onChange: state.onChange,
|
|
3286
|
+
onObjectChange: state.onObjectChange,
|
|
3287
|
+
onMouseDown: state.onMouseDown,
|
|
3288
|
+
onMouseUp: state.onMouseUp,
|
|
3289
|
+
onDraggingChanged: (d) => {
|
|
3290
|
+
if (!d) setDraggingKey(null);
|
|
3291
|
+
state.onDraggingChanged?.(d);
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
3298
|
+
cameraEl.addEventListener("pointerdown", onPointerDown);
|
|
3299
|
+
return () => cameraEl.removeEventListener("pointerdown", onPointerDown);
|
|
3300
|
+
}, [cameraElRef]);
|
|
3301
|
+
const target = resolveObject(object);
|
|
3302
|
+
if (!target) return null;
|
|
3303
|
+
const position = target.getPosition() ?? [0, 0, 0];
|
|
3304
|
+
const polygons = target.getPolygons();
|
|
3305
|
+
const bboxCenter = gizmoCenterForMesh(polygons);
|
|
3306
|
+
const wrapperPos = [
|
|
3307
|
+
position[0] + bboxCenter[0],
|
|
3308
|
+
position[1] + bboxCenter[1],
|
|
3309
|
+
position[2] + bboxCenter[2]
|
|
3310
|
+
];
|
|
3311
|
+
const baseLength = gizmoLengthForMesh(polygons);
|
|
3312
|
+
const shaftLengthCss = baseLength * size;
|
|
3313
|
+
dragRef.current = {
|
|
3314
|
+
target,
|
|
3315
|
+
mode,
|
|
3316
|
+
shaftLengthCss,
|
|
3317
|
+
enabled,
|
|
3318
|
+
show: { x: showX, y: showY, z: showZ },
|
|
3319
|
+
translationSnap,
|
|
3320
|
+
rotationSnap,
|
|
3321
|
+
onChange,
|
|
3322
|
+
onObjectChange,
|
|
3323
|
+
onMouseDown,
|
|
3324
|
+
onMouseUp,
|
|
3325
|
+
onDraggingChanged
|
|
3326
|
+
};
|
|
3327
|
+
const wrapperStyle = {
|
|
3328
|
+
position: "absolute",
|
|
3329
|
+
transformStyle: "preserve-3d",
|
|
3330
|
+
transform: `translate3d(${wrapperPos[0]}px, ${wrapperPos[1]}px, ${wrapperPos[2]}px)`,
|
|
3331
|
+
// No `pointer-events: none` here — that property is inherited, so
|
|
3332
|
+
// setting it on the wrapper would cascade to every arrow polygon
|
|
3333
|
+
// and disable native hit-testing on the gizmo entirely. The
|
|
3334
|
+
// wrapper is a 0×0 anchor so it has no surface to be hit on its
|
|
3335
|
+
// own; descendants opt in via the default `auto`.
|
|
3336
|
+
zIndex: 1e3
|
|
3337
|
+
};
|
|
3338
|
+
return /* @__PURE__ */ jsxs2(
|
|
3339
|
+
"div",
|
|
3340
|
+
{
|
|
3341
|
+
className: "polycss-transform-controls",
|
|
3342
|
+
"data-poly-transform-controls": true,
|
|
3343
|
+
"data-poly-mode": mode,
|
|
3344
|
+
"data-poly-space": space,
|
|
3345
|
+
style: wrapperStyle,
|
|
3346
|
+
children: [
|
|
3347
|
+
mode === "translate" && ARROW_SPECS.map((spec) => {
|
|
3348
|
+
const show = { x: showX, y: showY, z: showZ }[userAxisLetterOf(spec.key)];
|
|
3349
|
+
if (!show) return null;
|
|
3350
|
+
const hovered = hoveredKey === spec.key;
|
|
3351
|
+
const dragging = draggingKey === spec.key;
|
|
3352
|
+
const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
|
|
3353
|
+
return /* @__PURE__ */ jsx7(
|
|
3354
|
+
TranslateArrow,
|
|
3355
|
+
{
|
|
3356
|
+
cssAxis: spec.cssAxis,
|
|
3357
|
+
sign: spec.sign,
|
|
3358
|
+
axisKey: spec.key,
|
|
3359
|
+
color: withAlpha(spec.color, alpha),
|
|
3360
|
+
shaftLengthCss,
|
|
3361
|
+
target,
|
|
3362
|
+
enabled,
|
|
3363
|
+
translationSnap,
|
|
3364
|
+
onChange,
|
|
3365
|
+
onObjectChange,
|
|
3366
|
+
onMouseDown,
|
|
3367
|
+
onMouseUp,
|
|
3368
|
+
onDraggingChanged,
|
|
3369
|
+
onHoverChange: (h) => setHoveredKey(h ? spec.key : (cur) => cur === spec.key ? null : cur),
|
|
3370
|
+
onDraggingStart: () => setDraggingKey(spec.key),
|
|
3371
|
+
onDraggingStop: () => setDraggingKey((cur) => cur === spec.key ? null : cur)
|
|
3372
|
+
},
|
|
3373
|
+
spec.key
|
|
3374
|
+
);
|
|
3375
|
+
}),
|
|
3376
|
+
mode === "rotate" && RING_SPECS.map((spec) => {
|
|
3377
|
+
const show = { x: showX, y: showY, z: showZ }[spec.key];
|
|
3378
|
+
if (!show) return null;
|
|
3379
|
+
const hovered = hoveredKey === spec.key;
|
|
3380
|
+
const dragging = draggingKey === spec.key;
|
|
3381
|
+
const alpha = dragging ? ALPHA_DRAGGING : hovered ? ALPHA_HOVER : ALPHA_IDLE;
|
|
3382
|
+
return /* @__PURE__ */ jsx7(
|
|
3383
|
+
RotateRing,
|
|
3384
|
+
{
|
|
3385
|
+
cssAxis: spec.cssAxis,
|
|
3386
|
+
axisKey: spec.key,
|
|
3387
|
+
color: withAlpha(spec.color, alpha),
|
|
3388
|
+
radiusCss: shaftLengthCss * RING_RADIUS_RATIO,
|
|
3389
|
+
target,
|
|
3390
|
+
enabled,
|
|
3391
|
+
rotationSnap,
|
|
3392
|
+
onChange,
|
|
3393
|
+
onObjectChange,
|
|
3394
|
+
onMouseDown,
|
|
3395
|
+
onMouseUp,
|
|
3396
|
+
onDraggingChanged,
|
|
3397
|
+
onHoverChange: (h) => setHoveredKey(h ? spec.key : (cur) => cur === spec.key ? null : cur),
|
|
3398
|
+
onDraggingStart: () => setDraggingKey(spec.key),
|
|
3399
|
+
onDraggingStop: () => setDraggingKey((cur) => cur === spec.key ? null : cur)
|
|
3400
|
+
},
|
|
3401
|
+
spec.key
|
|
3402
|
+
);
|
|
3403
|
+
})
|
|
3404
|
+
]
|
|
3405
|
+
}
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
function TranslateArrow({
|
|
3409
|
+
cssAxis,
|
|
3410
|
+
sign,
|
|
3411
|
+
axisKey,
|
|
3412
|
+
color,
|
|
3413
|
+
shaftLengthCss,
|
|
3414
|
+
target,
|
|
3415
|
+
enabled,
|
|
3416
|
+
translationSnap,
|
|
3417
|
+
onChange,
|
|
3418
|
+
onObjectChange,
|
|
3419
|
+
onMouseDown,
|
|
3420
|
+
onMouseUp,
|
|
3421
|
+
onDraggingChanged,
|
|
3422
|
+
onHoverChange,
|
|
3423
|
+
onDraggingStart,
|
|
3424
|
+
onDraggingStop
|
|
3425
|
+
}) {
|
|
3426
|
+
const cbRef = useRef8({
|
|
3427
|
+
onChange,
|
|
3428
|
+
onObjectChange,
|
|
3429
|
+
onMouseDown,
|
|
3430
|
+
onMouseUp,
|
|
3431
|
+
onDraggingChanged,
|
|
3432
|
+
onDraggingStart,
|
|
3433
|
+
onDraggingStop,
|
|
3434
|
+
enabled,
|
|
3435
|
+
translationSnap
|
|
3436
|
+
});
|
|
3437
|
+
cbRef.current = {
|
|
3438
|
+
onChange,
|
|
3439
|
+
onObjectChange,
|
|
3440
|
+
onMouseDown,
|
|
3441
|
+
onMouseUp,
|
|
3442
|
+
onDraggingChanged,
|
|
3443
|
+
onDraggingStart,
|
|
3444
|
+
onDraggingStop,
|
|
3445
|
+
enabled,
|
|
3446
|
+
translationSnap
|
|
3447
|
+
};
|
|
3448
|
+
const polygons = useMemo10(() => {
|
|
3449
|
+
const lengthWorld = shaftLengthCss / SCENE_TILE_SIZE;
|
|
3450
|
+
return arrowPolygons({
|
|
3451
|
+
axis: WORLD_AXIS_FOR_CSS[cssAxis],
|
|
3452
|
+
sign,
|
|
3453
|
+
shaftLength: lengthWorld,
|
|
3454
|
+
shaftHalfThickness: lengthWorld * SHAFT_HALF_THICKNESS_RATIO,
|
|
3455
|
+
headLength: lengthWorld * HEAD_LENGTH_RATIO,
|
|
3456
|
+
headHalfThickness: lengthWorld * HEAD_HALF_THICKNESS_RATIO,
|
|
3457
|
+
color
|
|
3458
|
+
});
|
|
3459
|
+
}, [cssAxis, sign, color, shaftLengthCss]);
|
|
3460
|
+
const onPointerDown = useCallback7(
|
|
3461
|
+
(e) => {
|
|
3462
|
+
if (!cbRef.current.enabled) return;
|
|
3463
|
+
e.stopPropagation();
|
|
3464
|
+
const meshEl = e.eventObject.element;
|
|
3465
|
+
const wrapper = meshEl?.closest("[data-poly-transform-controls]");
|
|
3466
|
+
if (!wrapper) return;
|
|
3467
|
+
cbRef.current.onDraggingStart?.();
|
|
3468
|
+
startAxisDrag({
|
|
3469
|
+
cssAxis,
|
|
3470
|
+
sign,
|
|
3471
|
+
shaftLengthCss,
|
|
3472
|
+
wrapper,
|
|
3473
|
+
target,
|
|
3474
|
+
startClientX: e.nativeEvent.clientX,
|
|
3475
|
+
startClientY: e.nativeEvent.clientY,
|
|
3476
|
+
translationSnap: cbRef.current.translationSnap,
|
|
3477
|
+
onChange: cbRef.current.onChange,
|
|
3478
|
+
onObjectChange: cbRef.current.onObjectChange,
|
|
3479
|
+
onMouseDown: cbRef.current.onMouseDown,
|
|
3480
|
+
onMouseUp: cbRef.current.onMouseUp,
|
|
3481
|
+
onDraggingChanged: (d) => {
|
|
3482
|
+
if (!d) cbRef.current.onDraggingStop?.();
|
|
3483
|
+
cbRef.current.onDraggingChanged?.(d);
|
|
3484
|
+
}
|
|
3485
|
+
});
|
|
3486
|
+
},
|
|
3487
|
+
[cssAxis, sign, target, shaftLengthCss]
|
|
3488
|
+
);
|
|
3489
|
+
return /* @__PURE__ */ jsx7(
|
|
3490
|
+
PolyMesh,
|
|
3491
|
+
{
|
|
3492
|
+
polygons,
|
|
3493
|
+
onPointerDown,
|
|
3494
|
+
onPointerOver: () => onHoverChange?.(true),
|
|
3495
|
+
onPointerOut: () => onHoverChange?.(false),
|
|
3496
|
+
className: `polycss-transform-gizmo polycss-transform-arrow polycss-transform-arrow--${axisKey}`,
|
|
3497
|
+
textureLighting: "baked"
|
|
3498
|
+
}
|
|
3499
|
+
);
|
|
3500
|
+
}
|
|
3501
|
+
function RotateRing({
|
|
3502
|
+
cssAxis,
|
|
3503
|
+
axisKey,
|
|
3504
|
+
color,
|
|
3505
|
+
radiusCss,
|
|
3506
|
+
target,
|
|
3507
|
+
enabled,
|
|
3508
|
+
rotationSnap,
|
|
3509
|
+
onChange,
|
|
3510
|
+
onObjectChange,
|
|
3511
|
+
onMouseDown,
|
|
3512
|
+
onMouseUp,
|
|
3513
|
+
onDraggingChanged,
|
|
3514
|
+
onHoverChange,
|
|
3515
|
+
onDraggingStart,
|
|
3516
|
+
onDraggingStop
|
|
3517
|
+
}) {
|
|
3518
|
+
const cbRef = useRef8({
|
|
3519
|
+
onChange,
|
|
3520
|
+
onObjectChange,
|
|
3521
|
+
onMouseDown,
|
|
3522
|
+
onMouseUp,
|
|
3523
|
+
onDraggingChanged,
|
|
3524
|
+
onDraggingStart,
|
|
3525
|
+
onDraggingStop,
|
|
3526
|
+
enabled,
|
|
3527
|
+
rotationSnap
|
|
3528
|
+
});
|
|
3529
|
+
cbRef.current = {
|
|
3530
|
+
onChange,
|
|
3531
|
+
onObjectChange,
|
|
3532
|
+
onMouseDown,
|
|
3533
|
+
onMouseUp,
|
|
3534
|
+
onDraggingChanged,
|
|
3535
|
+
onDraggingStart,
|
|
3536
|
+
onDraggingStop,
|
|
3537
|
+
enabled,
|
|
3538
|
+
rotationSnap
|
|
3539
|
+
};
|
|
3540
|
+
const polygons = useMemo10(() => {
|
|
3541
|
+
const radiusWorld = radiusCss / SCENE_TILE_SIZE;
|
|
3542
|
+
return ringPolygons({
|
|
3543
|
+
axis: WORLD_AXIS_FOR_CSS[cssAxis],
|
|
3544
|
+
radius: radiusWorld,
|
|
3545
|
+
halfThickness: radiusWorld * RING_HALF_THICKNESS_RATIO,
|
|
3546
|
+
segments: RING_SEGMENTS,
|
|
3547
|
+
color
|
|
3548
|
+
});
|
|
3549
|
+
}, [cssAxis, color, radiusCss]);
|
|
3550
|
+
const onPointerDown = useCallback7(
|
|
3551
|
+
(e) => {
|
|
3552
|
+
if (!cbRef.current.enabled) return;
|
|
3553
|
+
e.stopPropagation();
|
|
3554
|
+
const meshEl = e.eventObject.element;
|
|
3555
|
+
const wrapper = meshEl?.closest("[data-poly-transform-controls]");
|
|
3556
|
+
if (!wrapper) return;
|
|
3557
|
+
cbRef.current.onDraggingStart?.();
|
|
3558
|
+
startRingDrag({
|
|
3559
|
+
cssAxis,
|
|
3560
|
+
wrapper,
|
|
3561
|
+
target,
|
|
3562
|
+
startClientX: e.nativeEvent.clientX,
|
|
3563
|
+
startClientY: e.nativeEvent.clientY,
|
|
3564
|
+
rotationSnap: cbRef.current.rotationSnap,
|
|
3565
|
+
onChange: cbRef.current.onChange,
|
|
3566
|
+
onObjectChange: cbRef.current.onObjectChange,
|
|
3567
|
+
onMouseDown: cbRef.current.onMouseDown,
|
|
3568
|
+
onMouseUp: cbRef.current.onMouseUp,
|
|
3569
|
+
onDraggingChanged: (d) => {
|
|
3570
|
+
if (!d) cbRef.current.onDraggingStop?.();
|
|
3571
|
+
cbRef.current.onDraggingChanged?.(d);
|
|
3572
|
+
}
|
|
3573
|
+
});
|
|
3574
|
+
},
|
|
3575
|
+
[cssAxis, target]
|
|
3576
|
+
);
|
|
3577
|
+
return /* @__PURE__ */ jsx7(
|
|
3578
|
+
PolyMesh,
|
|
3579
|
+
{
|
|
3580
|
+
polygons,
|
|
3581
|
+
onPointerDown,
|
|
3582
|
+
onPointerOver: () => onHoverChange?.(true),
|
|
3583
|
+
onPointerOut: () => onHoverChange?.(false),
|
|
3584
|
+
className: `polycss-transform-gizmo polycss-transform-ring polycss-transform-ring--${axisKey}`,
|
|
3585
|
+
textureLighting: "baked"
|
|
3586
|
+
}
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
// src/select/Select.tsx
|
|
3591
|
+
import {
|
|
3592
|
+
createContext as createContext3,
|
|
3593
|
+
useCallback as useCallback8,
|
|
3594
|
+
useContext as useContext5,
|
|
3595
|
+
useEffect as useEffect9,
|
|
3596
|
+
useMemo as useMemo11,
|
|
3597
|
+
useRef as useRef9,
|
|
3598
|
+
useState as useState5
|
|
3599
|
+
} from "react";
|
|
3600
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
3601
|
+
var SelectContext = createContext3(null);
|
|
3602
|
+
function PolySelect({
|
|
3603
|
+
multiple = false,
|
|
3604
|
+
filter,
|
|
3605
|
+
onChange,
|
|
3606
|
+
onPointerMissed,
|
|
3607
|
+
clearOnMiss = true,
|
|
3608
|
+
children,
|
|
3609
|
+
className,
|
|
3610
|
+
style
|
|
3611
|
+
}) {
|
|
3612
|
+
const [selected, setSelectedState] = useState5([]);
|
|
3613
|
+
const filterRef = useRef9(filter);
|
|
3614
|
+
filterRef.current = filter;
|
|
3615
|
+
const onChangeRef = useRef9(onChange);
|
|
3616
|
+
onChangeRef.current = onChange;
|
|
3617
|
+
const multipleRef = useRef9(multiple);
|
|
3618
|
+
multipleRef.current = multiple;
|
|
3619
|
+
const apply = useCallback8((next) => {
|
|
3620
|
+
const filtered = filterRef.current ? filterRef.current(next) : next;
|
|
3621
|
+
setSelectedState(filtered);
|
|
3622
|
+
if (onChangeRef.current) onChangeRef.current(filtered);
|
|
3623
|
+
}, []);
|
|
3624
|
+
const api = useMemo11(() => {
|
|
3625
|
+
return {
|
|
3626
|
+
selected,
|
|
3627
|
+
set: (next) => apply(next),
|
|
3628
|
+
add: (h) => {
|
|
3629
|
+
setSelectedState((prev) => {
|
|
3630
|
+
const next = multipleRef.current ? prev.includes(h) ? prev : [...prev, h] : [h];
|
|
3631
|
+
const filtered = filterRef.current ? filterRef.current(next) : next;
|
|
3632
|
+
if (onChangeRef.current) onChangeRef.current(filtered);
|
|
3633
|
+
return filtered;
|
|
3634
|
+
});
|
|
3635
|
+
},
|
|
3636
|
+
remove: (h) => {
|
|
3637
|
+
setSelectedState((prev) => {
|
|
3638
|
+
if (!prev.includes(h)) return prev;
|
|
3639
|
+
const next = prev.filter((x) => x !== h);
|
|
3640
|
+
const filtered = filterRef.current ? filterRef.current(next) : next;
|
|
3641
|
+
if (onChangeRef.current) onChangeRef.current(filtered);
|
|
3642
|
+
return filtered;
|
|
3643
|
+
});
|
|
3644
|
+
},
|
|
3645
|
+
toggle: (h) => {
|
|
3646
|
+
setSelectedState((prev) => {
|
|
3647
|
+
const next = prev.includes(h) ? prev.filter((x) => x !== h) : multipleRef.current ? [...prev, h] : [h];
|
|
3648
|
+
const filtered = filterRef.current ? filterRef.current(next) : next;
|
|
3649
|
+
if (onChangeRef.current) onChangeRef.current(filtered);
|
|
3650
|
+
return filtered;
|
|
3651
|
+
});
|
|
3652
|
+
},
|
|
3653
|
+
clear: () => apply([]),
|
|
3654
|
+
has: (h) => selected.includes(h)
|
|
3655
|
+
};
|
|
3656
|
+
}, [selected, apply]);
|
|
3657
|
+
const wrapperRef = useRef9(null);
|
|
3658
|
+
const cameraCtx = useContext5(PolyCameraContext);
|
|
3659
|
+
const cameraElRef = cameraCtx?.cameraElRef;
|
|
3660
|
+
const clearOnMissRef = useRef9(clearOnMiss);
|
|
3661
|
+
clearOnMissRef.current = clearOnMiss;
|
|
3662
|
+
const onPointerMissedRef = useRef9(onPointerMissed);
|
|
3663
|
+
onPointerMissedRef.current = onPointerMissed;
|
|
3664
|
+
const findMeshUnderPoint2 = useCallback8(
|
|
3665
|
+
(clientX, clientY) => findMeshUnderPoint(
|
|
3666
|
+
clientX,
|
|
3667
|
+
clientY,
|
|
3668
|
+
(meshEl) => !meshEl.closest("[data-poly-transform-controls]")
|
|
3669
|
+
),
|
|
3670
|
+
[]
|
|
3671
|
+
);
|
|
3672
|
+
useEffect9(() => {
|
|
3673
|
+
const cameraEl = cameraElRef?.current;
|
|
3674
|
+
if (!cameraEl) return;
|
|
3675
|
+
const onClick = (event) => {
|
|
3676
|
+
const target = event.target;
|
|
3677
|
+
if (target?.closest?.("[data-poly-transform-controls]")) return;
|
|
3678
|
+
const handle = findPolyMeshHandle(event.target) ?? findMeshUnderPoint2(event.clientX, event.clientY);
|
|
3679
|
+
if (!handle) {
|
|
3680
|
+
if (onPointerMissedRef.current) onPointerMissedRef.current(event);
|
|
3681
|
+
if (clearOnMissRef.current) apply([]);
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
const additive = multipleRef.current && (event.shiftKey || event.metaKey || event.ctrlKey);
|
|
3685
|
+
setSelectedState((prev) => {
|
|
3686
|
+
let next;
|
|
3687
|
+
if (additive) {
|
|
3688
|
+
next = prev.includes(handle) ? prev.filter((x) => x !== handle) : [...prev, handle];
|
|
3689
|
+
} else if (prev.length === 1 && prev[0] === handle) {
|
|
3690
|
+
next = [];
|
|
3691
|
+
} else {
|
|
3692
|
+
next = [handle];
|
|
3693
|
+
}
|
|
3694
|
+
const filtered = filterRef.current ? filterRef.current(next) : next;
|
|
3695
|
+
if (onChangeRef.current) onChangeRef.current(filtered);
|
|
3696
|
+
return filtered;
|
|
3697
|
+
});
|
|
3698
|
+
};
|
|
3699
|
+
cameraEl.addEventListener("click", onClick);
|
|
3700
|
+
return () => cameraEl.removeEventListener("click", onClick);
|
|
3701
|
+
}, [cameraElRef, findMeshUnderPoint2, apply]);
|
|
3702
|
+
const handleClick = (e) => {
|
|
3703
|
+
if (cameraCtx) return;
|
|
3704
|
+
const handle = findPolyMeshHandle(e.target) ?? findMeshUnderPoint2(e.clientX, e.clientY);
|
|
3705
|
+
if (!handle) {
|
|
3706
|
+
if (onPointerMissed) onPointerMissed(e);
|
|
3707
|
+
if (clearOnMiss) apply([]);
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
if (multiple && (e.shiftKey || e.metaKey || e.ctrlKey)) {
|
|
3711
|
+
api.toggle(handle);
|
|
3712
|
+
} else if (selected.length === 1 && selected[0] === handle) {
|
|
3713
|
+
apply([]);
|
|
3714
|
+
} else {
|
|
3715
|
+
apply([handle]);
|
|
3716
|
+
}
|
|
3717
|
+
};
|
|
3718
|
+
const wrapperStyle = { display: "contents", ...style };
|
|
3719
|
+
return /* @__PURE__ */ jsx8(SelectContext.Provider, { value: api, children: /* @__PURE__ */ jsx8(
|
|
3720
|
+
"div",
|
|
3721
|
+
{
|
|
3722
|
+
ref: wrapperRef,
|
|
3723
|
+
className,
|
|
3724
|
+
style: wrapperStyle,
|
|
3725
|
+
onClick: handleClick,
|
|
3726
|
+
"data-poly-select": true,
|
|
3727
|
+
children
|
|
3728
|
+
}
|
|
3729
|
+
) });
|
|
3730
|
+
}
|
|
3731
|
+
function usePolySelect() {
|
|
3732
|
+
return useContext5(SelectContext)?.selected ?? [];
|
|
3733
|
+
}
|
|
3734
|
+
function usePolySelectionApi() {
|
|
3735
|
+
const ctx = useContext5(SelectContext);
|
|
3736
|
+
if (!ctx) {
|
|
3737
|
+
throw new Error("polycss: usePolySelectionApi must be used inside <PolySelect>.");
|
|
3738
|
+
}
|
|
3739
|
+
return ctx;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
// src/helpers/PolyAxesHelper.tsx
|
|
3743
|
+
import { useMemo as useMemo12 } from "react";
|
|
3744
|
+
import { axesHelperPolygons } from "@layoutit/polycss-core";
|
|
3745
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
3746
|
+
function PolyAxesHelper({
|
|
3747
|
+
size,
|
|
3748
|
+
thickness,
|
|
3749
|
+
negative,
|
|
3750
|
+
xColor,
|
|
3751
|
+
yColor,
|
|
3752
|
+
zColor
|
|
3753
|
+
}) {
|
|
3754
|
+
const polygons = useMemo12(
|
|
3755
|
+
() => axesHelperPolygons({ size, thickness, negative, xColor, yColor, zColor }),
|
|
3756
|
+
[size, thickness, negative, xColor, yColor, zColor]
|
|
3757
|
+
);
|
|
3758
|
+
return /* @__PURE__ */ jsx9(PolyMesh, { polygons });
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
// src/helpers/PolyDirectionalLightHelper.tsx
|
|
3762
|
+
import { useMemo as useMemo13 } from "react";
|
|
3763
|
+
import { octahedronPolygons } from "@layoutit/polycss-core";
|
|
3764
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
3765
|
+
var TILE = 50;
|
|
3766
|
+
function PolyDirectionalLightHelper({
|
|
3767
|
+
light,
|
|
3768
|
+
target,
|
|
3769
|
+
distance = 5,
|
|
3770
|
+
size = 0.35,
|
|
3771
|
+
color
|
|
3772
|
+
}) {
|
|
3773
|
+
const swatch = color ?? light.color ?? "#ffd54a";
|
|
3774
|
+
const polygons = useMemo13(
|
|
3775
|
+
() => octahedronPolygons({ center: [0, 0, 0], size, color: swatch }),
|
|
3776
|
+
[size, swatch]
|
|
3777
|
+
);
|
|
3778
|
+
const meshPosition = useMemo13(() => {
|
|
3779
|
+
const [dx, dy, dz] = light.direction;
|
|
3780
|
+
const len = Math.hypot(dx, dy, dz) || 1;
|
|
3781
|
+
const tx = target?.[0] ?? 0;
|
|
3782
|
+
const ty = target?.[1] ?? 0;
|
|
3783
|
+
const tz = target?.[2] ?? 0;
|
|
3784
|
+
const worldX = tx + dy / len * distance;
|
|
3785
|
+
const worldY = ty + dx / len * distance;
|
|
3786
|
+
const worldZ = tz + dz / len * distance;
|
|
3787
|
+
return [worldY * TILE, worldX * TILE, worldZ * TILE];
|
|
3788
|
+
}, [light.direction, target, distance]);
|
|
3789
|
+
return /* @__PURE__ */ jsx10(PolyMesh, { polygons, position: meshPosition });
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
// src/styles/colorResolver.ts
|
|
3793
|
+
import { parsePureColor as parsePureColor2 } from "@layoutit/polycss-core";
|
|
3794
|
+
|
|
3795
|
+
// src/animation/usePolyAnimation.ts
|
|
3796
|
+
import { useEffect as useEffect10, useRef as useRef10, useMemo as useMemo14 } from "react";
|
|
3797
|
+
import { createPolyAnimationMixer } from "@layoutit/polycss-core";
|
|
3798
|
+
function resolveRoot(rootArg) {
|
|
3799
|
+
if (!rootArg) return null;
|
|
3800
|
+
if ("current" in rootArg) return rootArg.current;
|
|
3801
|
+
return rootArg;
|
|
3802
|
+
}
|
|
3803
|
+
function usePolyAnimation(clips, controller, root) {
|
|
3804
|
+
const internalRef = useRef10(null);
|
|
3805
|
+
const mixerRef = useRef10(null);
|
|
3806
|
+
useEffect10(() => {
|
|
3807
|
+
if (!clips || clips.length === 0 || !controller) {
|
|
3808
|
+
mixerRef.current = null;
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
const resolvedRoot = resolveRoot(root) ?? internalRef.current;
|
|
3812
|
+
if (!resolvedRoot) {
|
|
3813
|
+
mixerRef.current = null;
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
mixerRef.current = createPolyAnimationMixer(resolvedRoot, controller);
|
|
3817
|
+
return () => {
|
|
3818
|
+
mixerRef.current?.stopAllAction();
|
|
3819
|
+
mixerRef.current?.uncacheRoot();
|
|
3820
|
+
mixerRef.current = null;
|
|
3821
|
+
};
|
|
3822
|
+
}, [clips, controller]);
|
|
3823
|
+
useEffect10(() => {
|
|
3824
|
+
if (!clips || clips.length === 0 || !controller) return;
|
|
3825
|
+
let rafId;
|
|
3826
|
+
let lastTime = null;
|
|
3827
|
+
function tick(now) {
|
|
3828
|
+
if (lastTime === null) {
|
|
3829
|
+
lastTime = now;
|
|
3830
|
+
rafId = requestAnimationFrame(tick);
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
const dt = (now - lastTime) / 1e3;
|
|
3834
|
+
lastTime = now;
|
|
3835
|
+
mixerRef.current?.update(dt);
|
|
3836
|
+
rafId = requestAnimationFrame(tick);
|
|
3837
|
+
}
|
|
3838
|
+
rafId = requestAnimationFrame(tick);
|
|
3839
|
+
return () => {
|
|
3840
|
+
cancelAnimationFrame(rafId);
|
|
3841
|
+
lastTime = null;
|
|
3842
|
+
};
|
|
3843
|
+
}, [clips, controller]);
|
|
3844
|
+
const resolvedClips = clips ?? [];
|
|
3845
|
+
const resolvedNames = resolvedClips.map((c) => c.name);
|
|
3846
|
+
const actions = useMemo14(() => {
|
|
3847
|
+
const target = {};
|
|
3848
|
+
for (const clip of resolvedClips) {
|
|
3849
|
+
Object.defineProperty(target, clip.name, {
|
|
3850
|
+
enumerable: true,
|
|
3851
|
+
get() {
|
|
3852
|
+
const m = mixerRef.current;
|
|
3853
|
+
if (!m) return null;
|
|
3854
|
+
try {
|
|
3855
|
+
return m.clipAction(clip.name);
|
|
3856
|
+
} catch {
|
|
3857
|
+
return null;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
return target;
|
|
3863
|
+
}, [resolvedClips]);
|
|
3864
|
+
return {
|
|
3865
|
+
ref: internalRef,
|
|
3866
|
+
mixer: mixerRef.current,
|
|
3867
|
+
clips: resolvedClips,
|
|
3868
|
+
names: resolvedNames,
|
|
3869
|
+
actions
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// src/index.ts
|
|
3874
|
+
import {
|
|
3875
|
+
normalizePolygons,
|
|
3876
|
+
mergePolygons as mergePolygons2,
|
|
3877
|
+
coverPlanarPolygons,
|
|
3878
|
+
cullInteriorPolygons,
|
|
3879
|
+
parseObj,
|
|
3880
|
+
parseMtl,
|
|
3881
|
+
parseGltf,
|
|
3882
|
+
bakeSolidTextureSamples,
|
|
3883
|
+
bakeSolidTextureSampledPolygons,
|
|
3884
|
+
loadMesh as loadMesh2,
|
|
3885
|
+
createIsometricCamera as createIsometricCamera3,
|
|
3886
|
+
parseVox,
|
|
3887
|
+
polygonFaces,
|
|
3888
|
+
computeTexturePaintMetrics,
|
|
3889
|
+
computeShapeLighting,
|
|
3890
|
+
parseColor,
|
|
3891
|
+
parsePureColor as parsePureColor3,
|
|
3892
|
+
parseHexColor as parseHexColor2,
|
|
3893
|
+
parseRgbColor,
|
|
3894
|
+
formatColor,
|
|
3895
|
+
clampChannel,
|
|
3896
|
+
shadeColor,
|
|
3897
|
+
rotateVec3,
|
|
3898
|
+
inverseRotateVec3 as inverseRotateVec32,
|
|
3899
|
+
axesHelperPolygons as axesHelperPolygons2,
|
|
3900
|
+
arrowPolygons as arrowPolygons2,
|
|
3901
|
+
ringPolygons as ringPolygons2,
|
|
3902
|
+
octahedronPolygons as octahedronPolygons2,
|
|
3903
|
+
buildSceneContext as buildSceneContext2,
|
|
3904
|
+
computeSceneBbox as computeSceneBbox2,
|
|
3905
|
+
BASE_TILE as BASE_TILE3,
|
|
3906
|
+
DEFAULT_CAMERA_STATE,
|
|
3907
|
+
DEFAULT_PROJECTION,
|
|
3908
|
+
normalizeInvertMultiplier,
|
|
3909
|
+
createPolyAnimationMixer as createPolyAnimationMixer2,
|
|
3910
|
+
LoopOnce,
|
|
3911
|
+
LoopRepeat,
|
|
3912
|
+
LoopPingPong
|
|
3913
|
+
} from "@layoutit/polycss-core";
|
|
3914
|
+
export {
|
|
3915
|
+
BASE_TILE3 as BASE_TILE,
|
|
3916
|
+
DEFAULT_CAMERA_STATE,
|
|
3917
|
+
DEFAULT_PROJECTION,
|
|
3918
|
+
LoopOnce,
|
|
3919
|
+
LoopPingPong,
|
|
3920
|
+
LoopRepeat,
|
|
3921
|
+
Poly,
|
|
3922
|
+
PolyAxesHelper,
|
|
3923
|
+
PolyPerspectiveCamera as PolyCamera,
|
|
3924
|
+
PolyCameraContext,
|
|
3925
|
+
PolyDirectionalLightHelper,
|
|
3926
|
+
PolyMapControls,
|
|
3927
|
+
PolyMesh,
|
|
3928
|
+
PolyOrbitControls,
|
|
3929
|
+
PolyOrthographicCamera,
|
|
3930
|
+
PolyPerspectiveCamera,
|
|
3931
|
+
PolyScene,
|
|
3932
|
+
PolySelect,
|
|
3933
|
+
PolyTransformControls,
|
|
3934
|
+
arrowPolygons2 as arrowPolygons,
|
|
3935
|
+
axesHelperPolygons2 as axesHelperPolygons,
|
|
3936
|
+
bakeSolidTextureSampledPolygons,
|
|
3937
|
+
bakeSolidTextureSamples,
|
|
3938
|
+
buildSceneContext2 as buildSceneContext,
|
|
3939
|
+
clampChannel,
|
|
3940
|
+
computeSceneBbox2 as computeSceneBbox,
|
|
3941
|
+
computeShapeLighting,
|
|
3942
|
+
computeTexturePaintMetrics,
|
|
3943
|
+
coverPlanarPolygons,
|
|
3944
|
+
createIsometricCamera3 as createIsometricCamera,
|
|
3945
|
+
createPolyAnimationMixer2 as createPolyAnimationMixer,
|
|
3946
|
+
cullInteriorPolygons,
|
|
3947
|
+
findMeshUnderPoint,
|
|
3948
|
+
findPolyMeshHandle,
|
|
3949
|
+
formatColor,
|
|
3950
|
+
injectPolyBaseStyles,
|
|
3951
|
+
inverseRotateVec32 as inverseRotateVec3,
|
|
3952
|
+
loadMesh2 as loadMesh,
|
|
3953
|
+
mergePolygons2 as mergePolygons,
|
|
3954
|
+
normalizeInvertMultiplier,
|
|
3955
|
+
normalizePolygons,
|
|
3956
|
+
octahedronPolygons2 as octahedronPolygons,
|
|
3957
|
+
parseColor,
|
|
3958
|
+
parseGltf,
|
|
3959
|
+
parseHexColor2 as parseHexColor,
|
|
3960
|
+
parseMtl,
|
|
3961
|
+
parseObj,
|
|
3962
|
+
parsePureColor3 as parsePureColor,
|
|
3963
|
+
parseRgbColor,
|
|
3964
|
+
parseVox,
|
|
3965
|
+
pointInMeshElement,
|
|
3966
|
+
polygonFaces,
|
|
3967
|
+
ringPolygons2 as ringPolygons,
|
|
3968
|
+
rotateVec3,
|
|
3969
|
+
shadeColor,
|
|
3970
|
+
useCameraContext,
|
|
3971
|
+
usePolyAnimation,
|
|
3972
|
+
usePolyCamera,
|
|
3973
|
+
usePolyMaterial,
|
|
3974
|
+
usePolyMesh,
|
|
3975
|
+
usePolySceneContext,
|
|
3976
|
+
usePolySelect,
|
|
3977
|
+
usePolySelectionApi
|
|
3978
|
+
};
|