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