@particle-academy/fancy-3d-babylon 0.1.0
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/README.md +64 -0
- package/dist/chunk-DGUM43GV.js +10 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/engine.cjs +59 -0
- package/dist/engine.cjs.map +1 -0
- package/dist/engine.d.cts +19 -0
- package/dist/engine.d.ts +19 -0
- package/dist/engine.js +52 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.cjs +861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +323 -0
- package/dist/index.d.ts +323 -0
- package/dist/index.js +836 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +315 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +60 -0
- package/dist/react.d.ts +60 -0
- package/dist/react.js +311 -0
- package/dist/react.js.map +1 -0
- package/package.json +92 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import './chunk-DGUM43GV.js';
|
|
2
|
+
import { MeshBuilder, Mesh, DynamicTexture, Vector4, StandardMaterial, Color3, Vector3 } from '@babylonjs/core';
|
|
3
|
+
|
|
4
|
+
// src/painters.ts
|
|
5
|
+
var TEX_SCALE = 2;
|
|
6
|
+
function unitOf(_w, h) {
|
|
7
|
+
return h / 240;
|
|
8
|
+
}
|
|
9
|
+
function paintCardBg(ctx, w, h, selected) {
|
|
10
|
+
const u = unitOf(w, h);
|
|
11
|
+
ctx.fillStyle = "#0f172a";
|
|
12
|
+
roundRect(ctx, 0, 0, w, h, 14 * u);
|
|
13
|
+
ctx.fill();
|
|
14
|
+
ctx.strokeStyle = selected ? "#6366f1" : "#1e293b";
|
|
15
|
+
ctx.lineWidth = (selected ? 4 : 2) * u;
|
|
16
|
+
ctx.stroke();
|
|
17
|
+
}
|
|
18
|
+
function roundRect(ctx, x, y, w, h, r) {
|
|
19
|
+
ctx.beginPath();
|
|
20
|
+
ctx.moveTo(x + r, y);
|
|
21
|
+
ctx.arcTo(x + w, y, x + w, y + h, r);
|
|
22
|
+
ctx.arcTo(x + w, y + h, x, y + h, r);
|
|
23
|
+
ctx.arcTo(x, y + h, x, y, r);
|
|
24
|
+
ctx.arcTo(x, y, x + w, y, r);
|
|
25
|
+
ctx.closePath();
|
|
26
|
+
}
|
|
27
|
+
function paintLabel(ctx, text, x, y) {
|
|
28
|
+
ctx.fillStyle = "#94a3b8";
|
|
29
|
+
ctx.font = "600 22px ui-sans-serif, system-ui, sans-serif";
|
|
30
|
+
ctx.textBaseline = "top";
|
|
31
|
+
ctx.fillText(text.toUpperCase(), x, y);
|
|
32
|
+
}
|
|
33
|
+
function paintSparkline(ctx, values, x, y, w, h, variant, color) {
|
|
34
|
+
const max = Math.max(...values, 1);
|
|
35
|
+
const min = Math.min(...values, 0);
|
|
36
|
+
const range = max - min || 1;
|
|
37
|
+
const step = w / Math.max(values.length - 1, 1);
|
|
38
|
+
if (variant === "bar") {
|
|
39
|
+
const bw = w / values.length - 4;
|
|
40
|
+
ctx.fillStyle = color;
|
|
41
|
+
values.forEach((v, i) => {
|
|
42
|
+
const bh = (v - min) / range * h;
|
|
43
|
+
ctx.fillRect(x + i * (bw + 4), y + h - bh, bw, Math.max(bh, 1));
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
ctx.beginPath();
|
|
48
|
+
values.forEach((v, i) => {
|
|
49
|
+
const px = x + i * step;
|
|
50
|
+
const py = y + h - (v - min) / range * h;
|
|
51
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
52
|
+
else ctx.lineTo(px, py);
|
|
53
|
+
});
|
|
54
|
+
if (variant === "area") {
|
|
55
|
+
ctx.lineTo(x + w, y + h);
|
|
56
|
+
ctx.lineTo(x, y + h);
|
|
57
|
+
ctx.closePath();
|
|
58
|
+
ctx.fillStyle = color + "44";
|
|
59
|
+
ctx.fill();
|
|
60
|
+
ctx.beginPath();
|
|
61
|
+
values.forEach((v, i) => {
|
|
62
|
+
const px = x + i * step;
|
|
63
|
+
const py = y + h - (v - min) / range * h;
|
|
64
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
65
|
+
else ctx.lineTo(px, py);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
ctx.strokeStyle = color;
|
|
69
|
+
ctx.lineWidth = 3;
|
|
70
|
+
ctx.stroke();
|
|
71
|
+
}
|
|
72
|
+
function paintWidget(ctx, spec, w, h, selected) {
|
|
73
|
+
paintCardBg(ctx, w, h, selected);
|
|
74
|
+
const pad = 20;
|
|
75
|
+
switch (spec.kind) {
|
|
76
|
+
case "kpi": {
|
|
77
|
+
paintLabel(ctx, spec.label, pad, pad);
|
|
78
|
+
ctx.fillStyle = "#f1f5f9";
|
|
79
|
+
ctx.font = "700 56px ui-sans-serif, system-ui, sans-serif";
|
|
80
|
+
ctx.fillText(spec.value, pad, pad + 32);
|
|
81
|
+
if (spec.delta) {
|
|
82
|
+
ctx.fillStyle = spec.trend === "up" ? "#10b981" : spec.trend === "down" ? "#f43f5e" : "#94a3b8";
|
|
83
|
+
ctx.font = "600 22px ui-sans-serif, system-ui, sans-serif";
|
|
84
|
+
ctx.fillText(spec.delta, pad, pad + 92);
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "chart": {
|
|
89
|
+
paintLabel(ctx, spec.title, pad, pad);
|
|
90
|
+
paintSparkline(ctx, spec.series, pad, pad + 36, w - pad * 2, h - pad * 2 - 36, spec.variant, spec.color ?? "#6366f1");
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "kanban": {
|
|
94
|
+
const colW = (w - pad * 2 - (spec.columns.length - 1) * 10) / spec.columns.length;
|
|
95
|
+
spec.columns.forEach((col, i) => {
|
|
96
|
+
const cx = pad + i * (colW + 10);
|
|
97
|
+
ctx.fillStyle = "#1e293b";
|
|
98
|
+
roundRect(ctx, cx, pad, colW, h - pad * 2, 8);
|
|
99
|
+
ctx.fill();
|
|
100
|
+
paintLabel(ctx, col.title, cx + 8, pad + 10);
|
|
101
|
+
col.cards.forEach((c, j) => {
|
|
102
|
+
ctx.fillStyle = "#0f172a";
|
|
103
|
+
roundRect(ctx, cx + 8, pad + 44 + j * 38, colW - 16, 30, 6);
|
|
104
|
+
ctx.fill();
|
|
105
|
+
ctx.fillStyle = "#cbd5e1";
|
|
106
|
+
ctx.font = "500 20px ui-sans-serif, system-ui, sans-serif";
|
|
107
|
+
ctx.fillText(c, cx + 16, pad + 50 + j * 38);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "table": {
|
|
113
|
+
paintLabel(ctx, spec.title, pad, pad);
|
|
114
|
+
ctx.fillStyle = "#94a3b8";
|
|
115
|
+
ctx.font = "600 18px ui-sans-serif, system-ui, sans-serif";
|
|
116
|
+
const colW = (w - pad * 2) / spec.columns.length;
|
|
117
|
+
spec.columns.forEach((c, i) => ctx.fillText(c, pad + i * colW, pad + 38));
|
|
118
|
+
ctx.fillStyle = "#cbd5e1";
|
|
119
|
+
ctx.font = "400 18px ui-sans-serif, system-ui, sans-serif";
|
|
120
|
+
spec.rows.forEach((row, ri) => {
|
|
121
|
+
row.forEach((cell, ci) => ctx.fillText(String(cell), pad + ci * colW, pad + 70 + ri * 28));
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "profile": {
|
|
126
|
+
ctx.fillStyle = "#6366f1";
|
|
127
|
+
ctx.beginPath();
|
|
128
|
+
ctx.arc(pad + 30, pad + 30, 28, 0, Math.PI * 2);
|
|
129
|
+
ctx.fill();
|
|
130
|
+
ctx.fillStyle = "#fff";
|
|
131
|
+
ctx.font = "700 24px ui-sans-serif, system-ui, sans-serif";
|
|
132
|
+
ctx.textAlign = "center";
|
|
133
|
+
ctx.fillText(spec.initials, pad + 30, pad + 22);
|
|
134
|
+
ctx.textAlign = "left";
|
|
135
|
+
ctx.fillStyle = "#f1f5f9";
|
|
136
|
+
ctx.font = "600 24px ui-sans-serif, system-ui, sans-serif";
|
|
137
|
+
ctx.fillText(spec.name, pad + 76, pad + 12);
|
|
138
|
+
ctx.fillStyle = "#94a3b8";
|
|
139
|
+
ctx.font = "400 20px ui-sans-serif, system-ui, sans-serif";
|
|
140
|
+
ctx.fillText(spec.role, pad + 76, pad + 40);
|
|
141
|
+
if (spec.status) {
|
|
142
|
+
ctx.fillStyle = spec.status === "online" ? "#10b981" : spec.status === "away" ? "#f59e0b" : "#64748b";
|
|
143
|
+
ctx.beginPath();
|
|
144
|
+
ctx.arc(pad + 50, pad + 50, 6, 0, Math.PI * 2);
|
|
145
|
+
ctx.fill();
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "callout": {
|
|
150
|
+
const tone = spec.tone === "danger" ? "#f43f5e" : spec.tone === "warning" ? "#f59e0b" : spec.tone === "success" ? "#10b981" : "#3b82f6";
|
|
151
|
+
ctx.fillStyle = tone + "22";
|
|
152
|
+
roundRect(ctx, 4, 4, w - 8, h - 8, 12);
|
|
153
|
+
ctx.fill();
|
|
154
|
+
ctx.fillStyle = tone;
|
|
155
|
+
ctx.fillRect(12, 12, 4, h - 24);
|
|
156
|
+
ctx.fillStyle = "#f1f5f9";
|
|
157
|
+
ctx.font = "600 22px ui-sans-serif, system-ui, sans-serif";
|
|
158
|
+
ctx.fillText(spec.title, 28, pad);
|
|
159
|
+
ctx.fillStyle = "#cbd5e1";
|
|
160
|
+
ctx.font = "400 18px ui-sans-serif, system-ui, sans-serif";
|
|
161
|
+
wrapText(ctx, spec.body, 28, pad + 32, w - 40, 22);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case "form": {
|
|
165
|
+
paintLabel(ctx, spec.title, pad, pad);
|
|
166
|
+
spec.fields.forEach((f, i) => {
|
|
167
|
+
const y = pad + 40 + i * 44;
|
|
168
|
+
ctx.fillStyle = "#cbd5e1";
|
|
169
|
+
ctx.font = "500 18px ui-sans-serif, system-ui, sans-serif";
|
|
170
|
+
ctx.fillText(f.label, pad, y);
|
|
171
|
+
ctx.fillStyle = "#1e293b";
|
|
172
|
+
roundRect(ctx, w - pad - 100, y - 4, 100, 28, 6);
|
|
173
|
+
ctx.fill();
|
|
174
|
+
if (f.type === "switch") {
|
|
175
|
+
ctx.fillStyle = "#6366f1";
|
|
176
|
+
roundRect(ctx, w - pad - 60, y - 4, 60, 28, 14);
|
|
177
|
+
ctx.fill();
|
|
178
|
+
ctx.fillStyle = "#fff";
|
|
179
|
+
ctx.beginPath();
|
|
180
|
+
ctx.arc(w - pad - 18, y + 10, 10, 0, Math.PI * 2);
|
|
181
|
+
ctx.fill();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "action": {
|
|
187
|
+
paintLabel(ctx, spec.title, pad, pad);
|
|
188
|
+
let bx = pad;
|
|
189
|
+
const by = pad + 40;
|
|
190
|
+
spec.buttons.forEach((b) => {
|
|
191
|
+
const tw = ctx.measureText(b.label).width + 24;
|
|
192
|
+
ctx.fillStyle = b.variant === "primary" ? "#6366f1" : b.variant === "ghost" ? "transparent" : "#1e293b";
|
|
193
|
+
if (b.variant !== "ghost") {
|
|
194
|
+
roundRect(ctx, bx, by, tw, 36, 8);
|
|
195
|
+
ctx.fill();
|
|
196
|
+
}
|
|
197
|
+
ctx.fillStyle = b.variant === "primary" ? "#fff" : "#cbd5e1";
|
|
198
|
+
ctx.font = "600 18px ui-sans-serif, system-ui, sans-serif";
|
|
199
|
+
ctx.fillText(b.label, bx + 12, by + 9);
|
|
200
|
+
bx += tw + 8;
|
|
201
|
+
});
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "timeline": {
|
|
205
|
+
paintLabel(ctx, spec.title, pad, pad);
|
|
206
|
+
spec.events.forEach((e, i) => {
|
|
207
|
+
const y = pad + 40 + i * 32;
|
|
208
|
+
ctx.fillStyle = "#6366f1";
|
|
209
|
+
ctx.beginPath();
|
|
210
|
+
ctx.arc(pad + 4, y + 10, 4, 0, Math.PI * 2);
|
|
211
|
+
ctx.fill();
|
|
212
|
+
ctx.strokeStyle = "#1e293b";
|
|
213
|
+
if (i < spec.events.length - 1) {
|
|
214
|
+
ctx.beginPath();
|
|
215
|
+
ctx.moveTo(pad + 4, y + 14);
|
|
216
|
+
ctx.lineTo(pad + 4, y + 32);
|
|
217
|
+
ctx.stroke();
|
|
218
|
+
}
|
|
219
|
+
ctx.fillStyle = "#94a3b8";
|
|
220
|
+
ctx.font = "500 14px ui-monospace, monospace";
|
|
221
|
+
ctx.fillText(e.at, pad + 16, y);
|
|
222
|
+
ctx.fillStyle = "#cbd5e1";
|
|
223
|
+
ctx.font = "400 18px ui-sans-serif, system-ui, sans-serif";
|
|
224
|
+
ctx.fillText(e.label, pad + 60, y);
|
|
225
|
+
});
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case "code": {
|
|
229
|
+
paintLabel(ctx, spec.title, pad, pad);
|
|
230
|
+
ctx.fillStyle = "#020617";
|
|
231
|
+
roundRect(ctx, pad, pad + 36, w - pad * 2, h - pad * 2 - 36, 8);
|
|
232
|
+
ctx.fill();
|
|
233
|
+
ctx.fillStyle = "#a5b4fc";
|
|
234
|
+
ctx.font = "400 16px ui-monospace, monospace";
|
|
235
|
+
const lines = spec.code.split("\n");
|
|
236
|
+
lines.forEach((ln, i) => ctx.fillText(ln, pad + 14, pad + 56 + i * 22));
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "image": {
|
|
240
|
+
ctx.fillStyle = "#1e293b";
|
|
241
|
+
roundRect(ctx, pad, pad, w - pad * 2, h - pad * 2, 8);
|
|
242
|
+
ctx.fill();
|
|
243
|
+
ctx.fillStyle = "#64748b";
|
|
244
|
+
ctx.font = "400 18px ui-sans-serif, system-ui, sans-serif";
|
|
245
|
+
ctx.textAlign = "center";
|
|
246
|
+
ctx.fillText(spec.alt, w / 2, h / 2);
|
|
247
|
+
ctx.textAlign = "left";
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case "text": {
|
|
251
|
+
ctx.fillStyle = "#f1f5f9";
|
|
252
|
+
ctx.font = "700 26px ui-sans-serif, system-ui, sans-serif";
|
|
253
|
+
ctx.fillText(spec.heading, pad, pad);
|
|
254
|
+
ctx.fillStyle = "#cbd5e1";
|
|
255
|
+
ctx.font = "400 18px ui-sans-serif, system-ui, sans-serif";
|
|
256
|
+
wrapText(ctx, spec.body, pad, pad + 38, w - pad * 2, 22);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case "screen": {
|
|
260
|
+
const bezel = spec.bezel ?? "#0b0f17";
|
|
261
|
+
const t = spec.bezelThickness ?? 14;
|
|
262
|
+
const on = spec.on ?? true;
|
|
263
|
+
const brightness = spec.brightness ?? (on ? 1 : 0.06);
|
|
264
|
+
const bg = spec.background ?? "#020617";
|
|
265
|
+
ctx.fillStyle = bezel;
|
|
266
|
+
roundRect(ctx, 0, 0, w, h, 18);
|
|
267
|
+
ctx.fill();
|
|
268
|
+
const sx = t;
|
|
269
|
+
const sy = t;
|
|
270
|
+
const sw = w - t * 2;
|
|
271
|
+
const sh = h - t * 2;
|
|
272
|
+
ctx.save();
|
|
273
|
+
ctx.fillStyle = bg;
|
|
274
|
+
roundRect(ctx, sx, sy, sw, sh, 8);
|
|
275
|
+
ctx.fill();
|
|
276
|
+
ctx.beginPath();
|
|
277
|
+
roundRect(ctx, sx, sy, sw, sh, 8);
|
|
278
|
+
ctx.clip();
|
|
279
|
+
ctx.translate(sx, sy);
|
|
280
|
+
ctx.globalAlpha = brightness;
|
|
281
|
+
const c = spec.content;
|
|
282
|
+
if (c.type === "label") {
|
|
283
|
+
ctx.fillStyle = "#e2e8f0";
|
|
284
|
+
ctx.font = "700 32px ui-sans-serif, system-ui, sans-serif";
|
|
285
|
+
ctx.textAlign = "center";
|
|
286
|
+
ctx.textBaseline = "middle";
|
|
287
|
+
ctx.fillText(c.title, sw / 2, sh / 2 - (c.subtitle ? 16 : 0));
|
|
288
|
+
if (c.subtitle) {
|
|
289
|
+
ctx.fillStyle = "#94a3b8";
|
|
290
|
+
ctx.font = "400 18px ui-sans-serif, system-ui, sans-serif";
|
|
291
|
+
ctx.fillText(c.subtitle, sw / 2, sh / 2 + 18);
|
|
292
|
+
}
|
|
293
|
+
ctx.textAlign = "left";
|
|
294
|
+
ctx.textBaseline = "alphabetic";
|
|
295
|
+
} else if (c.type === "paint") {
|
|
296
|
+
c.paint(ctx, sw, sh);
|
|
297
|
+
} else if (c.type === "image") {
|
|
298
|
+
ctx.fillStyle = "#475569";
|
|
299
|
+
ctx.font = "500 16px ui-sans-serif, system-ui, sans-serif";
|
|
300
|
+
ctx.textAlign = "center";
|
|
301
|
+
ctx.fillText(c.alt ?? "image", sw / 2, sh / 2);
|
|
302
|
+
ctx.textAlign = "left";
|
|
303
|
+
}
|
|
304
|
+
ctx.globalAlpha = 1;
|
|
305
|
+
ctx.restore();
|
|
306
|
+
ctx.fillStyle = on ? "#10b981" : "#475569";
|
|
307
|
+
ctx.beginPath();
|
|
308
|
+
ctx.arc(w - t - 6, h - t - 6, 4, 0, Math.PI * 2);
|
|
309
|
+
ctx.fill();
|
|
310
|
+
const grad = ctx.createLinearGradient(sx, sy, sx, sy + sh);
|
|
311
|
+
grad.addColorStop(0, "rgba(255,255,255,0.08)");
|
|
312
|
+
grad.addColorStop(0.4, "rgba(255,255,255,0)");
|
|
313
|
+
ctx.fillStyle = grad;
|
|
314
|
+
ctx.beginPath();
|
|
315
|
+
roundRect(ctx, sx, sy, sw, sh, 8);
|
|
316
|
+
ctx.fill();
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case "demoPage": {
|
|
320
|
+
const u = unitOf(w, h);
|
|
321
|
+
const padX = w * 0.06;
|
|
322
|
+
const headerH = h * 0.22;
|
|
323
|
+
const titleY = headerH + h * 0.12;
|
|
324
|
+
const titleSize = h * 0.16;
|
|
325
|
+
const descY = titleY + titleSize * 0.6;
|
|
326
|
+
const descSize = h * 0.072;
|
|
327
|
+
const descLine = descSize * 1.4;
|
|
328
|
+
const chipSize = h * 0.052;
|
|
329
|
+
const pathSize = h * 0.052;
|
|
330
|
+
const ctaSize = h * 0.075;
|
|
331
|
+
const ctaH = ctaSize * 2;
|
|
332
|
+
ctx.fillStyle = spec.accent;
|
|
333
|
+
roundRect(ctx, 0, 0, w, headerH, 14 * u);
|
|
334
|
+
ctx.fill();
|
|
335
|
+
ctx.fillRect(0, headerH - 14 * u, w, 14 * u);
|
|
336
|
+
ctx.fillStyle = "rgba(255,255,255,0.22)";
|
|
337
|
+
const chipText = spec.category.toUpperCase();
|
|
338
|
+
ctx.font = `700 ${chipSize}px ui-sans-serif, system-ui, sans-serif`;
|
|
339
|
+
ctx.textBaseline = "top";
|
|
340
|
+
const chipPad = chipSize * 0.7;
|
|
341
|
+
const chipW = ctx.measureText(chipText).width + chipPad * 2;
|
|
342
|
+
const chipH = chipSize * 1.85;
|
|
343
|
+
const chipY = (headerH - chipH) / 2;
|
|
344
|
+
roundRect(ctx, padX, chipY, chipW, chipH, chipH / 2);
|
|
345
|
+
ctx.fill();
|
|
346
|
+
ctx.fillStyle = "#fff";
|
|
347
|
+
ctx.fillText(chipText, padX + chipPad, chipY + (chipH - chipSize) / 2);
|
|
348
|
+
ctx.fillStyle = "rgba(255,255,255,0.9)";
|
|
349
|
+
ctx.font = `500 ${pathSize}px ui-monospace, monospace`;
|
|
350
|
+
const pathW = ctx.measureText(spec.path).width;
|
|
351
|
+
ctx.fillText(spec.path, w - padX - pathW, chipY + (chipH - chipSize) / 2);
|
|
352
|
+
ctx.fillStyle = "#f8fafc";
|
|
353
|
+
ctx.font = `800 ${titleSize}px ui-sans-serif, system-ui, sans-serif`;
|
|
354
|
+
ctx.fillText(spec.name, padX, titleY);
|
|
355
|
+
ctx.fillStyle = "#94a3b8";
|
|
356
|
+
ctx.font = `400 ${descSize}px ui-sans-serif, system-ui, sans-serif`;
|
|
357
|
+
const ctaY = h - padX - ctaH;
|
|
358
|
+
wrapText(ctx, spec.description, padX, descY, w - padX * 2, descLine, ctaY - descLine);
|
|
359
|
+
ctx.fillStyle = spec.accent;
|
|
360
|
+
const ctaText = "Open demo \u2192";
|
|
361
|
+
ctx.font = `600 ${ctaSize}px ui-sans-serif, system-ui, sans-serif`;
|
|
362
|
+
const ctaTextPad = ctaSize * 0.9;
|
|
363
|
+
const ctaW = ctx.measureText(ctaText).width + ctaTextPad * 2;
|
|
364
|
+
roundRect(ctx, padX, ctaY, ctaW, ctaH, ctaH * 0.25);
|
|
365
|
+
ctx.fill();
|
|
366
|
+
ctx.fillStyle = "#0f172a";
|
|
367
|
+
ctx.fillText(ctaText, padX + ctaTextPad, ctaY + (ctaH - ctaSize) / 2);
|
|
368
|
+
ctx.textBaseline = "alphabetic";
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function wrapText(ctx, text, x, y, maxW, lineH, maxY) {
|
|
374
|
+
const words = text.split(" ");
|
|
375
|
+
let line = "";
|
|
376
|
+
let yy = y;
|
|
377
|
+
for (const word of words) {
|
|
378
|
+
const test = line ? `${line} ${word}` : word;
|
|
379
|
+
if (ctx.measureText(test).width > maxW && line) {
|
|
380
|
+
if (maxY !== void 0 && yy > maxY) return;
|
|
381
|
+
ctx.fillText(line, x, yy);
|
|
382
|
+
line = word;
|
|
383
|
+
yy += lineH;
|
|
384
|
+
} else {
|
|
385
|
+
line = test;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (line && (maxY === void 0 || yy <= maxY)) ctx.fillText(line, x, yy);
|
|
389
|
+
}
|
|
390
|
+
var DEFAULT_DENSITY = 120;
|
|
391
|
+
var MAX_TEXTURE_AXIS = 1024;
|
|
392
|
+
function autoPixelSize(worldWidth, worldHeight, density = DEFAULT_DENSITY) {
|
|
393
|
+
let w = Math.max(96, Math.round(worldWidth * density));
|
|
394
|
+
let h = Math.max(96, Math.round(worldHeight * density));
|
|
395
|
+
const scale = Math.min(1, MAX_TEXTURE_AXIS / Math.max(w, h));
|
|
396
|
+
if (scale < 1) {
|
|
397
|
+
w = Math.round(w * scale);
|
|
398
|
+
h = Math.round(h * scale);
|
|
399
|
+
}
|
|
400
|
+
return { w, h };
|
|
401
|
+
}
|
|
402
|
+
function buildSurface(scene, name, surface, autoSize) {
|
|
403
|
+
const mat = new StandardMaterial(`${name}-mat`, scene);
|
|
404
|
+
mat.specularColor = new Color3(0.05, 0.05, 0.05);
|
|
405
|
+
if (surface.type === "color") {
|
|
406
|
+
mat.diffuseColor = Color3.FromHexString(surface.color);
|
|
407
|
+
mat.emissiveColor = Color3.FromHexString(surface.color).scale(0.15);
|
|
408
|
+
return { material: mat, texture: null };
|
|
409
|
+
}
|
|
410
|
+
let w = surface.pixelWidth;
|
|
411
|
+
let h = surface.pixelHeight;
|
|
412
|
+
if ((w == null || h == null) && autoSize) {
|
|
413
|
+
const auto = autoPixelSize(autoSize.worldWidth, autoSize.worldHeight);
|
|
414
|
+
w = w ?? auto.w;
|
|
415
|
+
h = h ?? auto.h;
|
|
416
|
+
}
|
|
417
|
+
w = w ?? 512;
|
|
418
|
+
h = h ?? 512;
|
|
419
|
+
const tex = new DynamicTexture(`${name}-tex`, { width: w * TEX_SCALE, height: h * TEX_SCALE }, scene, true);
|
|
420
|
+
tex.vScale = -1;
|
|
421
|
+
tex.vOffset = 1;
|
|
422
|
+
if (surface.type === "paint" && surface.transparent) {
|
|
423
|
+
tex.hasAlpha = true;
|
|
424
|
+
}
|
|
425
|
+
if (surface.type === "widget") {
|
|
426
|
+
tex.hasAlpha = true;
|
|
427
|
+
}
|
|
428
|
+
const ctx = tex.getContext();
|
|
429
|
+
ctx.scale(TEX_SCALE, TEX_SCALE);
|
|
430
|
+
if (surface.type === "paint") {
|
|
431
|
+
surface.paint(ctx, w, h);
|
|
432
|
+
} else {
|
|
433
|
+
paintWidget(ctx, surface.widget, w, h, surface.selected ?? false);
|
|
434
|
+
}
|
|
435
|
+
tex.update(false);
|
|
436
|
+
mat.diffuseTexture = tex;
|
|
437
|
+
mat.emissiveTexture = tex;
|
|
438
|
+
mat.emissiveColor = new Color3(0.6, 0.6, 0.6);
|
|
439
|
+
if (tex.hasAlpha) mat.useAlphaFromDiffuseTexture = true;
|
|
440
|
+
return { material: mat, texture: tex };
|
|
441
|
+
}
|
|
442
|
+
function createPanel(opts) {
|
|
443
|
+
const name = opts.name ?? "panel";
|
|
444
|
+
const m = MeshBuilder.CreatePlane(name, { width: opts.width, height: opts.height, sideOrientation: opts.sideOrientation }, opts.scene);
|
|
445
|
+
const { material } = buildSurface(opts.scene, name, opts.surface, {
|
|
446
|
+
worldWidth: opts.width,
|
|
447
|
+
worldHeight: opts.height
|
|
448
|
+
});
|
|
449
|
+
m.material = material;
|
|
450
|
+
return m;
|
|
451
|
+
}
|
|
452
|
+
function createBillboard(opts) {
|
|
453
|
+
const m = createPanel({ ...opts, name: opts.name ?? "billboard" });
|
|
454
|
+
if (opts.faceCamera) {
|
|
455
|
+
m.billboardMode = Mesh.BILLBOARDMODE_ALL;
|
|
456
|
+
}
|
|
457
|
+
return m;
|
|
458
|
+
}
|
|
459
|
+
var BOX_FACE_ORDER = ["front", "back", "right", "left", "top", "bottom"];
|
|
460
|
+
function createBuilding(opts) {
|
|
461
|
+
const name = opts.name ?? "building";
|
|
462
|
+
if (opts.surface && !opts.faces) {
|
|
463
|
+
const box2 = MeshBuilder.CreateBox(name, { width: opts.width, height: opts.height, depth: opts.depth }, opts.scene);
|
|
464
|
+
const { material } = buildSurface(opts.scene, name, opts.surface, {
|
|
465
|
+
worldWidth: opts.width,
|
|
466
|
+
worldHeight: opts.height
|
|
467
|
+
});
|
|
468
|
+
box2.material = material;
|
|
469
|
+
return box2;
|
|
470
|
+
}
|
|
471
|
+
const faceW = 256;
|
|
472
|
+
const faceH = 256;
|
|
473
|
+
const atlasW = faceW * 3;
|
|
474
|
+
const atlasH = faceH * 2;
|
|
475
|
+
const tex = new DynamicTexture(`${name}-atlas`, { width: atlasW * TEX_SCALE, height: atlasH * TEX_SCALE }, opts.scene, true);
|
|
476
|
+
tex.vScale = -1;
|
|
477
|
+
tex.vOffset = 1;
|
|
478
|
+
const ctx = tex.getContext();
|
|
479
|
+
ctx.scale(TEX_SCALE, TEX_SCALE);
|
|
480
|
+
const faceUV = [];
|
|
481
|
+
BOX_FACE_ORDER.forEach((face, i) => {
|
|
482
|
+
const col = i % 3;
|
|
483
|
+
const row = Math.floor(i / 3);
|
|
484
|
+
const fx = col * faceW;
|
|
485
|
+
const fy = row * faceH;
|
|
486
|
+
const surface = opts.faces?.[face] ?? opts.surface ?? { type: "color", color: "#475569" };
|
|
487
|
+
ctx.save();
|
|
488
|
+
ctx.translate(fx, fy);
|
|
489
|
+
if (surface.type === "color") {
|
|
490
|
+
ctx.fillStyle = surface.color;
|
|
491
|
+
ctx.fillRect(0, 0, faceW, faceH);
|
|
492
|
+
} else if (surface.type === "paint") {
|
|
493
|
+
surface.paint(ctx, faceW, faceH);
|
|
494
|
+
} else {
|
|
495
|
+
paintWidget(ctx, surface.widget, faceW, faceH, surface.selected ?? false);
|
|
496
|
+
}
|
|
497
|
+
ctx.restore();
|
|
498
|
+
faceUV.push(new Vector4(fx / atlasW, 1 - (fy + faceH) / atlasH, (fx + faceW) / atlasW, 1 - fy / atlasH));
|
|
499
|
+
});
|
|
500
|
+
tex.update(false);
|
|
501
|
+
const mat = new StandardMaterial(`${name}-mat`, opts.scene);
|
|
502
|
+
mat.diffuseTexture = tex;
|
|
503
|
+
mat.emissiveTexture = tex;
|
|
504
|
+
mat.emissiveColor = new Color3(0.4, 0.4, 0.4);
|
|
505
|
+
mat.specularColor = new Color3(0.05, 0.05, 0.05);
|
|
506
|
+
const box = MeshBuilder.CreateBox(name, {
|
|
507
|
+
width: opts.width,
|
|
508
|
+
height: opts.height,
|
|
509
|
+
depth: opts.depth,
|
|
510
|
+
faceUV,
|
|
511
|
+
wrap: true
|
|
512
|
+
}, opts.scene);
|
|
513
|
+
box.material = mat;
|
|
514
|
+
return box;
|
|
515
|
+
}
|
|
516
|
+
function createPillar(opts) {
|
|
517
|
+
return createBuilding({
|
|
518
|
+
scene: opts.scene,
|
|
519
|
+
name: opts.name ?? "pillar",
|
|
520
|
+
width: opts.thickness,
|
|
521
|
+
depth: opts.thickness,
|
|
522
|
+
height: opts.height,
|
|
523
|
+
surface: opts.surface
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
function createCylinder(opts) {
|
|
527
|
+
const name = opts.name ?? "cylinder";
|
|
528
|
+
const m = MeshBuilder.CreateCylinder(name, {
|
|
529
|
+
diameter: opts.radius * 2,
|
|
530
|
+
height: opts.height,
|
|
531
|
+
tessellation: opts.tessellation ?? 48
|
|
532
|
+
}, opts.scene);
|
|
533
|
+
const { material } = buildSurface(opts.scene, name, opts.surface, {
|
|
534
|
+
worldWidth: opts.radius * 2 * Math.PI,
|
|
535
|
+
worldHeight: opts.height
|
|
536
|
+
});
|
|
537
|
+
m.material = material;
|
|
538
|
+
return m;
|
|
539
|
+
}
|
|
540
|
+
function createCurvedPanel(opts) {
|
|
541
|
+
const name = opts.name ?? "curved-panel";
|
|
542
|
+
const radius = opts.width / opts.arc;
|
|
543
|
+
const tess = opts.tessellation ?? 48;
|
|
544
|
+
const fullCircleSegments = Math.max(8, Math.ceil(tess * (Math.PI * 2) / opts.arc));
|
|
545
|
+
const m = MeshBuilder.CreateCylinder(name, {
|
|
546
|
+
diameter: radius * 2,
|
|
547
|
+
height: opts.height,
|
|
548
|
+
tessellation: fullCircleSegments,
|
|
549
|
+
arc: opts.arc / (Math.PI * 2),
|
|
550
|
+
cap: Mesh.NO_CAP,
|
|
551
|
+
sideOrientation: Mesh.DOUBLESIDE
|
|
552
|
+
}, opts.scene);
|
|
553
|
+
const { material } = buildSurface(opts.scene, name, opts.surface, {
|
|
554
|
+
worldWidth: opts.width,
|
|
555
|
+
worldHeight: opts.height
|
|
556
|
+
});
|
|
557
|
+
m.material = material;
|
|
558
|
+
return m;
|
|
559
|
+
}
|
|
560
|
+
function createSphere(opts) {
|
|
561
|
+
const name = opts.name ?? "sphere";
|
|
562
|
+
const m = MeshBuilder.CreateSphere(name, { diameter: opts.diameter, segments: opts.segments ?? 32 }, opts.scene);
|
|
563
|
+
const { material } = buildSurface(opts.scene, name, opts.surface, {
|
|
564
|
+
worldWidth: opts.diameter * Math.PI,
|
|
565
|
+
worldHeight: opts.diameter * Math.PI / 2
|
|
566
|
+
});
|
|
567
|
+
m.material = material;
|
|
568
|
+
return m;
|
|
569
|
+
}
|
|
570
|
+
function createDisc(opts) {
|
|
571
|
+
const name = opts.name ?? "disc";
|
|
572
|
+
const m = MeshBuilder.CreateDisc(name, { radius: opts.radius, tessellation: opts.tessellation ?? 64 }, opts.scene);
|
|
573
|
+
const { material } = buildSurface(opts.scene, name, opts.surface, {
|
|
574
|
+
worldWidth: opts.radius * 2,
|
|
575
|
+
worldHeight: opts.radius * 2
|
|
576
|
+
});
|
|
577
|
+
m.material = material;
|
|
578
|
+
return m;
|
|
579
|
+
}
|
|
580
|
+
function createCard3D(opts) {
|
|
581
|
+
const name = opts.name ?? "card3d";
|
|
582
|
+
const depth = opts.depth ?? 0.06;
|
|
583
|
+
const edge = opts.edge ?? { type: "color", color: "#0b0f17" };
|
|
584
|
+
return createBuilding({
|
|
585
|
+
scene: opts.scene,
|
|
586
|
+
name,
|
|
587
|
+
width: opts.width,
|
|
588
|
+
height: opts.height,
|
|
589
|
+
depth,
|
|
590
|
+
faces: {
|
|
591
|
+
front: opts.front,
|
|
592
|
+
back: edge,
|
|
593
|
+
top: edge,
|
|
594
|
+
bottom: edge,
|
|
595
|
+
left: edge,
|
|
596
|
+
right: edge
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function createSign(opts) {
|
|
601
|
+
const name = opts.name ?? "sign";
|
|
602
|
+
const postHeight = opts.postHeight ?? 1.4;
|
|
603
|
+
const postThickness = opts.postThickness ?? 0.12;
|
|
604
|
+
const postColor = opts.postColor ?? "#1f2937";
|
|
605
|
+
const root = new Mesh(`${name}-root`, opts.scene);
|
|
606
|
+
const panel = createBillboard({
|
|
607
|
+
scene: opts.scene,
|
|
608
|
+
name: `${name}-panel`,
|
|
609
|
+
width: opts.width,
|
|
610
|
+
height: opts.height,
|
|
611
|
+
surface: opts.surface
|
|
612
|
+
});
|
|
613
|
+
panel.position = new Vector3(0, postHeight + opts.height / 2, 0);
|
|
614
|
+
panel.parent = root;
|
|
615
|
+
const post = MeshBuilder.CreateBox(`${name}-post`, {
|
|
616
|
+
width: postThickness,
|
|
617
|
+
height: postHeight,
|
|
618
|
+
depth: postThickness
|
|
619
|
+
}, opts.scene);
|
|
620
|
+
post.position = new Vector3(0, postHeight / 2, 0);
|
|
621
|
+
const postMat = new StandardMaterial(`${name}-post-mat`, opts.scene);
|
|
622
|
+
postMat.diffuseColor = Color3.FromHexString(postColor);
|
|
623
|
+
post.material = postMat;
|
|
624
|
+
post.parent = root;
|
|
625
|
+
return { root, panel, post };
|
|
626
|
+
}
|
|
627
|
+
function createMonitor(opts) {
|
|
628
|
+
const name = opts.name ?? "monitor";
|
|
629
|
+
const depth = opts.depth ?? 0.18;
|
|
630
|
+
const bezelColor = opts.bezel ?? "#0b0f17";
|
|
631
|
+
const root = new Mesh(`${name}-root`, opts.scene);
|
|
632
|
+
const body = MeshBuilder.CreateBox(`${name}-body`, {
|
|
633
|
+
width: opts.width,
|
|
634
|
+
height: opts.height,
|
|
635
|
+
depth
|
|
636
|
+
}, opts.scene);
|
|
637
|
+
const bodyMat = new StandardMaterial(`${name}-body-mat`, opts.scene);
|
|
638
|
+
bodyMat.diffuseColor = Color3.FromHexString(bezelColor);
|
|
639
|
+
bodyMat.specularColor = new Color3(0.08, 0.08, 0.08);
|
|
640
|
+
body.material = bodyMat;
|
|
641
|
+
body.parent = root;
|
|
642
|
+
const screenInset = 0.04;
|
|
643
|
+
const screenW = opts.width - screenInset * 2;
|
|
644
|
+
const screenH = opts.height - screenInset * 2;
|
|
645
|
+
const screen = createCard3D({
|
|
646
|
+
scene: opts.scene,
|
|
647
|
+
name: `${name}-screen`,
|
|
648
|
+
width: screenW,
|
|
649
|
+
height: screenH,
|
|
650
|
+
depth: 0.01,
|
|
651
|
+
front: opts.screen,
|
|
652
|
+
edge: { type: "color", color: bezelColor }
|
|
653
|
+
});
|
|
654
|
+
screen.position = new Vector3(0, 0, depth / 2 + 5e-3);
|
|
655
|
+
screen.parent = root;
|
|
656
|
+
let stand;
|
|
657
|
+
if (opts.standHeight && opts.standHeight > 0) {
|
|
658
|
+
stand = MeshBuilder.CreateBox(`${name}-stand`, {
|
|
659
|
+
width: opts.width * 0.3,
|
|
660
|
+
height: opts.standHeight,
|
|
661
|
+
depth: depth * 1.5
|
|
662
|
+
}, opts.scene);
|
|
663
|
+
stand.position = new Vector3(0, -opts.height / 2 - opts.standHeight / 2, 0);
|
|
664
|
+
stand.material = bodyMat;
|
|
665
|
+
stand.parent = root;
|
|
666
|
+
}
|
|
667
|
+
return { root, body, screen, stand };
|
|
668
|
+
}
|
|
669
|
+
function createDecal(opts) {
|
|
670
|
+
const name = opts.name ?? "decal";
|
|
671
|
+
const normal = opts.normal ?? new Vector3(0, 0, -1);
|
|
672
|
+
const size = new Vector3(opts.size.width, opts.size.height, opts.size.depth ?? 1);
|
|
673
|
+
const decal = MeshBuilder.CreateDecal(name, opts.target, {
|
|
674
|
+
position: opts.position,
|
|
675
|
+
normal,
|
|
676
|
+
size,
|
|
677
|
+
angle: opts.angle
|
|
678
|
+
});
|
|
679
|
+
const { material } = buildSurface(opts.scene, name, opts.surface, {
|
|
680
|
+
worldWidth: opts.size.width,
|
|
681
|
+
worldHeight: opts.size.height
|
|
682
|
+
});
|
|
683
|
+
if ("hasAlpha" in material) {
|
|
684
|
+
material.useAlphaFromDiffuseTexture = true;
|
|
685
|
+
}
|
|
686
|
+
decal.material = material;
|
|
687
|
+
return decal;
|
|
688
|
+
}
|
|
689
|
+
function createWidgetTexture(scene, name, widget, pixelWidth, pixelHeight, selected = false) {
|
|
690
|
+
const tex = new DynamicTexture(name, { width: pixelWidth * TEX_SCALE, height: pixelHeight * TEX_SCALE }, scene, true);
|
|
691
|
+
tex.vScale = -1;
|
|
692
|
+
tex.vOffset = 1;
|
|
693
|
+
tex.hasAlpha = true;
|
|
694
|
+
const ctx = tex.getContext();
|
|
695
|
+
ctx.scale(TEX_SCALE, TEX_SCALE);
|
|
696
|
+
paintWidget(ctx, widget, pixelWidth, pixelHeight, selected);
|
|
697
|
+
tex.update(false);
|
|
698
|
+
return tex;
|
|
699
|
+
}
|
|
700
|
+
function sizeOf(node) {
|
|
701
|
+
return { w: node.size?.w ?? 200, h: node.size?.h ?? 120 };
|
|
702
|
+
}
|
|
703
|
+
function normalizedCenter(node, bounds) {
|
|
704
|
+
const { w, h } = sizeOf(node);
|
|
705
|
+
const cx = (node.position.x + w / 2 - bounds.minX) / Math.max(1, bounds.maxX - bounds.minX);
|
|
706
|
+
const cy = (node.position.y + h / 2 - bounds.minY) / Math.max(1, bounds.maxY - bounds.minY);
|
|
707
|
+
return { cx, cy };
|
|
708
|
+
}
|
|
709
|
+
function placeOnGrid(node, mesh, bounds, opts = {}) {
|
|
710
|
+
const scale = opts.scale ?? 1 / 120;
|
|
711
|
+
const origin = opts.origin ?? new Vector3(0, 0, 0);
|
|
712
|
+
const { w, h } = sizeOf(node);
|
|
713
|
+
const cx = node.position.x + w / 2 - (bounds.minX + bounds.maxX) / 2;
|
|
714
|
+
const cy = node.position.y + h / 2 - (bounds.minY + bounds.maxY) / 2;
|
|
715
|
+
mesh.position = new Vector3(origin.x + cx * scale, origin.y - cy * scale, origin.z);
|
|
716
|
+
}
|
|
717
|
+
function placeOnWall(node, mesh, bounds, opts = {}) {
|
|
718
|
+
const spreadX = opts.spreadX ?? 10;
|
|
719
|
+
const spreadY = opts.spreadY ?? 4;
|
|
720
|
+
const centerY = opts.centerY ?? 1.5;
|
|
721
|
+
const z = opts.z ?? 0;
|
|
722
|
+
const { cx, cy } = normalizedCenter(node, bounds);
|
|
723
|
+
mesh.position = new Vector3((cx - 0.5) * spreadX, centerY + (0.5 - cy) * spreadY, z);
|
|
724
|
+
mesh.rotation.y = 0;
|
|
725
|
+
}
|
|
726
|
+
function placeOnArc(node, mesh, bounds, opts = {}) {
|
|
727
|
+
const radius = opts.radius ?? 6;
|
|
728
|
+
const arc = opts.arc ?? Math.PI * 0.9;
|
|
729
|
+
const height = opts.height ?? 6;
|
|
730
|
+
const baseY = opts.baseY ?? 0;
|
|
731
|
+
const { cx, cy } = normalizedCenter(node, bounds);
|
|
732
|
+
const angle = (cx - 0.5) * arc;
|
|
733
|
+
mesh.position = new Vector3(Math.sin(angle) * radius, baseY + (0.5 - cy) * height, Math.cos(angle) * radius);
|
|
734
|
+
mesh.rotation.y = angle;
|
|
735
|
+
}
|
|
736
|
+
function lerpPath(waypoints, t) {
|
|
737
|
+
if (waypoints.length === 0) return { pos: new Vector3(0, 0, 0), tangent: new Vector3(0, 0, 1) };
|
|
738
|
+
if (waypoints.length === 1) return { pos: waypoints[0].clone(), tangent: new Vector3(0, 0, 1) };
|
|
739
|
+
const segments = waypoints.length - 1;
|
|
740
|
+
const scaled = Math.max(0, Math.min(1, t)) * segments;
|
|
741
|
+
const i = Math.min(segments - 1, Math.floor(scaled));
|
|
742
|
+
const local = scaled - i;
|
|
743
|
+
const a = waypoints[i];
|
|
744
|
+
const b = waypoints[i + 1];
|
|
745
|
+
const pos = Vector3.Lerp(a, b, local);
|
|
746
|
+
const tangent = b.subtract(a).normalize();
|
|
747
|
+
return { pos, tangent };
|
|
748
|
+
}
|
|
749
|
+
function placeOnPath(node, mesh, bounds, opts) {
|
|
750
|
+
const faceForward = opts.faceForward ?? true;
|
|
751
|
+
let t;
|
|
752
|
+
if (opts.byIndex) {
|
|
753
|
+
const total = opts.nodeCount ?? Math.max(1, bounds.maxX - bounds.minX);
|
|
754
|
+
const idx = node.position.x;
|
|
755
|
+
t = total > 1 ? idx / (total - 1) : 0.5;
|
|
756
|
+
} else {
|
|
757
|
+
const { cx } = normalizedCenter(node, bounds);
|
|
758
|
+
t = cx;
|
|
759
|
+
}
|
|
760
|
+
const { pos, tangent } = lerpPath(opts.waypoints, t);
|
|
761
|
+
mesh.position = pos;
|
|
762
|
+
if (faceForward) {
|
|
763
|
+
mesh.rotation.y = Math.atan2(tangent.x, tangent.z);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function placeOnSphere(node, mesh, bounds, opts = {}) {
|
|
767
|
+
const radius = opts.radius ?? 5;
|
|
768
|
+
const center = opts.center ?? new Vector3(0, 0, 0);
|
|
769
|
+
const latSpan = opts.latSpan ?? Math.PI * 0.7;
|
|
770
|
+
const lonSpan = opts.lonSpan ?? Math.PI * 2;
|
|
771
|
+
const { cx, cy } = normalizedCenter(node, bounds);
|
|
772
|
+
const lon = (cx - 0.5) * lonSpan;
|
|
773
|
+
const lat = (0.5 - cy) * latSpan;
|
|
774
|
+
const r = radius;
|
|
775
|
+
const x = r * Math.cos(lat) * Math.sin(lon);
|
|
776
|
+
const y = r * Math.sin(lat);
|
|
777
|
+
const z = r * Math.cos(lat) * Math.cos(lon);
|
|
778
|
+
mesh.position = new Vector3(center.x + x, center.y + y, center.z + z);
|
|
779
|
+
mesh.rotation.y = lon;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/index.ts
|
|
783
|
+
var WORLD_SCALE = 1 / 180;
|
|
784
|
+
function createBabylonAdapter(deps) {
|
|
785
|
+
return {
|
|
786
|
+
render(spec, ctx) {
|
|
787
|
+
const { w, h } = deps.sizeFor(spec);
|
|
788
|
+
const tex = new DynamicTexture(`tex-${ctx.nodeId}`, { width: w * TEX_SCALE, height: h * TEX_SCALE }, deps.scene3D, true);
|
|
789
|
+
tex.hasAlpha = true;
|
|
790
|
+
tex.vScale = -1;
|
|
791
|
+
tex.vOffset = 1;
|
|
792
|
+
const c = tex.getContext();
|
|
793
|
+
c.scale(TEX_SCALE, TEX_SCALE);
|
|
794
|
+
paintWidget(c, spec, w, h, ctx.selected);
|
|
795
|
+
tex.update(false);
|
|
796
|
+
const mat = new StandardMaterial(`mat-${ctx.nodeId}`, deps.scene3D);
|
|
797
|
+
mat.diffuseTexture = tex;
|
|
798
|
+
mat.emissiveTexture = tex;
|
|
799
|
+
mat.emissiveColor = new Color3(1, 1, 1);
|
|
800
|
+
mat.disableLighting = false;
|
|
801
|
+
mat.useAlphaFromDiffuseTexture = true;
|
|
802
|
+
const plane = MeshBuilder.CreatePlane(`node-${ctx.nodeId}`, { width: w * WORLD_SCALE, height: h * WORLD_SCALE }, deps.scene3D);
|
|
803
|
+
plane.material = mat;
|
|
804
|
+
plane.metadata = { nodeId: ctx.nodeId, kind: spec.kind };
|
|
805
|
+
return plane;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function placeOnCylinder(node, mesh, bounds, opts = {}) {
|
|
810
|
+
const radius = opts.radius ?? 6;
|
|
811
|
+
const arc = opts.arc ?? Math.PI * 0.9;
|
|
812
|
+
const rangeX = bounds.maxX - bounds.minX || 1;
|
|
813
|
+
const rangeY = bounds.maxY - bounds.minY || 1;
|
|
814
|
+
const cx = (node.position.x + (node.size?.w ?? 200) / 2 - bounds.minX) / rangeX;
|
|
815
|
+
const cy = (node.position.y + (node.size?.h ?? 120) / 2 - bounds.minY) / rangeY;
|
|
816
|
+
const angle = (cx - 0.5) * arc;
|
|
817
|
+
const height = (0.5 - cy) * 6;
|
|
818
|
+
mesh.position = new Vector3(Math.sin(angle) * radius, height, Math.cos(angle) * radius);
|
|
819
|
+
mesh.rotation = new Vector3(0, angle, 0);
|
|
820
|
+
}
|
|
821
|
+
function sceneBounds(scene) {
|
|
822
|
+
const xs = scene.nodes.map((n) => n.position.x);
|
|
823
|
+
const ys = scene.nodes.map((n) => n.position.y);
|
|
824
|
+
const xe = scene.nodes.map((n) => n.position.x + (n.size?.w ?? 200));
|
|
825
|
+
const ye = scene.nodes.map((n) => n.position.y + (n.size?.h ?? 120));
|
|
826
|
+
return {
|
|
827
|
+
minX: Math.min(...xs),
|
|
828
|
+
maxX: Math.max(...xe),
|
|
829
|
+
minY: Math.min(...ys),
|
|
830
|
+
maxY: Math.max(...ye)
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export { TEX_SCALE, createBabylonAdapter, createBillboard, createBuilding, createCard3D, createCurvedPanel, createCylinder, createDecal, createDisc, createMonitor, createPanel, createPillar, createSign, createSphere, createWidgetTexture, paintWidget, placeOnArc, placeOnCylinder, placeOnGrid, placeOnPath, placeOnSphere, placeOnWall, sceneBounds };
|
|
835
|
+
//# sourceMappingURL=index.js.map
|
|
836
|
+
//# sourceMappingURL=index.js.map
|