@almadar/ui 5.148.0 → 5.149.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.
Files changed (29) hide show
  1. package/dist/{GameAudioProvider-CPGwD49P.d.cts → GameAudioProvider-B48iXwz3.d.ts} +2 -50
  2. package/dist/{GameAudioProvider-CPGwD49P.d.ts → GameAudioProvider-CQAdPreB.d.cts} +2 -50
  3. package/dist/avl/index.cjs +438 -13
  4. package/dist/avl/index.js +438 -13
  5. package/dist/{avl-schema-parser-DVmrgdwc.d.cts → avl-schema-parser-CVzkNzg7.d.cts} +18 -9
  6. package/dist/{avl-schema-parser-BRJ77Yze.d.ts → avl-schema-parser-Cx_4SVg9.d.ts} +18 -9
  7. package/dist/{cn-BAn68sNO.d.cts → cn-BnAJZcNb.d.cts} +2 -8
  8. package/dist/{cn-CWxxLkri.d.ts → cn-DJTUjk1M.d.ts} +2 -8
  9. package/dist/components/index.cjs +448 -16
  10. package/dist/components/index.d.cts +269 -95
  11. package/dist/components/index.d.ts +269 -95
  12. package/dist/components/index.js +444 -18
  13. package/dist/lib/drawable/three/index.cjs +56 -29
  14. package/dist/lib/drawable/three/index.d.cts +4 -3
  15. package/dist/lib/drawable/three/index.d.ts +4 -3
  16. package/dist/lib/drawable/three/index.js +56 -29
  17. package/dist/lib/index.d.cts +3 -2
  18. package/dist/lib/index.d.ts +3 -2
  19. package/dist/{paintDispatch-Cb_hQj4Y.d.ts → paintDispatch-BjZjUbcb.d.cts} +63 -69
  20. package/dist/{paintDispatch-Cb_hQj4Y.d.cts → paintDispatch-CxdLjHQq.d.ts} +63 -69
  21. package/dist/providers/index.cjs +438 -13
  22. package/dist/providers/index.d.cts +2 -1
  23. package/dist/providers/index.d.ts +2 -1
  24. package/dist/providers/index.js +438 -13
  25. package/dist/runtime/index.cjs +438 -13
  26. package/dist/runtime/index.js +438 -13
  27. package/dist/types-B3nHgnMG.d.cts +51 -0
  28. package/dist/types-B3nHgnMG.d.ts +51 -0
  29. package/package.json +3 -3
