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