package/dist/avl/index.js CHANGED
@@ -14041,9 +14041,10 @@ function Canvas({
14041
14041
  drawables: [...drawables ?? [], ...childDrawables],
14042
14042
  isLoading,
14043
14043
  cameraMode: to3DCameraMode(camera?.mode),
14044
- ...zoom !== void 0 ? { scale: zoom } : {},
14044
+ ...zoom !== void 0 ? { zoom } : {},
14045
14045
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
14046
14046
  ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
14047
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
14047
14048
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
14048
14049
  unitScale,
14049
14050
  backgroundColor,
@@ -17095,23 +17096,188 @@ var init_AuthLayout = __esm({
17095
17096
  AuthLayout.displayName = "AuthLayout";
17096
17097
  }
17097
17098
  });
17098
- var BiologyCanvas;
17099
+ function LearningScene3D({
17100
+ className,
17101
+ width = 600,
17102
+ height = 400,
17103
+ title,
17104
+ backgroundColor,
17105
+ drawables,
17106
+ camera,
17107
+ lighting,
17108
+ post,
17109
+ showGrid = false,
17110
+ shadows,
17111
+ interactive,
17112
+ isLoading,
17113
+ error,
17114
+ onItemClick
17115
+ }) {
17116
+ const instanceId = useId().replace(/[^a-zA-Z0-9]/g, "");
17117
+ const clickEvent = onItemClick ? `LEARNING_SCENE_3D.ITEM_CLICK.${instanceId}` : void 0;
17118
+ const onItemClickRef = useRef(onItemClick);
17119
+ onItemClickRef.current = onItemClick;
17120
+ useEventListener(`UI:${clickEvent ?? "LEARNING_SCENE_3D.ITEM_CLICK.__disabled"}`, (event) => {
17121
+ const unitId = event.payload?.unitId;
17122
+ if (typeof unitId === "string") onItemClickRef.current?.(unitId);
17123
+ });
17124
+ const props3d = {
17125
+ drawables,
17126
+ isLoading,
17127
+ error: error ? error.message : null,
17128
+ cameraMode: camera?.mode ?? "perspective",
17129
+ ...camera?.zoom !== void 0 ? { zoom: camera.zoom } : {},
17130
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
17131
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
17132
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
17133
+ ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
17134
+ backgroundColor,
17135
+ showGrid,
17136
+ ...shadows !== void 0 ? { shadows } : {},
17137
+ ...interactive !== void 0 ? { controlsEnabled: interactive } : {},
17138
+ lighting,
17139
+ post,
17140
+ ...clickEvent !== void 0 ? { unitClickEvent: clickEvent } : {}
17141
+ };
17142
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
17143
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
17144
+ /* @__PURE__ */ jsx("div", { style: { width, height, display: "flex" }, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Canvas3DHost2, { ...props3d }) }) })
17145
+ ] }) });
17146
+ }
17147
+ function meshSphere(id, x, y, z, radius, color, opts) {
17148
+ return {
17149
+ type: "draw-mesh",
17150
+ ...id !== void 0 ? { id } : {},
17151
+ shape: opts?.shape ?? "sphere",
17152
+ position: { x, y, z },
17153
+ radius,
17154
+ pivot: "center",
17155
+ segments: opts?.segments ?? 24,
17156
+ material: { color, ...opts?.material },
17157
+ ...opts?.opacity !== void 0 ? { opacity: opts.opacity } : {}
17158
+ };
17159
+ }
17160
+ function axisRotation(from, to) {
17161
+ const wx = to[0] - from[0];
17162
+ const wy = to[2] - from[2];
17163
+ const wz = to[1] - from[1];
17164
+ const len = Math.sqrt(wx * wx + wy * wy + wz * wz);
17165
+ const tilt = Math.acos(Math.min(1, Math.max(-1, len > 0 ? wy / len : 1)));
17166
+ return [0, Math.atan2(wz, -wx), tilt];
17167
+ }
17168
+ function segmentLength(from, to) {
17169
+ const dx = to[0] - from[0];
17170
+ const dy = to[1] - from[1];
17171
+ const dz = to[2] - from[2];
17172
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
17173
+ }
17174
+ function cylinderBetween(from, to, radius, color) {
17175
+ const len = segmentLength(from, to);
17176
+ if (len < 1e-6) return null;
17177
+ return {
17178
+ type: "draw-mesh",
17179
+ shape: "cylinder",
17180
+ position: { x: (from[0] + to[0]) / 2, y: (from[1] + to[1]) / 2, z: (from[2] + to[2]) / 2 },
17181
+ radius,
17182
+ height: len,
17183
+ rotation: axisRotation(from, to),
17184
+ pivot: "center",
17185
+ segments: 12,
17186
+ material: { color }
17187
+ };
17188
+ }
17189
+ function arrowBetween(from, to, color, shaftRadius = 0.08) {
17190
+ const len = segmentLength(from, to);
17191
+ if (len < 1e-6) return null;
17192
+ const tipLen = Math.min(shaftRadius * 8, len * 0.35);
17193
+ const k = (len - tipLen) / len;
17194
+ const delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
17195
+ const shaftEnd = [delta[0] * k, delta[1] * k, delta[2] * k];
17196
+ const tipStart = shaftEnd;
17197
+ const tipEnd = delta;
17198
+ const shaft = cylinderBetween([0, 0, 0], shaftEnd, shaftRadius, color);
17199
+ const tipLenActual = segmentLength(tipStart, tipEnd);
17200
+ const tip = {
17201
+ type: "draw-mesh",
17202
+ shape: "cone",
17203
+ position: {
17204
+ x: (tipStart[0] + tipEnd[0]) / 2,
17205
+ y: (tipStart[1] + tipEnd[1]) / 2,
17206
+ z: (tipStart[2] + tipEnd[2]) / 2
17207
+ },
17208
+ radius: shaftRadius * 3,
17209
+ height: tipLenActual,
17210
+ rotation: axisRotation(tipStart, tipEnd),
17211
+ pivot: "center",
17212
+ segments: 12,
17213
+ material: { color }
17214
+ };
17215
+ return {
17216
+ type: "draw-group",
17217
+ position: { x: from[0], y: from[1], z: from[2] },
17218
+ items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
17219
+ };
17220
+ }
17221
+ function billboardLabel(text, x, y, z, opts) {
17222
+ return {
17223
+ type: "draw-text",
17224
+ text,
17225
+ position: { x, y, z },
17226
+ color: opts?.color ?? "#111827"
17227
+ };
17228
+ }
17229
+ function labelColorForBackground(backgroundColor) {
17230
+ const m = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i.exec(backgroundColor ?? "");
17231
+ if (!m) return "#111827";
17232
+ const hex = m[1] !== void 0 ? m[1].split("").map((c) => c + c).join("") : m[2];
17233
+ const r2 = parseInt(hex.slice(0, 2), 16) / 255;
17234
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
17235
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
17236
+ const luminance = 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
17237
+ return luminance < 0.5 ? "#e5e7eb" : "#111827";
17238
+ }
17239
+ function get3DClickPayload(onShapeClick, idToIndex) {
17240
+ if (!onShapeClick) return void 0;
17241
+ return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
17242
+ }
17243
+ var Canvas3DHost2;
17244
+ var init_learningScene3D = __esm({
17245
+ "components/learning/molecules/learningScene3D.tsx"() {
17246
+ "use client";
17247
+ init_atoms();
17248
+ init_Stack();
17249
+ init_useEventBus();
17250
+ Canvas3DHost2 = lazy(
17251
+ () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
17252
+ );
17253
+ LearningScene3D.displayName = "LearningScene3D";
17254
+ }
17255
+ });
17256
+ var biologyLog, BiologyCanvas;
17099
17257
  var init_BiologyCanvas = __esm({
17100
17258
  "components/learning/molecules/BiologyCanvas.tsx"() {
17101
17259
  "use client";
17102
17260
  init_atoms();
17103
17261
  init_Stack();
17104
17262
  init_LearningCanvas();
17263
+ init_learningScene3D();
17264
+ biologyLog = createLogger("almadar:ui:biology-canvas");
17105
17265
  BiologyCanvas = ({
17106
17266
  className,
17107
17267
  width = 600,
17108
17268
  height = 400,
17109
17269
  title,
17110
17270
  backgroundColor,
17271
+ mode = "2d",
17272
+ camera,
17273
+ lighting,
17274
+ post,
17111
17275
  nodes = [],
17112
17276
  edges = [],
17113
17277
  shapes = [],
17114
- interactive = false,
17278
+ showGrid,
17279
+ shadows,
17280
+ interactive,
17115
17281
  animate = false,
17116
17282
  onShapeClick,
17117
17283
  isLoading,
@@ -17172,6 +17338,75 @@ var init_BiologyCanvas = __esm({
17172
17338
  out.push(...shapes);
17173
17339
  return out;
17174
17340
  }, [nodes, edges, shapes]);
17341
+ const drawables3D = useMemo(() => {
17342
+ if (mode !== "3d") return [];
17343
+ if (shapes.length > 0) {
17344
+ biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
17345
+ }
17346
+ const out = [];
17347
+ const labelColor = labelColorForBackground(backgroundColor);
17348
+ const nodeById = /* @__PURE__ */ new Map();
17349
+ for (const n of nodes) {
17350
+ if (n.id) nodeById.set(n.id, n);
17351
+ }
17352
+ for (const e of edges) {
17353
+ const a = nodeById.get(e.from);
17354
+ const b = nodeById.get(e.to);
17355
+ if (!a || !b) continue;
17356
+ const edgeRadius = Math.max(0.04, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.12);
17357
+ const edge = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], edgeRadius, e.color ?? "#9ca3af");
17358
+ if (edge) out.push(edge);
17359
+ if (e.label) {
17360
+ out.push(
17361
+ billboardLabel(
17362
+ e.label,
17363
+ (a.x + b.x) / 2,
17364
+ (a.y + b.y) / 2,
17365
+ ((a.z ?? 0) + (b.z ?? 0)) / 2,
17366
+ { color: labelColor }
17367
+ )
17368
+ );
17369
+ }
17370
+ }
17371
+ for (const n of nodes) {
17372
+ const radius = n.radius ?? 0.5;
17373
+ const nz = n.z ?? 0;
17374
+ out.push(meshSphere(n.id, n.x, n.y, nz, radius, n.color ?? "#16a34a", { shape: n.shape, ...n.opacity !== void 0 ? { opacity: n.opacity } : {} }));
17375
+ if (n.label) {
17376
+ out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
17377
+ }
17378
+ }
17379
+ return out;
17380
+ }, [mode, nodes, edges, shapes, backgroundColor]);
17381
+ const nodeIndexById = useMemo(() => {
17382
+ const m = /* @__PURE__ */ new Map();
17383
+ nodes.forEach((n, i) => {
17384
+ if (n.id) m.set(n.id, i);
17385
+ });
17386
+ return m;
17387
+ }, [nodes]);
17388
+ if (mode === "3d") {
17389
+ return /* @__PURE__ */ jsx(
17390
+ LearningScene3D,
17391
+ {
17392
+ className,
17393
+ width,
17394
+ height,
17395
+ title,
17396
+ backgroundColor,
17397
+ drawables: drawables3D,
17398
+ camera,
17399
+ lighting,
17400
+ post,
17401
+ showGrid,
17402
+ shadows,
17403
+ interactive,
17404
+ isLoading,
17405
+ error,
17406
+ onItemClick: get3DClickPayload(onShapeClick, nodeIndexById)
17407
+ }
17408
+ );
17409
+ }
17175
17410
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
17176
17411
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
17177
17412
  /* @__PURE__ */ jsx(
@@ -17181,7 +17416,7 @@ var init_BiologyCanvas = __esm({
17181
17416
  height,
17182
17417
  backgroundColor,
17183
17418
  shapes: derivedShapes,
17184
- interactive,
17419
+ interactive: interactive ?? false,
17185
17420
  animate,
17186
17421
  onShapeClick,
17187
17422
  isLoading,
@@ -24021,24 +24256,41 @@ var init_ChatBar = __esm({
24021
24256
  ChatBar.displayName = "ChatBar";
24022
24257
  }
24023
24258
  });
24024
- var ChemistryCanvas;
24259
+ function bondPerpendicular(a, b) {
24260
+ const dx = b[0] - a[0];
24261
+ const dy = b[1] - a[1];
24262
+ const px = dy;
24263
+ const py = -dx;
24264
+ const len = Math.sqrt(px * px + py * py);
24265
+ if (len < 1e-6) return [1, 0, 0];
24266
+ return [px / len, py / len, 0];
24267
+ }
24268
+ var chemistryLog, ChemistryCanvas;
24025
24269
  var init_ChemistryCanvas = __esm({
24026
24270
  "components/learning/molecules/ChemistryCanvas.tsx"() {
24027
24271
  "use client";
24028
24272
  init_atoms();
24029
24273
  init_Stack();
24030
24274
  init_LearningCanvas();
24275
+ init_learningScene3D();
24276
+ chemistryLog = createLogger("almadar:ui:chemistry-canvas");
24031
24277
  ChemistryCanvas = ({
24032
24278
  className,
24033
24279
  width = 600,
24034
24280
  height = 400,
24035
24281
  title,
24036
24282
  backgroundColor,
24283
+ mode = "2d",
24284
+ camera,
24285
+ lighting,
24286
+ post,
24037
24287
  atoms = [],
24038
24288
  bonds = [],
24039
24289
  arrows = [],
24040
24290
  shapes = [],
24041
- interactive = false,
24291
+ showGrid,
24292
+ shadows,
24293
+ interactive,
24042
24294
  animate = false,
24043
24295
  onShapeClick,
24044
24296
  isLoading,
@@ -24117,6 +24369,88 @@ var init_ChemistryCanvas = __esm({
24117
24369
  out.push(...shapes);
24118
24370
  return out;
24119
24371
  }, [atoms, bonds, arrows, shapes]);
24372
+ const drawables3D = useMemo(() => {
24373
+ if (mode !== "3d") return [];
24374
+ if (shapes.length > 0) {
24375
+ chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
24376
+ }
24377
+ const out = [];
24378
+ const labelColor = labelColorForBackground(backgroundColor);
24379
+ const atomById = /* @__PURE__ */ new Map();
24380
+ for (const a of atoms) {
24381
+ if (a.id) atomById.set(a.id, a);
24382
+ }
24383
+ for (const b of bonds) {
24384
+ const a = atomById.get(b.from);
24385
+ const c = atomById.get(b.to);
24386
+ if (!a || !c) continue;
24387
+ const color = b.color ?? "#6b7280";
24388
+ const from = [a.x, a.y, a.z ?? 0];
24389
+ const to = [c.x, c.y, c.z ?? 0];
24390
+ const perp = bondPerpendicular(from, to);
24391
+ const bondRadius = Math.max(0.06, Math.min(a.radius ?? 0.45, c.radius ?? 0.45) * 0.22);
24392
+ const step = bondRadius * 2.2;
24393
+ const offsets = b.type === "double" ? [-step, step] : b.type === "triple" ? [-step, 0, step] : [0];
24394
+ for (const off of offsets) {
24395
+ const bond = cylinderBetween(
24396
+ [from[0] + perp[0] * off, from[1] + perp[1] * off, from[2] + perp[2] * off],
24397
+ [to[0] + perp[0] * off, to[1] + perp[1] * off, to[2] + perp[2] * off],
24398
+ bondRadius,
24399
+ color
24400
+ );
24401
+ if (bond) out.push(bond);
24402
+ }
24403
+ }
24404
+ for (const a of arrows) {
24405
+ const angle = (a.angle ?? 0) * (Math.PI / 180);
24406
+ const len = a.length ?? 60;
24407
+ const x2 = a.x + Math.cos(angle) * len;
24408
+ const y2 = a.y + Math.sin(angle) * len;
24409
+ const arrow = arrowBetween([a.x, a.y, 0], [x2, y2, 0], a.color ?? "#dc2626");
24410
+ if (arrow) out.push(arrow);
24411
+ if (a.label) {
24412
+ out.push(billboardLabel(a.label, (a.x + x2) / 2, (a.y + y2) / 2, 0, { color: labelColor }));
24413
+ }
24414
+ }
24415
+ for (const a of atoms) {
24416
+ const radius = a.radius ?? 0.45;
24417
+ const az = a.z ?? 0;
24418
+ out.push(meshSphere(a.id, a.x, a.y, az, radius, a.color ?? "#2563eb"));
24419
+ if (a.element) {
24420
+ out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
24421
+ }
24422
+ }
24423
+ return out;
24424
+ }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
24425
+ const atomIndexById = useMemo(() => {
24426
+ const m = /* @__PURE__ */ new Map();
24427
+ atoms.forEach((a, i) => {
24428
+ if (a.id) m.set(a.id, i);
24429
+ });
24430
+ return m;
24431
+ }, [atoms]);
24432
+ if (mode === "3d") {
24433
+ return /* @__PURE__ */ jsx(
24434
+ LearningScene3D,
24435
+ {
24436
+ className,
24437
+ width,
24438
+ height,
24439
+ title,
24440
+ backgroundColor,
24441
+ drawables: drawables3D,
24442
+ camera,
24443
+ lighting,
24444
+ post,
24445
+ showGrid,
24446
+ shadows,
24447
+ interactive,
24448
+ isLoading,
24449
+ error,
24450
+ onItemClick: get3DClickPayload(onShapeClick, atomIndexById)
24451
+ }
24452
+ );
24453
+ }
24120
24454
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
24121
24455
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
24122
24456
  /* @__PURE__ */ jsx(
@@ -24126,7 +24460,7 @@ var init_ChemistryCanvas = __esm({
24126
24460
  height,
24127
24461
  backgroundColor,
24128
24462
  shapes: derivedShapes,
24129
- interactive,
24463
+ interactive: interactive ?? false,
24130
24464
  animate,
24131
24465
  onShapeClick,
24132
24466
  isLoading,
@@ -30431,19 +30765,25 @@ var init_MathCanvas = __esm({
30431
30765
  };
30432
30766
  }
30433
30767
  });
30434
- var PhysicsCanvas;
30768
+ var physicsLog2, PhysicsCanvas;
30435
30769
  var init_PhysicsCanvas = __esm({
30436
30770
  "components/learning/molecules/PhysicsCanvas.tsx"() {
30437
30771
  "use client";
30438
30772
  init_atoms();
30439
30773
  init_Stack();
30440
30774
  init_LearningCanvas();
30775
+ init_learningScene3D();
30776
+ physicsLog2 = createLogger("almadar:ui:physics-canvas");
30441
30777
  PhysicsCanvas = ({
30442
30778
  className,
30443
30779
  width = 600,
30444
30780
  height = 400,
30445
30781
  title,
30446
30782
  backgroundColor,
30783
+ mode = "2d",
30784
+ camera,
30785
+ lighting,
30786
+ post,
30447
30787
  bodies = [],
30448
30788
  constraints = [],
30449
30789
  showVelocity = true,
@@ -30451,7 +30791,9 @@ var init_PhysicsCanvas = __esm({
30451
30791
  velocityScale = 20,
30452
30792
  forceScale = 20,
30453
30793
  shapes = [],
30454
- interactive = false,
30794
+ showGrid,
30795
+ shadows,
30796
+ interactive,
30455
30797
  animate = false,
30456
30798
  onShapeClick,
30457
30799
  isLoading,
@@ -30523,6 +30865,89 @@ var init_PhysicsCanvas = __esm({
30523
30865
  out.push(...shapes);
30524
30866
  return out;
30525
30867
  }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30868
+ const drawables3D = useMemo(() => {
30869
+ if (mode !== "3d") return [];
30870
+ if (shapes.length > 0) {
30871
+ physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
30872
+ }
30873
+ const out = [];
30874
+ const labelColor = labelColorForBackground(backgroundColor);
30875
+ const bodyById = /* @__PURE__ */ new Map();
30876
+ for (const b of bodies) {
30877
+ if (b.id) bodyById.set(b.id, b);
30878
+ }
30879
+ for (const c of constraints) {
30880
+ const a = bodyById.get(c.from);
30881
+ const b = bodyById.get(c.to);
30882
+ if (!a || !b) continue;
30883
+ const rodRadius = Math.max(0.05, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.15);
30884
+ const rod = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], rodRadius, c.color ?? "#9ca3af");
30885
+ if (rod) out.push(rod);
30886
+ }
30887
+ for (const b of bodies) {
30888
+ const radius = b.radius ?? 0.5;
30889
+ const bz = b.z ?? 0;
30890
+ out.push(meshSphere(b.id, b.x, b.y, bz, radius, b.color ?? "#2563eb"));
30891
+ if (b.label) {
30892
+ out.push(billboardLabel(b.label, b.x, b.y, bz + radius, { color: labelColor }));
30893
+ }
30894
+ const vx = b.vx ?? 0;
30895
+ const vy = b.vy ?? 0;
30896
+ const vz = b.vz ?? 0;
30897
+ const arrowRadius = Math.max(0.05, radius * 0.15);
30898
+ if (showVelocity && (vx !== 0 || vy !== 0 || vz !== 0)) {
30899
+ const arrow = arrowBetween(
30900
+ [b.x, b.y, bz],
30901
+ [b.x + vx * velocityScale, b.y + vy * velocityScale, bz + vz * velocityScale],
30902
+ "#16a34a",
30903
+ arrowRadius
30904
+ );
30905
+ if (arrow) out.push(arrow);
30906
+ }
30907
+ const fx = b.fx ?? 0;
30908
+ const fy = b.fy ?? 0;
30909
+ const fz = b.fz ?? 0;
30910
+ if (showForces && (fx !== 0 || fy !== 0 || fz !== 0)) {
30911
+ const arrow = arrowBetween(
30912
+ [b.x, b.y, bz],
30913
+ [b.x + fx * forceScale, b.y + fy * forceScale, bz + fz * forceScale],
30914
+ "#dc2626",
30915
+ arrowRadius
30916
+ );
30917
+ if (arrow) out.push(arrow);
30918
+ }
30919
+ }
30920
+ return out;
30921
+ }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
30922
+ const bodyIndexById = useMemo(() => {
30923
+ const m = /* @__PURE__ */ new Map();
30924
+ bodies.forEach((b, i) => {
30925
+ if (b.id) m.set(b.id, i);
30926
+ });
30927
+ return m;
30928
+ }, [bodies]);
30929
+ if (mode === "3d") {
30930
+ return /* @__PURE__ */ jsx(
30931
+ LearningScene3D,
30932
+ {
30933
+ className,
30934
+ width,
30935
+ height,
30936
+ title,
30937
+ backgroundColor,
30938
+ drawables: drawables3D,
30939
+ camera,
30940
+ lighting,
30941
+ post,
30942
+ showGrid,
30943
+ shadows,
30944
+ interactive,
30945
+ isLoading,
30946
+ error,
30947
+ onItemClick: get3DClickPayload(onShapeClick, bodyIndexById)
30948
+ }
30949
+ );
30950
+ }
30526
30951
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
30527
30952
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
30528
30953
  /* @__PURE__ */ jsx(
@@ -30532,7 +30957,7 @@ var init_PhysicsCanvas = __esm({
30532
30957
  height,
30533
30958
  backgroundColor,
30534
30959
  shapes: derivedShapes,
30535
- interactive,
30960
+ interactive: interactive ?? false,
30536
30961
  animate,
30537
30962
  onShapeClick,
30538
30963
  isLoading,
@@ -31150,12 +31575,12 @@ var init_MapView = __esm({
31150
31575
  shadowSize: [41, 41]
31151
31576
  });
31152
31577
  L.Marker.prototype.options.icon = defaultIcon;
31153
- const { useEffect: useEffect64, useRef: useRef64, useCallback: useCallback96, useState: useState100 } = React94__default;
31578
+ const { useEffect: useEffect64, useRef: useRef65, useCallback: useCallback96, useState: useState100 } = React94__default;
31154
31579
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31155
31580
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31156
31581
  function MapUpdater({ centerLat, centerLng, zoom }) {
31157
31582
  const map = useMap();
31158
- const prevRef = useRef64({ centerLat, centerLng, zoom });
31583
+ const prevRef = useRef65({ centerLat, centerLng, zoom });
31159
31584
  useEffect64(() => {
31160
31585
  const prev = prevRef.current;
31161
31586
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -41569,7 +41994,7 @@ function getEnumOptions(field) {
41569
41994
  }));
41570
41995
  }
41571
41996
  const validation = field.validation;
41572
- if (validation?.enum && Array.isArray(validation.enum)) {
41997
+ if (validation?.enum && validation.enum.length > 0) {
41573
41998
  return validation.enum.map((v) => ({
41574
41999
  value: v,
41575
42000
  label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " ")
@@ -1,7 +1,7 @@
1
1
  import { AnimationName, Asset, ScenePos, EventEmit, JsonObject, JsonValue } from '@almadar/core';
2
2
  import React__default from 'react';
3
3
  import * as THREE from 'three';
4
- import { D as DrawableNode } from './paintDispatch-Cb_hQj4Y.cjs';
4
+ import { D as DrawableNode } from './paintDispatch-BjZjUbcb.cjs';
5
5
 
6
6
  /**
7
7
  * Sprite Sheet Animation Types
@@ -307,10 +307,11 @@ interface CameraState {
307
307
  * (forwarded as `followTarget`), falling back to the scene centre.
308
308
  *
309
309
  * Interaction: keyboard maps to semantic events (device-agnostic input). Pointer
310
- * click/hover on neutral drawables needs a per-entity id + a scene-space raycast
311
- * the descriptors don't yet carry that hit-test is a tracked fork
312
- * (docs/Almadar_Std_Game_V2_PLAN.md); the click/hover event props are accepted but
313
- * not yet emitted from a raycast.
310
+ * click on a tagged drawable (a descriptor carrying `DrawableBase.id`) raycasts to
311
+ * the mesh and walks up to the tagged ancestor `unitClickEvent {unitId,x,z}`;
312
+ * a click that hits no tagged mesh falls back to the ground-plane cell raycast →
313
+ * `tileClickEvent`. Pointer hover/feature events are accepted but not yet
314
+ * emitted (docs/Almadar_Std_Gaps.md S-CANVAS3D-HOVER-DEAD).
314
315
  *
315
316
  * @packageDocumentation
316
317
  */
@@ -422,8 +423,9 @@ interface Canvas3DHostProps {
422
423
  terrain?: string;
423
424
  elevation?: number;
424
425
  }>;
425
- /** Declarative event: unit click. Emitted `{ unitId, x, z }` when the raycast lands on a
426
- * cell whose descriptor carries an `id` (a tagged unit sprite). */
426
+ /** Declarative event: unit click. Emitted `{ unitId, x, z }` when the raycast hits a
427
+ * mesh tagged with a descriptor `id` (or lands on a ground cell whose descriptor
428
+ * carries an `id`). */
427
429
  unitClickEvent?: EventEmit<{
428
430
  unitId: string;
429
431
  x: number;
@@ -477,8 +479,12 @@ interface Canvas3DHostProps {
477
479
  loadingMessage?: string;
478
480
  /** Unit draw-size multiplier (accepted for API parity; sizing is drawable-authored). */
479
481
  unitScale?: number;
480
- /** Board zoom (accepted for API parity; 3D zoom is camera-driven, not group-scaled). */
481
- scale?: number;
482
+ /** Board zoom the neutral `Camera.zoom`. Dollies the camera: the framing
483
+ * distance is divided by `zoom` (undefined/1 = default framing; <= 0 ignored). */
484
+ zoom?: number;
485
+ /** Enable the orbit camera controls. Default true; `follow`/`chase` modes always
486
+ * disable them (the follow camera is authoritative). */
487
+ controlsEnabled?: boolean;
482
488
  /** Maps a keydown `e.code` → the board's SEMANTIC event (device-agnostic input). */
483
489
  keyMap?: Record<string, string>;
484
490
  /** Maps a keyup `e.code` → the board's SEMANTIC event. */
@@ -494,6 +500,9 @@ interface Canvas3DHostProps {
494
500
  fov?: number;
495
501
  /** Orbit the mode's framing position around the vertical axis through the target, in radians — the neutral `Camera.azimuth`. */
496
502
  azimuth?: number;
503
+ /** Orbit height angle above the ground plane, in radians — the neutral `Camera.elevation`
504
+ * (perspective mode). Omitted → the mode's fixed framing height. */
505
+ elevation?: number;
497
506
  /** 3D scene light rig as data — ambient/directional/hemisphere/point lights + optional
498
507
  * 'room' environment. Omitted → the standard fixed rig (this host's current
499
508
  * hardcoded lights, unchanged). */