@almadar/ui 6.56.0 → 6.57.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 (59) hide show
  1. package/dist/{ArbitraryClassCompiler-DvzdeHxc.d.ts → ArbitraryClassCompiler-tChQ3nVq.d.ts} +15 -1
  2. package/dist/{FileTree-DUi7e7Z8.d.ts → FileTree-BSfgp-HH.d.ts} +29 -2
  3. package/dist/Typography-LZTP72Y4.js +5 -0
  4. package/dist/{UISlotRenderer-QTHFT6Z4.js → UISlotRenderer-NCGDIXN4.js} +8 -8
  5. package/dist/avl/index.cjs +4832 -4212
  6. package/dist/avl/index.d.ts +74 -5
  7. package/dist/avl/index.js +68 -711
  8. package/dist/{avl-schema-parser-C0wnAp5x.d.ts → avl-schema-parser-CH1QoZw1.d.ts} +1 -1
  9. package/dist/{chunk-HCSJHRC4.js → chunk-4ZGXIW3B.js} +1 -1
  10. package/dist/{chunk-RNY5LJXJ.js → chunk-723MJUU3.js} +1 -1
  11. package/dist/{chunk-6EV3OEBQ.js → chunk-CHNC4UYE.js} +3 -2
  12. package/dist/{chunk-SGJHVPHC.js → chunk-GZGDBS4S.js} +2 -2
  13. package/dist/{chunk-Y53Q55H5.js → chunk-HA7CJWQX.js} +1123 -1121
  14. package/dist/{chunk-JRW33U35.js → chunk-I5QOYW2Z.js} +1 -1
  15. package/dist/{chunk-KQXXO42S.js → chunk-KJGVLXEN.js} +78 -1
  16. package/dist/{chunk-HRCR53OW.js → chunk-LZRW6GXG.js} +18 -3
  17. package/dist/{chunk-2LFBQ4IS.js → chunk-MZIDOJMD.js} +2 -2
  18. package/dist/{chunk-QGH4NAQO.js → chunk-N7JIG57B.js} +9 -4
  19. package/dist/{chunk-XZKGRSIO.js → chunk-PYJ2UQWM.js} +2487 -1156
  20. package/dist/{chunk-FQM37C56.js → chunk-RHP5WL55.js} +61 -55
  21. package/dist/chunk-YYYHH5BA.js +2519 -0
  22. package/dist/{cn-ClXGlIOY.d.ts → cn-DIql5232.d.ts} +152 -24
  23. package/dist/components/index.cjs +6876 -3072
  24. package/dist/components/index.d.ts +45 -94
  25. package/dist/components/index.js +14 -16
  26. package/dist/context/index.cjs +23 -0
  27. package/dist/context/index.d.ts +2 -2
  28. package/dist/context/index.js +20 -1
  29. package/dist/hooks/index.cjs +2544 -7
  30. package/dist/hooks/index.d.ts +4 -1
  31. package/dist/hooks/index.js +7 -6
  32. package/dist/lib/drawable/three/index.cjs +22 -6
  33. package/dist/lib/drawable/three/index.d.ts +1 -1
  34. package/dist/lib/drawable/three/index.js +7 -7
  35. package/dist/lib/index.cjs +18 -3
  36. package/dist/lib/index.d.ts +1 -1
  37. package/dist/lib/index.js +4 -5
  38. package/dist/locales/index.cjs +54 -9
  39. package/dist/locales/index.js +2 -2489
  40. package/dist/providers/index.cjs +4035 -2382
  41. package/dist/providers/index.d.ts +2 -2
  42. package/dist/providers/index.js +25 -14
  43. package/dist/runtime/index.cjs +4092 -2447
  44. package/dist/runtime/index.d.ts +0 -13
  45. package/dist/runtime/index.js +11 -10
  46. package/dist/ssr/index.cjs +23 -6
  47. package/dist/ssr/index.js +23 -6
  48. package/dist/{useEventBus-JUAH3EFW.js → useEventBus-SWRN475O.js} +1 -1
  49. package/locales/ar.json +18 -3
  50. package/locales/en.json +18 -3
  51. package/locales/sl.json +18 -3
  52. package/package.json +6 -6
  53. package/tailwind-preset.cjs +3 -0
  54. package/themes/_fonts.css +1 -1
  55. package/themes/_presets.css +1 -0
  56. package/themes/kflow.css +299 -0
  57. package/dist/Typography-7PDT62PG.js +0 -5
  58. package/dist/chunk-F4ZTPKZL.js +0 -284
  59. package/dist/chunk-OKWD7LPO.js +0 -80
@@ -1,10 +1,10 @@
1
1
  import { useTapReveal } from './chunk-XJTHMW4G.js';
2
- import { useEventBus } from './chunk-6EV3OEBQ.js';
2
+ import { useEventBus } from './chunk-CHNC4UYE.js';
3
3
  import { updateAssetStatus } from './chunk-Q47ZN2DW.js';
4
4
  import { cn, domPassthrough } from './chunk-64WACXM7.js';
5
5
  import { __publicField } from './chunk-QPXIAUJD.js';
6
6
  import { createLogger } from '@almadar/logger';
7
- import React, { useCallback, createContext, useContext, useState, useLayoutEffect } from 'react';
7
+ import React, { useCallback, createContext, useState, useLayoutEffect, useContext } from 'react';
8
8
  import { useThemeScope, useTheme } from '@almadar/ui/context';
9
9
  import { jsx } from 'react/jsx-runtime';
10
10
  import { renderUiEntriesOf } from '@almadar/core/patterns';
@@ -182,6 +182,7 @@ var EFFECT_OPERATOR_CATEGORY = {
182
182
  "notify": "communication",
183
183
  "send-server": "communication",
184
184
  "call-service": "communication",
185
+ "cancel-call": "communication",
185
186
  "integration/http": "communication",
186
187
  "integration/github-get-repo": "communication",
187
188
  "integration/github-create-issue": "communication",
@@ -253,6 +254,7 @@ var EFFECT_OPERATOR_ZONE = {
253
254
  "send-server": "bus",
254
255
  "notify": "toast",
255
256
  "call-service": "remote",
257
+ "cancel-call": "remote",
256
258
  "integration/http": "remote",
257
259
  "integration/github-get-repo": "remote",
258
260
  "integration/github-create-issue": "remote",
@@ -1149,593 +1151,409 @@ var CONFETTI_BURST_KEYFRAMES = `
1149
1151
  }
1150
1152
  }
1151
1153
  `;
1152
-
1153
- // lib/drawable/contract.ts
1154
- function isValidScenePos(pos) {
1155
- return Number.isFinite(pos?.x) && Number.isFinite(pos?.y);
1156
- }
1157
- function spriteAssetState(node) {
1158
- if (!isValidScenePos(node.position)) return "invalid-position";
1159
- return typeof node.asset?.url === "string" && node.asset.url.length > 0 ? "ready" : "asset-unset";
1154
+ var log2 = createLogger("almadar:ui:canvas-theme");
1155
+ var toneVar = (tone) => `--color-${DIAGRAM_TONE_COLOR_KEYS[tone]}`;
1156
+ var SERIES_TONES = ["series-1", "series-2", "series-3", "series-4", "series-5", "series-6", "series-7", "series-8"];
1157
+ var TONE_SET = new Set(DIAGRAM_TONES);
1158
+ function isDiagramTone(value) {
1159
+ return TONE_SET.has(value);
1160
1160
  }
1161
- var SPRITE_FALLBACK_FILL = "#9b8f7f";
1162
- var SPRITE_FALLBACK_STROKE = "#5e564b";
1163
- function spriteRect(projector, node, natural) {
1164
- const tw = projector.tileWidth;
1165
- const fallbackW = projector.worldPixelDirect ? natural?.w ?? 0 : tw;
1166
- const fallbackH = projector.worldPixelDirect ? natural?.h ?? 0 : tw;
1167
- const w = node.width !== void 0 ? node.width * tw : fallbackW;
1168
- const h = node.height !== void 0 ? node.height * tw : fallbackH;
1169
- const anchor = node.anchor ?? "top-left";
1170
- const p = projector.anchorPoint(node.position, anchor);
1171
- return {
1172
- x: anchor === "top-left" ? p.x : p.x - w / 2,
1173
- y: anchor === "ground" ? p.y - h : anchor === "center" ? p.y - h / 2 : p.y,
1174
- w,
1175
- h
1176
- };
1161
+ function colorCss(c) {
1162
+ return c.a >= 1 ? c.css : `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
1177
1163
  }
1178
- var DrawableRegistryContext = createContext(null);
1179
- var NUMERIC_TRACKS = [
1180
- "offsetX",
1181
- "offsetY",
1182
- "rotate",
1183
- "opacity",
1184
- "radiusX",
1185
- "radiusY",
1186
- "width",
1187
- "height",
1188
- "strokeWidth",
1189
- "strokeDashOffset",
1190
- "blur"
1191
- ];
1192
- function isAnimatedShape(node) {
1193
- return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1164
+ function oneOf(value, allowed, fallback, name) {
1165
+ const hit = allowed.find((a) => a === value);
1166
+ if (hit !== void 0) return hit;
1167
+ if (value !== "") log2.warn("invalid-theme-keyword", { var: name, value, allowed });
1168
+ return fallback;
1194
1169
  }
1195
- var lerp = (a, b, k) => a + (b - a) * k;
1196
- function applyShapeAnimation(node, timeMs) {
1197
- const anim = node.animation;
1198
- if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
1199
- const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
1200
- const cycle = timeMs / anim.durationMs;
1201
- const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
1202
- const out = { ...node };
1203
- const trackValue = (key) => {
1204
- const defined = frames.filter((f) => f[key] !== void 0);
1205
- if (defined.length === 0) return void 0;
1206
- let prev;
1207
- let next;
1208
- for (const f of defined) {
1209
- if (f.at <= t) prev = f;
1210
- else if (!next) next = f;
1211
- }
1212
- if (!prev) return defined[0][key];
1213
- if (!next) return prev[key];
1214
- const span = next.at - prev.at;
1215
- const k = span > 0 ? (t - prev.at) / span : 1;
1216
- const a = prev[key];
1217
- const b = next[key];
1218
- if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
1219
- return a;
1220
- };
1221
- for (const key of NUMERIC_TRACKS) {
1222
- const v = trackValue(key);
1223
- if (v !== void 0) out[key] = v;
1170
+ var PLAIN_LENGTH = /^(-?[\d.]+)(px)?$/;
1171
+ var ScopeReader = class {
1172
+ constructor(scope) {
1173
+ __publicField(this, "scope", scope);
1174
+ __publicField(this, "style");
1175
+ __publicField(this, "probe");
1176
+ this.style = getComputedStyle(scope);
1177
+ const doc = scope.ownerDocument;
1178
+ this.probe = doc.createElement("span");
1179
+ this.probe.style.position = "absolute";
1180
+ this.probe.style.visibility = "hidden";
1181
+ this.probe.style.pointerEvents = "none";
1182
+ scope.appendChild(this.probe);
1224
1183
  }
1225
- const fill = trackValue("fill");
1226
- if (fill !== void 0) out.fill = fill;
1227
- const stroke = trackValue("stroke");
1228
- if (stroke !== void 0) out.stroke = stroke;
1229
- const shadowFrames = frames.filter((f) => f.shadow !== void 0);
1230
- if (shadowFrames.length > 0) {
1231
- const sh = trackValue("shadow");
1232
- if (sh !== void 0) {
1233
- let prevSh;
1234
- let nextSh;
1235
- for (const f of shadowFrames) {
1236
- if (f.at <= t) prevSh = f;
1237
- else if (!nextSh) nextSh = f;
1238
- }
1239
- if (prevSh?.shadow && nextSh?.shadow) {
1240
- const span = nextSh.at - prevSh.at;
1241
- const k = span > 0 ? (t - prevSh.at) / span : 1;
1242
- out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
1243
- } else {
1244
- out.shadow = sh;
1245
- }
1246
- }
1184
+ dispose() {
1185
+ this.probe.remove();
1247
1186
  }
1248
- return out;
1249
- }
1250
- function gradientStyle(g, ox, oy, scale) {
1251
- if (g.kind === "linear") {
1252
- if (!g.from || !g.to) return void 0;
1253
- return {
1254
- kind: "linear",
1255
- x1: ox + g.from.x * scale,
1256
- y1: oy + g.from.y * scale,
1257
- x2: ox + g.to.x * scale,
1258
- y2: oy + g.to.y * scale,
1259
- stops: g.stops
1260
- };
1187
+ raw(name) {
1188
+ return this.style.getPropertyValue(name).trim();
1261
1189
  }
1262
- if (g.kind === "conic") {
1263
- if (!g.center) return void 0;
1264
- return {
1265
- kind: "conic",
1266
- cx: ox + g.center.x * scale,
1267
- cy: oy + g.center.y * scale,
1268
- angle: g.angle ?? 0,
1269
- stops: g.stops
1190
+ /** A length var in px: a plain value parses directly, a derived one (`calc`, `var`) through the probe. */
1191
+ length(name, fallback) {
1192
+ const raw = this.raw(name);
1193
+ const plain = PLAIN_LENGTH.exec(raw);
1194
+ if (plain) return Number(plain[1]);
1195
+ if (raw === "") return fallback;
1196
+ this.probe.style.width = `var(${name})`;
1197
+ const computed = PLAIN_LENGTH.exec(getComputedStyle(this.probe).width.trim());
1198
+ this.probe.style.width = "";
1199
+ if (computed) return Number(computed[1]);
1200
+ log2.warn("unresolvable-theme-length", { var: name, raw });
1201
+ return fallback;
1202
+ }
1203
+ number(name, fallback) {
1204
+ const raw = this.raw(name);
1205
+ if (raw === "") return fallback;
1206
+ const n = Number(raw);
1207
+ if (Number.isFinite(n)) return n;
1208
+ log2.warn("unresolvable-theme-number", { var: name, raw });
1209
+ return fallback;
1210
+ }
1211
+ /** A duration var in ms (`250ms`, `0.25s`, or a `var()` alias resolved through the probe). */
1212
+ durationMs(name, fallback) {
1213
+ const parse = (v) => {
1214
+ const m = /^([\d.]+)(ms|s)$/.exec(v.trim());
1215
+ return m ? Number(m[1]) * (m[2] === "s" ? 1e3 : 1) : null;
1270
1216
  };
1217
+ const raw = this.raw(name);
1218
+ const direct = parse(raw);
1219
+ if (direct !== null) return direct;
1220
+ if (raw === "") return fallback;
1221
+ this.probe.style.transitionDuration = `var(${name})`;
1222
+ const computed = parse(getComputedStyle(this.probe).transitionDuration.split(",")[0] ?? "");
1223
+ this.probe.style.transitionDuration = "";
1224
+ return computed ?? fallback;
1271
1225
  }
1272
- if (!g.center || g.radius === void 0) return void 0;
1273
- return {
1274
- kind: "radial",
1275
- cx: ox + g.center.x * scale,
1276
- cy: oy + g.center.y * scale,
1277
- r: g.radius * scale,
1278
- stops: g.stops
1279
- };
1280
- }
1281
- function patternStyle(p, unit) {
1282
- if (p.kind === "image") {
1283
- if (!p.url) return void 0;
1284
- return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
1226
+ color(name) {
1227
+ const raw = this.raw(name);
1228
+ return parseComputedColor(raw) ?? resolveThemeColor(`var(${name})`, this.scope);
1285
1229
  }
1286
- return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
1287
- }
1288
- var paintShape = (painter, rawNode, dctx) => {
1289
- const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
1290
- if (!isValidScenePos(node.position)) return;
1291
- painter.save();
1292
- if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
1293
- const origin = dctx.projector.project(node.position);
1294
- const tileWidth = dctx.projector.tileWidth;
1295
- const groupScale = dctx.groupScale ?? 1;
1296
- const strokePx = (node.strokeWidth ?? 1) / groupScale;
1297
- if (node.blendMode) painter.setBlend(node.blendMode);
1298
- if (node.strokeDash && node.strokeDash.length > 0) {
1299
- painter.setLineDash(
1300
- node.strokeDash.map((v) => v / groupScale),
1301
- (node.strokeDashOffset ?? 0) / groupScale
1302
- );
1230
+ shadow(name) {
1231
+ const raw = this.raw(name);
1232
+ if (raw === "" || raw === "none") return null;
1233
+ this.probe.style.boxShadow = `var(${name})`;
1234
+ const computed = getComputedStyle(this.probe).boxShadow.trim() || raw;
1235
+ this.probe.style.boxShadow = "";
1236
+ const colorMatch = /(rgba?\([^)]*\)|#[0-9a-f]{3,8}|oklch\([^)]*\))/i.exec(computed);
1237
+ const lengths = computed.replace(colorMatch?.[0] ?? "", "").trim().split(/\s+/).map((t) => PLAIN_LENGTH.exec(t)).filter((m) => m !== null).map((m) => Number(m[1]));
1238
+ if (lengths.length < 2) return null;
1239
+ const color = colorMatch ? parseComputedColor(colorMatch[0]) : null;
1240
+ return { offsetX: lengths[0], offsetY: lengths[1], blur: lengths[2] ?? 0, color: color ? colorCss(color) : "rgba(0, 0, 0, 0.25)" };
1303
1241
  }
1304
- if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
1305
- if (node.rotate) {
1306
- const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
1307
- const px = origin.x + pivot.x * tileWidth;
1308
- const py = origin.y + pivot.y * tileWidth;
1309
- painter.translate(px, py);
1310
- painter.rotate(node.rotate);
1311
- painter.translate(-px, -py);
1242
+ family(name, fallback) {
1243
+ return this.raw(name) || fallback;
1312
1244
  }
1313
- if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
1314
- const fill = node.fill === "none" ? void 0 : node.fill;
1315
- const stroke = node.stroke === "none" ? void 0 : node.stroke;
1316
- const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
1317
- const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
1318
- const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
1319
- switch (node.shape) {
1320
- case "cell": {
1321
- const pts = dctx.projector.cellPath(node.position);
1322
- if (pxFill) painter.fillPoly(pts, pxFill);
1323
- if (patFill) painter.fillPoly(pts, patFill);
1324
- if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1325
- break;
1326
- }
1327
- case "rect": {
1328
- const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
1329
- const tw = dctx.projector.tileWidth;
1330
- const x = p.x + (node.offsetX ?? 0) * tw;
1331
- const y = p.y + (node.offsetY ?? 0) * tw;
1332
- const w = (node.width ?? 0) * tw;
1333
- const h = (node.height ?? 0) * tw;
1334
- if (pxFill) painter.fillRect(x, y, w, h, pxFill);
1335
- if (patFill) painter.fillRect(x, y, w, h, patFill);
1336
- if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
1337
- break;
1338
- }
1339
- case "ellipse": {
1340
- const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
1341
- const tw = dctx.projector.tileWidth;
1342
- const cx = p.x + (node.offsetX ?? 0) * tw;
1343
- const cy = p.y + (node.offsetY ?? 0) * tw;
1344
- const rx = (node.radiusX ?? 0) * tw;
1345
- const ry = (node.radiusY ?? node.radiusX ?? 0) * tw;
1346
- if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
1347
- if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
1348
- if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
1349
- break;
1350
- }
1351
- case "poly": {
1352
- const pts = (node.points ?? []).map((pt) => ({
1353
- x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
1354
- y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
1355
- }));
1356
- if (pxFill) painter.fillPoly(pts, pxFill);
1357
- if (patFill) painter.fillPoly(pts, patFill);
1358
- if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1359
- break;
1245
+ };
1246
+ function dashList(raw, fallback) {
1247
+ if (raw === "") return fallback;
1248
+ const parts = raw.split(/[\s,]+/).map(Number);
1249
+ return parts.every((n) => Number.isFinite(n) && n >= 0) ? parts : fallback;
1250
+ }
1251
+ function prefersReducedMotion(scope) {
1252
+ const view = scope.ownerDocument.defaultView;
1253
+ return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
1254
+ }
1255
+ function resolveCanvasTheme(scope) {
1256
+ const r = new ScopeReader(scope);
1257
+ try {
1258
+ const tones = {};
1259
+ for (const tone of DIAGRAM_TONES) {
1260
+ const c = r.color(toneVar(tone));
1261
+ tones[tone] = c ? colorCss(c) : "currentColor";
1360
1262
  }
1361
- case "path": {
1362
- if (!node.d) break;
1363
- painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
1364
- painter.scale(tileWidth, tileWidth);
1365
- if (node.strokeDash && node.strokeDash.length > 0) {
1366
- painter.setLineDash(
1367
- node.strokeDash.map((v) => v / (groupScale * tileWidth)),
1368
- (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
1369
- );
1263
+ const ground = r.color("--surface-diagram");
1264
+ const text = {};
1265
+ const TEXT_FALLBACK = { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24 };
1266
+ for (const size of DIAGRAM_TEXT_SIZES) text[size] = r.length(`--text-${size}`, TEXT_FALLBACK[size]);
1267
+ const body = r.family("--font-family-body", r.family("--font-family", "system-ui, sans-serif"));
1268
+ return {
1269
+ ground: ground ? colorCss(ground) : "transparent",
1270
+ tones,
1271
+ series: SERIES_TONES.map((t) => tones[t]),
1272
+ strokes: {
1273
+ thin: r.length("--diagram-stroke-thin", 1),
1274
+ normal: r.length("--diagram-stroke-normal", 1.5),
1275
+ bold: r.length("--diagram-stroke-bold", 3)
1276
+ },
1277
+ lineCap: oneOf(r.raw("--diagram-line-cap"), ["butt", "round", "square"], "round", "--diagram-line-cap"),
1278
+ lineJoin: oneOf(r.raw("--diagram-line-join"), ["miter", "round", "bevel"], "round", "--diagram-line-join"),
1279
+ dash: dashList(r.raw("--diagram-dash"), [6, 4]),
1280
+ dot: dashList(r.raw("--diagram-dot"), [1, 4]),
1281
+ fillStyle: oneOf(r.raw("--diagram-fill-style"), ["solid", "tint", "outline", "hatch"], "tint", "--diagram-fill-style"),
1282
+ fillOpacity: r.number("--diagram-fill-opacity", 0.18),
1283
+ roughness: r.number("--diagram-roughness", 0),
1284
+ glow: r.number("--diagram-glow", 0),
1285
+ shadow: r.shadow("--diagram-shadow"),
1286
+ marker: oneOf(r.raw("--diagram-marker"), ["triangle", "open", "line", "dot"], "triangle", "--diagram-marker"),
1287
+ faces: {
1288
+ body,
1289
+ display: r.family("--font-family-display", body),
1290
+ mono: r.family("--font-family-mono", "ui-monospace, monospace")
1291
+ },
1292
+ label: {
1293
+ font: oneOf(r.raw("--diagram-label-font"), ["body", "display", "mono"], "body", "--diagram-label-font"),
1294
+ size: oneOf(r.raw("--diagram-label-size"), DIAGRAM_TEXT_SIZES, "sm", "--diagram-label-size"),
1295
+ weight: r.raw("--diagram-label-weight") || "500",
1296
+ uppercase: oneOf(r.raw("--diagram-label-case"), ["none", "uppercase"], "none", "--diagram-label-case") === "uppercase"
1297
+ },
1298
+ text,
1299
+ corner: r.length("--diagram-corner", 4),
1300
+ motion: {
1301
+ enabled: r.raw("--motion-enable") !== "off" && !prefersReducedMotion(scope),
1302
+ fastMs: r.durationMs("--duration-fast", 150),
1303
+ normalMs: r.durationMs("--duration-normal", 250),
1304
+ slowMs: r.durationMs("--duration-slow", 400),
1305
+ easing: r.raw("--easing-standard") || "ease"
1306
+ },
1307
+ scene: {
1308
+ roughness: r.number("--material-roughness", 0.6),
1309
+ metalness: r.number("--material-metalness", 0),
1310
+ flat: r.number("--material-flat", 0) >= 1,
1311
+ outline: r.number("--material-outline", 0),
1312
+ ambient: r.number("--light-ambient", 0.55),
1313
+ key: r.number("--light-key", 1),
1314
+ keyColor: (() => {
1315
+ const c = r.color("--light-key-color");
1316
+ return c ? colorCss(c) : "#ffffff";
1317
+ })(),
1318
+ fog: r.number("--scene-fog", 0)
1370
1319
  }
1371
- if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
1372
- const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
1373
- const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
1374
- const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
1375
- if (localFill) painter.fillPath(node.d, localFill);
1376
- if (localPat) painter.fillPath(node.d, localPat);
1377
- if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
1378
- break;
1379
- }
1380
- }
1381
- painter.restore();
1382
- };
1383
- function DrawShape(props) {
1384
- const register = useContext(DrawableRegistryContext);
1385
- if (register) {
1386
- const { opacity, ...rest } = props;
1387
- const node = {
1388
- ...rest,
1389
- type: "draw-shape",
1390
- ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
1391
1320
  };
1392
- register(node);
1321
+ } finally {
1322
+ r.dispose();
1393
1323
  }
1394
- return null;
1395
1324
  }
1396
- function isAnimatedGroup(node) {
1397
- return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1325
+ function withSeries(theme, series, scope) {
1326
+ if (!theme || !series || series.length === 0 || !scope) return theme;
1327
+ const colors = series.map((c, i) => markColor(theme, c, scope, SERIES_TONES[i % SERIES_TONES.length]));
1328
+ const tones = { ...theme.tones };
1329
+ SERIES_TONES.forEach((tone, i) => {
1330
+ tones[tone] = colors[i % colors.length];
1331
+ });
1332
+ return { ...theme, tones, series: SERIES_TONES.map((t) => tones[t]) };
1398
1333
  }
1399
- function DrawGroup(props) {
1400
- const register = useContext(DrawableRegistryContext);
1401
- if (register) register({ ...props, type: "draw-group" });
1402
- return null;
1334
+ function markColor(theme, value, scope, fallback) {
1335
+ if (value === void 0 || value === "") return theme.tones[fallback];
1336
+ if (isDiagramTone(value)) return theme.tones[value];
1337
+ if (value.startsWith("var(") || value.startsWith("color-mix(")) {
1338
+ const resolved = resolveThemeColor(value, scope);
1339
+ return resolved ? colorCss(resolved) : theme.tones[fallback];
1340
+ }
1341
+ return value;
1403
1342
  }
1404
-
1405
- // lib/imageCache.ts
1406
- var RETRY_AFTER_MS = 5e3;
1407
- var cache = /* @__PURE__ */ new Map();
1408
- function startLoad(url, onReady, existing) {
1409
- const img = new Image();
1410
- img.crossOrigin = "anonymous";
1411
- const entry = existing ?? { img, status: "pending" };
1412
- entry.img = img;
1413
- if (onReady) entry.onReady = onReady;
1414
- img.onload = () => {
1415
- entry.status = "loaded";
1416
- updateAssetStatus(url, "loaded");
1417
- entry.onReady?.();
1418
- };
1419
- img.onerror = () => {
1420
- entry.status = "failed";
1421
- entry.failedAt = Date.now();
1422
- updateAssetStatus(url, "failed");
1423
- entry.onReady?.();
1424
- };
1425
- img.src = url;
1426
- return entry;
1343
+ function markStrokeWidth(theme, literal, role, fallback = "normal") {
1344
+ return literal ?? theme.strokes[role ?? fallback];
1427
1345
  }
1428
- function getOrLoadImage(url, onReady) {
1429
- if (!url || typeof window === "undefined") return null;
1430
- const existing = cache.get(url);
1431
- if (existing) {
1432
- if (existing.status === "loaded") return existing.img;
1433
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
1434
- if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
1435
- existing.failedAt = Date.now();
1436
- startLoad(url, onReady, existing);
1437
- }
1438
- return null;
1439
- }
1440
- const entry = startLoad(url, onReady);
1441
- cache.set(url, entry);
1442
- updateAssetStatus(url, "pending");
1443
- return null;
1346
+ function labelFont(theme, roles, literalPx, literalFamily) {
1347
+ const px = literalPx ?? theme.text[roles.textSize ?? theme.label.size];
1348
+ const family = literalFamily ?? theme.faces[roles.font ?? theme.label.font];
1349
+ return { font: `${theme.label.weight} ${px}px ${family}`, px };
1444
1350
  }
1445
- function getImageStatus(url) {
1446
- return cache.get(url)?.status;
1351
+ function labelText(theme, text, textCase = "theme") {
1352
+ return theme.label.uppercase && textCase === "theme" ? text.toUpperCase() : text;
1447
1353
  }
1448
-
1449
- // lib/skinning.ts
1450
- var IDENTITY = [1, 0, 0, 1, 0, 0];
1451
- function composeAffine(a, b) {
1452
- return [
1453
- a[0] * b[0] + a[2] * b[1],
1454
- a[1] * b[0] + a[3] * b[1],
1455
- a[0] * b[2] + a[2] * b[3],
1456
- a[1] * b[2] + a[3] * b[3],
1457
- a[0] * b[4] + a[2] * b[5] + a[4],
1458
- a[1] * b[4] + a[3] * b[5] + a[5]
1459
- ];
1354
+ function fillLabel(ctx, theme, text, x, y) {
1355
+ if (!text) return;
1356
+ ctx.save();
1357
+ ctx.strokeStyle = theme.ground;
1358
+ ctx.lineWidth = Math.max(2, theme.text.xs * 0.3);
1359
+ ctx.lineJoin = "round";
1360
+ ctx.strokeText(text, x, y);
1361
+ ctx.restore();
1362
+ ctx.fillText(text, x, y);
1460
1363
  }
1461
- function invertAffine(m) {
1462
- const det = m[0] * m[3] - m[1] * m[2];
1463
- if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return IDENTITY;
1464
- const inv = 1 / det;
1465
- return [
1466
- m[3] * inv,
1467
- -m[1] * inv,
1468
- -m[2] * inv,
1469
- m[0] * inv,
1470
- (m[2] * m[5] - m[3] * m[4]) * inv,
1471
- (m[1] * m[4] - m[0] * m[5]) * inv
1472
- ];
1364
+ function outlineStroke(stroke, fill, style, fillTone) {
1365
+ return outlineCarriesFill(fill, style, fillTone) && fill !== void 0 ? fill : stroke;
1473
1366
  }
1474
- function applyAffine(m, x, y) {
1475
- return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
1367
+ function outlineCarriesFill(fill, style, fillTone) {
1368
+ return style === "outline" && fill !== void 0 && fillTone !== "fill";
1476
1369
  }
1477
- function rotationAbout(pivot, degrees) {
1478
- const r = degrees * Math.PI / 180;
1479
- const cos = Math.cos(r);
1480
- const sin = Math.sin(r);
1481
- const [px, py] = pivot;
1482
- return [cos, sin, -sin, cos, px - cos * px + sin * py, py - sin * px - cos * py];
1370
+ function applyLineCharacter(ctx, theme, strokeColor) {
1371
+ ctx.lineCap = theme.lineCap;
1372
+ ctx.lineJoin = theme.lineJoin;
1373
+ if (theme.glow > 0) {
1374
+ ctx.shadowColor = strokeColor;
1375
+ ctx.shadowBlur = theme.glow;
1376
+ ctx.shadowOffsetX = 0;
1377
+ ctx.shadowOffsetY = 0;
1378
+ }
1483
1379
  }
1484
- function computeWorldMatrices(bones, pose) {
1485
- const indexByName = /* @__PURE__ */ new Map();
1486
- bones.forEach((b, i) => indexByName.set(b.name, i));
1487
- const world = new Array(bones.length);
1488
- const visiting = /* @__PURE__ */ new Set();
1489
- const resolve = (i) => {
1490
- const cached = world[i];
1491
- if (cached) return cached;
1492
- if (visiting.has(i)) return IDENTITY;
1493
- visiting.add(i);
1494
- const b = bones[i];
1495
- const parentIndex = b.parent === null ? void 0 : indexByName.get(b.parent);
1496
- const parentWorld = parentIndex === void 0 ? IDENTITY : resolve(parentIndex);
1497
- const local = rotationAbout(b.pivot, pose[b.name] ?? 0);
1498
- const m = composeAffine(parentWorld, local);
1499
- visiting.delete(i);
1500
- world[i] = m;
1501
- return m;
1502
- };
1503
- return bones.map((_, i) => resolve(i));
1380
+ function markDash(theme, dash) {
1381
+ if (dash === "dashed") return theme.dash;
1382
+ if (dash === "dotted") return theme.dot;
1383
+ return [];
1504
1384
  }
1505
- function skinVertices(mesh, bindWorld, curWorld) {
1506
- const skinMats = /* @__PURE__ */ new Map();
1507
- const skinMat = (bone) => {
1508
- let m = skinMats.get(bone);
1509
- if (!m) {
1510
- m = composeAffine(curWorld[bone] ?? IDENTITY, invertAffine(bindWorld[bone] ?? IDENTITY));
1511
- skinMats.set(bone, m);
1385
+ function wobble(x, y, k) {
1386
+ const s = Math.sin(x * 12.9898 + y * 78.233 + k * 37.719) * 43758.5453;
1387
+ return (s - Math.floor(s)) * 2 - 1;
1388
+ }
1389
+ function tracePolyline(ctx, theme, points, closed) {
1390
+ ctx.beginPath();
1391
+ if (points.length === 0) return;
1392
+ const amp = theme.roughness;
1393
+ const pts = closed ? [...points, points[0]] : [...points];
1394
+ ctx.moveTo(pts[0].x + amp * wobble(pts[0].x, pts[0].y, 0), pts[0].y + amp * wobble(pts[0].y, pts[0].x, 1));
1395
+ for (let i = 1; i < pts.length; i++) {
1396
+ const a = pts[i - 1];
1397
+ const b = pts[i];
1398
+ if (amp === 0) {
1399
+ ctx.lineTo(b.x, b.y);
1400
+ continue;
1512
1401
  }
1513
- return m;
1514
- };
1515
- return mesh.vertices.map((v) => {
1516
- if (v.weights.length === 0) return [v.x, v.y];
1517
- let x = 0;
1518
- let y = 0;
1519
- for (const { bone, w } of v.weights) {
1520
- const [bx, by] = applyAffine(skinMat(bone), v.x, v.y);
1521
- x += w * bx;
1522
- y += w * by;
1402
+ const steps = Math.max(1, Math.round(Math.hypot(b.x - a.x, b.y - a.y) / 24));
1403
+ for (let s = 1; s <= steps; s++) {
1404
+ const t = s / steps;
1405
+ const x = a.x + (b.x - a.x) * t;
1406
+ const y = a.y + (b.y - a.y) * t;
1407
+ ctx.lineTo(x + amp * wobble(x, y, 2), y + amp * wobble(y, x, 3));
1523
1408
  }
1524
- return [x, y];
1525
- });
1526
- }
1527
- function triangleAffine(src, dst) {
1528
- const [s0, s1, s2] = src;
1529
- const det = s0[0] * (s1[1] - s2[1]) + s1[0] * (s2[1] - s0[1]) + s2[0] * (s0[1] - s1[1]);
1530
- if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
1531
- const [d0, d1, d2] = dst;
1532
- const a = (d0[0] * (s1[1] - s2[1]) + d1[0] * (s2[1] - s0[1]) + d2[0] * (s0[1] - s1[1])) / det;
1533
- const c = (d0[0] * (s2[0] - s1[0]) + d1[0] * (s0[0] - s2[0]) + d2[0] * (s1[0] - s0[0])) / det;
1534
- const e = (d0[0] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[0] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[0] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1535
- const b = (d0[1] * (s1[1] - s2[1]) + d1[1] * (s2[1] - s0[1]) + d2[1] * (s0[1] - s1[1])) / det;
1536
- const d = (d0[1] * (s2[0] - s1[0]) + d1[1] * (s0[0] - s2[0]) + d2[1] * (s1[0] - s0[0])) / det;
1537
- const f = (d0[1] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[1] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[1] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1538
- return [a, b, c, d, e, f];
1539
- }
1540
- var meshCache = /* @__PURE__ */ new Map();
1541
- function isRigBundle(json) {
1542
- return json.mesh !== void 0 || json.bones !== void 0;
1543
- }
1544
- function getRigBundle(url, onReady) {
1545
- if (meshCache.has(url)) return meshCache.get(url) ?? void 0;
1546
- meshCache.set(url, void 0);
1547
- fetch(url).then((r) => {
1548
- if (!r.ok) throw new Error(`rigbundle fetch failed: HTTP ${r.status}`);
1549
- return r.json();
1550
- }).then((json) => {
1551
- const ok = isRigBundle(json) || Array.isArray(json.vertices);
1552
- if (!ok) throw new Error("rigbundle payload is neither a SkinMesh nor a rigbundle");
1553
- meshCache.set(url, json);
1554
- onReady();
1555
- }).catch(() => {
1556
- meshCache.set(url, null);
1557
- onReady();
1558
- });
1559
- return void 0;
1560
- }
1561
- function skinMeshFailed(url) {
1562
- return meshCache.get(url) === null;
1563
- }
1564
- function vertexBounds(points) {
1565
- if (points.length === 0) return null;
1566
- let minX = Infinity;
1567
- let minY = Infinity;
1568
- let maxX = -Infinity;
1569
- let maxY = -Infinity;
1570
- for (const [x, y] of points) {
1571
- if (x < minX) minX = x;
1572
- if (y < minY) minY = y;
1573
- if (x > maxX) maxX = x;
1574
- if (y > maxY) maxY = y;
1575
1409
  }
1576
- return { minX, minY, maxX, maxY };
1577
- }
1578
-
1579
- // components/game/atoms/DrawSkinnedMesh.tsx
1580
- var skinnedLog = createLogger("almadar:ui:draw-skinned-mesh");
1581
- var loggedMissing = /* @__PURE__ */ new Set();
1582
- function warnMissingOnce(reason, node) {
1583
- const key = `${reason}:${node.asset.url}:${String(node.meshUrl)}`;
1584
- if (loggedMissing.has(key)) return;
1585
- loggedMissing.add(key);
1586
- skinnedLog.warn("draw-skinned-mesh unresolvable \u2014 painting fallback", { reason, url: node.asset.url, meshUrl: node.meshUrl });
1410
+ if (closed) ctx.closePath();
1587
1411
  }
1588
- var WEIGHT_PALETTE = Array.from({ length: 10 }, (_, i) => `hsl(${i * 36 % 360}, 70%, 55%)`);
1589
- function dominantBone(v) {
1590
- let best = -1;
1591
- let bestW = 0;
1592
- for (const { bone, w } of v.weights) {
1593
- if (w > bestW) {
1594
- bestW = w;
1595
- best = bone;
1596
- }
1412
+ function arcPoints(cx, cy, rx, ry, startRad = 0, endRad = Math.PI * 2) {
1413
+ const steps = Math.max(12, Math.round(Math.abs(endRad - startRad) * Math.max(rx, ry) / 8));
1414
+ const out = [];
1415
+ for (let i = 0; i <= steps; i++) {
1416
+ const t = startRad + (endRad - startRad) * i / steps;
1417
+ out.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
1597
1418
  }
1598
- return best;
1419
+ return out;
1599
1420
  }
1600
- function resolveSkinPose(node, clip) {
1601
- const effectiveClip = clip ?? node.clip;
1602
- if (effectiveClip && node.frame !== void 0 && effectiveClip.frames.length > 0) {
1603
- return effectiveClip.frames[node.frame % effectiveClip.frames.length];
1421
+ function fillMark(ctx, theme, color, style = theme.fillStyle) {
1422
+ if (style === "outline") return;
1423
+ ctx.save();
1424
+ ctx.shadowBlur = 0;
1425
+ ctx.shadowColor = "transparent";
1426
+ if (theme.shadow) {
1427
+ ctx.shadowColor = theme.shadow.color;
1428
+ ctx.shadowOffsetX = theme.shadow.offsetX;
1429
+ ctx.shadowOffsetY = theme.shadow.offsetY;
1430
+ ctx.shadowBlur = theme.shadow.blur;
1604
1431
  }
1605
- return node.pose ?? {};
1606
- }
1607
- function resolveRig(node, dctx) {
1608
- const fetched = node.meshUrl ? getRigBundle(node.meshUrl, dctx.invalidate) : void 0;
1609
- if (node.meshUrl && fetched === void 0) return void 0;
1610
- const rb = fetched && isRigBundle(fetched) ? fetched : void 0;
1611
- const mesh = node.mesh ?? rb?.mesh ?? (!rb ? fetched : void 0);
1612
- if (!mesh) return void 0;
1613
- const bones = node.bones.length > 0 ? node.bones : rb?.bones ?? [];
1614
- if (bones.length === 0) return void 0;
1615
- const clip = node.clip ?? (node.clipName ? rb?.clips?.[node.clipName] : void 0);
1616
- return { mesh, bones, clip };
1432
+ if (style === "hatch") {
1433
+ ctx.clip();
1434
+ const span = ctx.canvas.width + ctx.canvas.height;
1435
+ ctx.strokeStyle = color;
1436
+ ctx.lineWidth = theme.strokes.thin;
1437
+ ctx.setLineDash([]);
1438
+ ctx.beginPath();
1439
+ for (let d = -span; d < span; d += 6) {
1440
+ ctx.moveTo(d, 0);
1441
+ ctx.lineTo(d + span, span);
1442
+ }
1443
+ ctx.stroke();
1444
+ } else {
1445
+ ctx.globalAlpha *= style === "tint" ? theme.fillOpacity : 1;
1446
+ ctx.fillStyle = color;
1447
+ ctx.fill();
1448
+ }
1449
+ ctx.restore();
1617
1450
  }
1618
- var pixelUVByMesh = /* @__PURE__ */ new WeakMap();
1619
- function meshHasPixelUVs(mesh) {
1620
- const cached = pixelUVByMesh.get(mesh);
1621
- if (cached !== void 0) return cached;
1622
- let pixel = false;
1623
- for (const v of mesh.vertices) {
1624
- if (v.u < 0 || v.u > 1 || v.v < 0 || v.v > 1) {
1625
- pixel = true;
1451
+ function drawMarker(ctx, theme, x1, y1, x2, y2, color, width) {
1452
+ const size = Math.max(8, width * 4);
1453
+ const angle = Math.atan2(y2 - y1, x2 - x1);
1454
+ const left = { x: x2 - size * Math.cos(angle - Math.PI / 6), y: y2 - size * Math.sin(angle - Math.PI / 6) };
1455
+ const right = { x: x2 - size * Math.cos(angle + Math.PI / 6), y: y2 - size * Math.sin(angle + Math.PI / 6) };
1456
+ ctx.save();
1457
+ ctx.strokeStyle = color;
1458
+ ctx.fillStyle = color;
1459
+ ctx.lineWidth = width;
1460
+ ctx.setLineDash([]);
1461
+ switch (theme.marker) {
1462
+ case "triangle": {
1463
+ ctx.beginPath();
1464
+ ctx.moveTo(x2, y2);
1465
+ ctx.lineTo(left.x, left.y);
1466
+ ctx.lineTo(right.x, right.y);
1467
+ ctx.closePath();
1468
+ ctx.fill();
1469
+ break;
1470
+ }
1471
+ case "open":
1472
+ case "line": {
1473
+ ctx.beginPath();
1474
+ ctx.moveTo(left.x, left.y);
1475
+ ctx.lineTo(x2, y2);
1476
+ ctx.lineTo(right.x, right.y);
1477
+ if (theme.marker === "open") ctx.stroke();
1478
+ else {
1479
+ ctx.moveTo(x2 - size * 0.5 * Math.cos(angle + Math.PI / 2), y2 - size * 0.5 * Math.sin(angle + Math.PI / 2));
1480
+ ctx.lineTo(x2 + size * 0.5 * Math.cos(angle + Math.PI / 2), y2 + size * 0.5 * Math.sin(angle + Math.PI / 2));
1481
+ ctx.stroke();
1482
+ }
1483
+ break;
1484
+ }
1485
+ case "dot": {
1486
+ ctx.beginPath();
1487
+ ctx.arc(x2, y2, Math.max(2.5, width * 1.5), 0, Math.PI * 2);
1488
+ ctx.fill();
1626
1489
  break;
1627
1490
  }
1628
1491
  }
1629
- pixelUVByMesh.set(mesh, pixel);
1630
- return pixel;
1492
+ ctx.restore();
1631
1493
  }
1632
- function paintMeshPass(painter, node, bones, mesh, tex, dctx, pose, opacity) {
1633
- const bindWorld = computeWorldMatrices(bones, {});
1634
- const curWorld = computeWorldMatrices(bones, pose);
1635
- const skinned = skinVertices(mesh, bindWorld, curWorld);
1636
- const origin = dctx.projector.project(node.position);
1637
- const tw = dctx.projector.tileWidth;
1638
- painter.save();
1639
- if (opacity !== 1) painter.setAlpha(opacity);
1640
- for (const [i0, i1, i2] of mesh.triangles) {
1641
- const v0 = mesh.vertices[i0];
1642
- const v1 = mesh.vertices[i1];
1643
- const v2 = mesh.vertices[i2];
1644
- const p0 = skinned[i0];
1645
- const p1 = skinned[i1];
1646
- const p2 = skinned[i2];
1647
- if (!v0 || !v1 || !v2 || !p0 || !p1 || !p2) continue;
1648
- const dst = [
1649
- [origin.x + p0[0] * tw, origin.y + p0[1] * tw],
1650
- [origin.x + p1[0] * tw, origin.y + p1[1] * tw],
1651
- [origin.x + p2[0] * tw, origin.y + p2[1] * tw]
1652
- ];
1653
- if (node.weightsOverlay || !tex) {
1654
- if (!node.weightsOverlay) continue;
1655
- const bone = dominantBone(v0);
1656
- painter.fillPoly(
1657
- dst.map(([x, y]) => ({ x, y })),
1658
- WEIGHT_PALETTE[(bone % WEIGHT_PALETTE.length + WEIGHT_PALETTE.length) % WEIGHT_PALETTE.length]
1659
- );
1660
- continue;
1661
- }
1662
- const pixelUV = meshHasPixelUVs(mesh);
1663
- const uv = pixelUV ? [[v0.u, v0.v], [v1.u, v1.v], [v2.u, v2.v]] : [
1664
- [v0.u * tex.width, v0.v * tex.height],
1665
- [v1.u * tex.width, v1.v * tex.height],
1666
- [v2.u * tex.width, v2.v * tex.height]
1667
- ];
1668
- const affine = triangleAffine(uv, dst);
1669
- if (!affine) continue;
1670
- painter.save();
1671
- painter.clipPath(`M ${dst[0][0]} ${dst[0][1]} L ${dst[1][0]} ${dst[1][1]} L ${dst[2][0]} ${dst[2][1]} Z`);
1672
- painter.blitTransformed(tex, { x: 0, y: 0, w: tex.width, h: tex.height }, affine);
1673
- painter.restore();
1674
- }
1675
- painter.restore();
1676
- }
1677
- var paintSkinnedMesh = (painter, node, dctx) => {
1678
- if (!isValidScenePos(node.position)) return;
1679
- const rig = resolveRig(node, dctx);
1680
- if (!rig) {
1681
- if (node.meshUrl && skinMeshFailed(node.meshUrl)) warnMissingOnce("mesh-failed", node);
1682
- else if (!node.mesh && !node.meshUrl) warnMissingOnce("mesh-missing", node);
1683
- return;
1684
- }
1685
- const tex = node.weightsOverlay ? null : painter.resolveTexture(node.asset.url);
1686
- if (!node.weightsOverlay && !tex) {
1687
- if (getImageStatus(node.asset.url) === "failed") warnMissingOnce("texture-failed", node);
1688
- return;
1689
- }
1690
- if (node.ghost && rig.clip && rig.clip.frames.length > 0) {
1691
- paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, rig.clip.frames[node.ghost.frame % rig.clip.frames.length], node.ghost.opacity);
1692
- }
1693
- paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, resolveSkinPose(node, rig.clip), node.opacity ?? 1);
1694
- };
1695
- function DrawSkinnedMesh(props) {
1696
- const register = useContext(DrawableRegistryContext);
1697
- if (register) register({ ...props, type: "draw-skinned-mesh" });
1698
- return null;
1494
+ function useCanvasTheme(scopeRef) {
1495
+ const { appliedTheme } = useTheme();
1496
+ const scopeKey = JSON.stringify(useThemeScope());
1497
+ const [state, setState] = useState({ theme: null, version: 0 });
1498
+ useLayoutEffect(() => {
1499
+ const scope = scopeRef.current;
1500
+ if (!scope) return;
1501
+ const theme = resolveCanvasTheme(scope);
1502
+ setState((prev) => ({ theme, version: prev.version + 1 }));
1503
+ }, [appliedTheme, scopeKey, scopeRef]);
1504
+ return state;
1699
1505
  }
1700
- var MESH_SEGMENTS_DEFAULT = 24;
1701
- function polyhedronBounds(vertices) {
1702
- if (!vertices || vertices.length < 3) return null;
1703
- const min = [Infinity, Infinity, Infinity];
1704
- const max = [-Infinity, -Infinity, -Infinity];
1705
- for (const v of vertices) {
1706
- if (v.length < 3 || !v.every((c) => Number.isFinite(c))) return null;
1707
- for (let axis = 0; axis < 3; axis++) {
1708
- min[axis] = Math.min(min[axis], v[axis]);
1709
- max[axis] = Math.max(max[axis], v[axis]);
1710
- }
1711
- }
1712
- return { min, max };
1506
+
1507
+ // lib/drawable/contract.ts
1508
+ function isValidScenePos(pos) {
1509
+ return Number.isFinite(pos?.x) && Number.isFinite(pos?.y);
1713
1510
  }
1714
- function clampSegments(segments) {
1715
- const s = segments ?? MESH_SEGMENTS_DEFAULT;
1716
- return Math.min(64, Math.max(3, Math.round(s)));
1511
+ function spriteAssetState(node) {
1512
+ if (!isValidScenePos(node.position)) return "invalid-position";
1513
+ return typeof node.asset?.url === "string" && node.asset.url.length > 0 ? "ready" : "asset-unset";
1717
1514
  }
1718
- function isAnimatedMesh(node) {
1719
- return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1515
+ var SPRITE_FALLBACK_FILL = "#9b8f7f";
1516
+ var SPRITE_FALLBACK_STROKE = "#5e564b";
1517
+ function spriteRect(projector, node, natural) {
1518
+ const tw = projector.tileWidth;
1519
+ const fallbackW = projector.worldPixelDirect ? natural?.w ?? 0 : tw;
1520
+ const fallbackH = projector.worldPixelDirect ? natural?.h ?? 0 : tw;
1521
+ const w = node.width !== void 0 ? node.width * tw : fallbackW;
1522
+ const h = node.height !== void 0 ? node.height * tw : fallbackH;
1523
+ const anchor = node.anchor ?? "top-left";
1524
+ const p = projector.anchorPoint(node.position, anchor);
1525
+ return {
1526
+ x: anchor === "top-left" ? p.x : p.x - w / 2,
1527
+ y: anchor === "ground" ? p.y - h : anchor === "center" ? p.y - h / 2 : p.y,
1528
+ w,
1529
+ h
1530
+ };
1720
1531
  }
1721
- var lerp2 = (a, b, k) => a + (b - a) * k;
1722
- var NUMERIC_TRACKS2 = [
1532
+ var DrawableRegistryContext = createContext(null);
1533
+ var NUMERIC_TRACKS = [
1723
1534
  "offsetX",
1724
1535
  "offsetY",
1725
- "offsetZ",
1726
- "rotateX",
1727
- "rotateY",
1728
- "rotateZ",
1729
- "scale",
1536
+ "rotate",
1730
1537
  "opacity",
1731
- "emissiveIntensity"
1538
+ "radiusX",
1539
+ "radiusY",
1540
+ "width",
1541
+ "height",
1542
+ "strokeWidth",
1543
+ "strokeDashOffset",
1544
+ "blur"
1732
1545
  ];
1733
- function applyMeshAnimation(node, timeMs) {
1546
+ function isAnimatedShape(node) {
1547
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1548
+ }
1549
+ var lerp = (a, b, k) => a + (b - a) * k;
1550
+ function applyShapeAnimation(node, timeMs) {
1734
1551
  const anim = node.animation;
1735
- if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
1552
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
1736
1553
  const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
1737
1554
  const cycle = timeMs / anim.durationMs;
1738
1555
  const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
1556
+ const out = { ...node };
1739
1557
  const trackValue = (key) => {
1740
1558
  const defined = frames.filter((f) => f[key] !== void 0);
1741
1559
  if (defined.length === 0) return void 0;
@@ -1751,652 +1569,836 @@ function applyMeshAnimation(node, timeMs) {
1751
1569
  const k = span > 0 ? (t - prev.at) / span : 1;
1752
1570
  const a = prev[key];
1753
1571
  const b = next[key];
1754
- if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
1572
+ if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
1755
1573
  return a;
1756
1574
  };
1757
- const num = {};
1758
- for (const key of NUMERIC_TRACKS2) {
1575
+ for (const key of NUMERIC_TRACKS) {
1759
1576
  const v = trackValue(key);
1760
- if (v !== void 0) num[key] = v;
1577
+ if (v !== void 0) out[key] = v;
1761
1578
  }
1762
- return {
1763
- offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
1764
- rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
1765
- scale: num.scale ?? 1,
1766
- opacity: num.opacity,
1767
- emissiveIntensity: num.emissiveIntensity,
1768
- color: trackValue("color"),
1769
- emissive: trackValue("emissive")
1770
- };
1771
- }
1772
- function DrawMesh(props) {
1773
- const register = useContext(DrawableRegistryContext);
1774
- if (register) register({ ...props, type: "draw-mesh" });
1775
- return null;
1579
+ const fill = trackValue("fill");
1580
+ if (fill !== void 0) out.fill = fill;
1581
+ const stroke = trackValue("stroke");
1582
+ if (stroke !== void 0) out.stroke = stroke;
1583
+ const shadowFrames = frames.filter((f) => f.shadow !== void 0);
1584
+ if (shadowFrames.length > 0) {
1585
+ const sh = trackValue("shadow");
1586
+ if (sh !== void 0) {
1587
+ let prevSh;
1588
+ let nextSh;
1589
+ for (const f of shadowFrames) {
1590
+ if (f.at <= t) prevSh = f;
1591
+ else if (!nextSh) nextSh = f;
1592
+ }
1593
+ if (prevSh?.shadow && nextSh?.shadow) {
1594
+ const span = nextSh.at - prevSh.at;
1595
+ const k = span > 0 ? (t - prevSh.at) / span : 1;
1596
+ out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
1597
+ } else {
1598
+ out.shadow = sh;
1599
+ }
1600
+ }
1601
+ }
1602
+ return out;
1776
1603
  }
1777
-
1778
- // components/game/molecules/DrawFxLayer.tsx
1779
- var DEFAULT_TICK_MS = 500;
1780
- var DEFAULT_TEXT_COLOR = "#ffe066";
1781
- var DEFAULT_SPARK_COLOR = "#ffffff";
1782
- var easeOut = (t) => 1 - (1 - t) * (1 - t);
1783
- function fxPosition(view, dim, ageSec) {
1784
- const g = view.gravity ?? 0;
1785
- const fall = 0.5 * g * ageSec * ageSec;
1786
- if (dim === "2d") {
1787
- const base2 = view.position ?? { x: view.x, y: view.z ?? view.y ?? 0 };
1788
- if (!Number.isFinite(base2.x) || !Number.isFinite(base2.y)) return void 0;
1604
+ function gradientStyle(g, ox, oy, scale) {
1605
+ if (g.kind === "linear") {
1606
+ if (!g.from || !g.to) return void 0;
1789
1607
  return {
1790
- x: base2.x + (view.vx ?? 0) * ageSec,
1791
- y: base2.y + (view.vz ?? 0) * ageSec + fall
1608
+ kind: "linear",
1609
+ x1: ox + g.from.x * scale,
1610
+ y1: oy + g.from.y * scale,
1611
+ x2: ox + g.to.x * scale,
1612
+ y2: oy + g.to.y * scale,
1613
+ stops: g.stops
1792
1614
  };
1793
1615
  }
1794
- const base = view.position ?? { x: view.x, y: view.z ?? 0, z: view.y };
1795
- if (!Number.isFinite(base.x) || !Number.isFinite(base.y)) return void 0;
1616
+ if (g.kind === "conic") {
1617
+ if (!g.center) return void 0;
1618
+ return {
1619
+ kind: "conic",
1620
+ cx: ox + g.center.x * scale,
1621
+ cy: oy + g.center.y * scale,
1622
+ angle: g.angle ?? 0,
1623
+ stops: g.stops
1624
+ };
1625
+ }
1626
+ if (!g.center || g.radius === void 0) return void 0;
1796
1627
  return {
1797
- x: base.x + (view.vx ?? 0) * ageSec,
1798
- y: base.y + (view.vz ?? 0) * ageSec,
1799
- ...base.z !== void 0 || view.vy !== void 0 || g !== 0 ? { z: (base.z ?? 0) + (view.vy ?? 0) * ageSec - fall } : {}
1628
+ kind: "radial",
1629
+ cx: ox + g.center.x * scale,
1630
+ cy: oy + g.center.y * scale,
1631
+ r: g.radius * scale,
1632
+ stops: g.stops
1800
1633
  };
1801
1634
  }
1802
- function sparkShape(view, pos, i, fade, progress) {
1803
- const size = view.size ?? 0.5;
1804
- const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
1805
- const dist = size * (0.4 + 0.6 * fxHash01(view.id, i * 3 + 1)) * easeOut(progress);
1806
- const r = size * (0.06 + 0.06 * fxHash01(view.id, i * 3 + 2));
1807
- const color = view.color ?? DEFAULT_SPARK_COLOR;
1808
- return {
1809
- type: "draw-shape",
1810
- shape: "ellipse",
1811
- position: { ...pos, x: pos.x + Math.cos(angle) * dist, y: pos.y + Math.sin(angle) * dist },
1812
- anchor: "center",
1813
- radiusX: r,
1814
- fill: color,
1815
- blendMode: "lighter",
1816
- opacity: fade,
1817
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
1818
- };
1635
+ function patternStyle(p, unit) {
1636
+ if (p.kind === "image") {
1637
+ if (!p.url) return void 0;
1638
+ return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
1639
+ }
1640
+ return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
1819
1641
  }
1820
- function proceduralShapes(view, pos, fade, progress) {
1821
- const size = view.size ?? 0.5;
1822
- const color = view.color ?? DEFAULT_SPARK_COLOR;
1823
- switch (view.shape ?? "spark") {
1824
- case "ring": {
1825
- return [
1826
- {
1827
- type: "draw-shape",
1828
- shape: "ellipse",
1829
- position: pos,
1830
- anchor: "center",
1831
- radiusX: size * easeOut(progress),
1832
- stroke: view.color2 ?? color,
1833
- strokeWidth: 2,
1834
- blendMode: "lighter",
1835
- opacity: fade,
1836
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
1837
- }
1838
- ];
1839
- }
1840
- case "puff": {
1841
- const count = view.count ?? 3;
1842
- return Array.from({ length: count }, (_, i) => {
1843
- const angle = fxHash01(view.id, i * 5) * Math.PI * 2;
1844
- const drift = size * 0.3 * fxHash01(view.id, i * 5 + 1) * easeOut(progress);
1845
- return {
1846
- type: "draw-shape",
1847
- shape: "ellipse",
1848
- position: { ...pos, x: pos.x + Math.cos(angle) * drift, y: pos.y + Math.sin(angle) * drift },
1849
- anchor: "center",
1850
- radiusX: size * (0.15 + 0.35 * progress) * (0.7 + 0.6 * fxHash01(view.id, i * 5 + 2)),
1851
- fill: i === 0 && view.color2 ? view.color2 : color,
1852
- opacity: fade * 0.6,
1853
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
1854
- };
1855
- });
1642
+ var paintShape = (painter, rawNode, dctx) => {
1643
+ const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
1644
+ if (!isValidScenePos(node.position)) return;
1645
+ painter.save();
1646
+ if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
1647
+ const origin = dctx.projector.project(node.position);
1648
+ const tileWidth = dctx.projector.tileWidth;
1649
+ const groupScale = dctx.groupScale ?? 1;
1650
+ const strokePx = (node.strokeWidth ?? 1) / groupScale;
1651
+ if (node.blendMode) painter.setBlend(node.blendMode);
1652
+ if (node.strokeDash && node.strokeDash.length > 0) {
1653
+ painter.setLineDash(
1654
+ node.strokeDash.map((v) => v / groupScale),
1655
+ (node.strokeDashOffset ?? 0) / groupScale
1656
+ );
1657
+ }
1658
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
1659
+ if (node.rotate) {
1660
+ const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
1661
+ const px = origin.x + pivot.x * tileWidth;
1662
+ const py = origin.y + pivot.y * tileWidth;
1663
+ painter.translate(px, py);
1664
+ painter.rotate(node.rotate);
1665
+ painter.translate(-px, -py);
1666
+ }
1667
+ if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
1668
+ const fill = node.fill === "none" ? void 0 : node.fill;
1669
+ const stroke = node.stroke === "none" ? void 0 : node.stroke;
1670
+ const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
1671
+ const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
1672
+ const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
1673
+ switch (node.shape) {
1674
+ case "cell": {
1675
+ const pts = dctx.projector.cellPath(node.position);
1676
+ if (pxFill) painter.fillPoly(pts, pxFill);
1677
+ if (patFill) painter.fillPoly(pts, patFill);
1678
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1679
+ break;
1856
1680
  }
1857
- case "streak": {
1858
- const count = view.count ?? 5;
1859
- return Array.from({ length: count }, (_, i) => {
1860
- const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
1861
- const inner = size * (0.2 + 0.8 * easeOut(progress)) * (0.6 + 0.4 * fxHash01(view.id, i * 3 + 1));
1862
- const len = size * 0.35;
1863
- return {
1864
- type: "draw-shape",
1865
- shape: "ellipse",
1866
- position: pos,
1867
- anchor: "center",
1868
- offsetX: inner + len / 2,
1869
- offsetY: 0,
1870
- radiusX: len / 2,
1871
- radiusY: size * 0.04,
1872
- rotate: angle,
1873
- fill: color,
1874
- blendMode: "lighter",
1875
- opacity: fade,
1876
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
1877
- };
1878
- });
1681
+ case "rect": {
1682
+ const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
1683
+ const tw = dctx.projector.tileWidth;
1684
+ const x = p.x + (node.offsetX ?? 0) * tw;
1685
+ const y = p.y + (node.offsetY ?? 0) * tw;
1686
+ const w = (node.width ?? 0) * tw;
1687
+ const h = (node.height ?? 0) * tw;
1688
+ if (pxFill) painter.fillRect(x, y, w, h, pxFill);
1689
+ if (patFill) painter.fillRect(x, y, w, h, patFill);
1690
+ if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
1691
+ break;
1879
1692
  }
1880
- default: {
1881
- const count = view.count ?? 6;
1882
- return Array.from({ length: count }, (_, i) => sparkShape(view, pos, i, fade, progress));
1693
+ case "ellipse": {
1694
+ const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
1695
+ const tw = dctx.projector.tileWidth;
1696
+ const cx = p.x + (node.offsetX ?? 0) * tw;
1697
+ const cy = p.y + (node.offsetY ?? 0) * tw;
1698
+ const rx = (node.radiusX ?? 0) * tw;
1699
+ const ry = (node.radiusY ?? node.radiusX ?? 0) * tw;
1700
+ if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
1701
+ if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
1702
+ if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
1703
+ break;
1704
+ }
1705
+ case "poly": {
1706
+ const pts = (node.points ?? []).map((pt) => ({
1707
+ x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
1708
+ y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
1709
+ }));
1710
+ if (pxFill) painter.fillPoly(pts, pxFill);
1711
+ if (patFill) painter.fillPoly(pts, patFill);
1712
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1713
+ break;
1714
+ }
1715
+ case "path": {
1716
+ if (!node.d) break;
1717
+ painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
1718
+ painter.scale(tileWidth, tileWidth);
1719
+ if (node.strokeDash && node.strokeDash.length > 0) {
1720
+ painter.setLineDash(
1721
+ node.strokeDash.map((v) => v / (groupScale * tileWidth)),
1722
+ (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
1723
+ );
1724
+ }
1725
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
1726
+ const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
1727
+ const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
1728
+ const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
1729
+ if (localFill) painter.fillPath(node.d, localFill);
1730
+ if (localPat) painter.fillPath(node.d, localPat);
1731
+ if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
1732
+ break;
1883
1733
  }
1884
1734
  }
1885
- }
1886
- function expandFxItem(view, node, epochNowMs, dim, projector, fontFamily) {
1887
- if (view.space === "screen") return [];
1888
- const tickMs = node.tickMs ?? DEFAULT_TICK_MS;
1889
- const { ageMs, progress, fade } = fxLifecycle(view, epochNowMs, tickMs);
1890
- if (progress >= 1) return [];
1891
- const pos = fxPosition(view, dim, ageMs / 1e3);
1892
- if (!pos) return [];
1893
- const out = [];
1894
- const artItems = node.art?.[view.type]?.["idle"];
1895
- const sprite = node.sprites?.[view.type];
1896
- if (Array.isArray(artItems)) {
1897
- out.push({
1898
- type: "draw-group",
1899
- position: pos,
1900
- opacity: fade,
1901
- ...view.size !== void 0 ? { scale: view.size } : {},
1902
- items: artItems
1903
- });
1904
- } else if (sprite?.url) {
1905
- const spriteSize = view.size ?? 0.6;
1906
- out.push({
1907
- type: "draw-sprite",
1908
- position: pos,
1909
- asset: sprite,
1910
- anchor: "center",
1911
- width: spriteSize,
1912
- height: spriteSize,
1913
- opacity: fade,
1914
- animation: view.animation ?? sprite.animations?.[0],
1915
- // One-shot fx rows (burst) play from the item's birth, not wall-time.
1916
- clockMs: ageMs
1917
- });
1918
- } else {
1919
- out.push(...proceduralShapes(view, pos, fade, progress));
1920
- }
1921
- if (view.message) {
1922
- const pxSize = projector && view.size !== void 0 ? Math.max(10, Math.round(view.size * projector.tileWidth)) : void 0;
1923
- const family = fontFamily ?? "system-ui, sans-serif";
1924
- const text = {
1925
- type: "draw-text",
1926
- text: view.message,
1927
- position: pos,
1928
- offsetY: -(0.15 + 0.55 * progress),
1929
- color: view.color ?? node.textColor ?? DEFAULT_TEXT_COLOR,
1930
- opacity: fade,
1931
- font: `bold ${pxSize ?? 14}px ${family}`
1735
+ painter.restore();
1736
+ };
1737
+ function DrawShape(props) {
1738
+ const register = useContext(DrawableRegistryContext);
1739
+ if (register) {
1740
+ const { opacity, ...rest } = props;
1741
+ const node = {
1742
+ ...rest,
1743
+ type: "draw-shape",
1744
+ ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
1932
1745
  };
1933
- out.push(text);
1746
+ register(node);
1934
1747
  }
1935
- return out;
1748
+ return null;
1936
1749
  }
1937
- function expandFxLayer(node, epochNowMs, dim, projector, fontFamily) {
1938
- if (!Array.isArray(node.items)) return [];
1939
- const out = [];
1940
- for (const item of node.items) {
1941
- if (!item || typeof item.id !== "string") continue;
1942
- out.push(...expandFxItem(resolveFxView(item, node.presets), node, epochNowMs, dim, projector, fontFamily));
1943
- }
1944
- return out;
1750
+ function isAnimatedGroup(node) {
1751
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1945
1752
  }
1946
- function DrawFxLayer(_props) {
1753
+ function DrawGroup(props) {
1754
+ const register = useContext(DrawableRegistryContext);
1755
+ if (register) register({ ...props, type: "draw-group" });
1947
1756
  return null;
1948
1757
  }
1949
1758
 
1950
- // lib/drawable/themeDrawables.ts
1951
- function opt(value, resolve) {
1952
- return value === void 0 ? void 0 : resolve(value);
1953
- }
1954
- function themeShape(node, resolve) {
1955
- return {
1956
- ...node,
1957
- ...node.fill !== void 0 ? { fill: resolve(node.fill) } : {},
1958
- ...node.stroke !== void 0 ? { stroke: resolve(node.stroke) } : {},
1959
- ...node.gradient ? { gradient: { ...node.gradient, stops: node.gradient.stops.map((s) => ({ ...s, color: resolve(s.color) })) } } : {},
1960
- ...node.fillPattern?.color !== void 0 ? { fillPattern: { ...node.fillPattern, color: resolve(node.fillPattern.color) } } : {},
1961
- ...node.shadow ? { shadow: { ...node.shadow, color: resolve(node.shadow.color) } } : {},
1962
- ...node.animation ? {
1963
- animation: {
1964
- ...node.animation,
1965
- keyframes: node.animation.keyframes.map((k) => ({
1966
- ...k,
1967
- ...k.fill !== void 0 ? { fill: resolve(k.fill) } : {},
1968
- ...k.stroke !== void 0 ? { stroke: resolve(k.stroke) } : {}
1969
- }))
1970
- }
1971
- } : {}
1759
+ // lib/imageCache.ts
1760
+ var RETRY_AFTER_MS = 5e3;
1761
+ var cache = /* @__PURE__ */ new Map();
1762
+ function startLoad(url, onReady, existing) {
1763
+ const img = new Image();
1764
+ img.crossOrigin = "anonymous";
1765
+ const entry = existing ?? { img, status: "pending" };
1766
+ entry.img = img;
1767
+ if (onReady) entry.onReady = onReady;
1768
+ img.onload = () => {
1769
+ entry.status = "loaded";
1770
+ updateAssetStatus(url, "loaded");
1771
+ entry.onReady?.();
1972
1772
  };
1973
- }
1974
- function themeText(node, resolve) {
1975
- return { ...node, color: resolve(node.color) };
1976
- }
1977
- function themeMaterial(material, resolve) {
1978
- if (!material) return material;
1979
- return {
1980
- ...material,
1981
- ...material.color !== void 0 ? { color: resolve(material.color) } : {},
1982
- ...material.emissive !== void 0 ? { emissive: resolve(material.emissive) } : {}
1773
+ img.onerror = () => {
1774
+ entry.status = "failed";
1775
+ entry.failedAt = Date.now();
1776
+ updateAssetStatus(url, "failed");
1777
+ entry.onReady?.();
1983
1778
  };
1779
+ img.src = url;
1780
+ return entry;
1984
1781
  }
1985
- function resolveDrawableColors(nodes, resolve) {
1986
- return nodes.map((node) => {
1987
- switch (node.type) {
1988
- case "draw-shape":
1989
- return themeShape(node, resolve);
1990
- case "draw-text":
1991
- return themeText(node, resolve);
1992
- case "draw-group":
1993
- return { ...node, items: resolveDrawableColors(node.items, resolve) };
1994
- case "draw-shape-layer":
1995
- return { ...node, items: node.items.map((item) => themeShape(item, resolve)) };
1996
- case "draw-text-layer":
1997
- return { ...node, items: node.items.map((item) => themeText(item, resolve)) };
1998
- case "draw-fx-layer":
1999
- return {
2000
- ...node,
2001
- ...node.textColor !== void 0 ? { textColor: resolve(node.textColor) } : {},
2002
- ...node.presets ? {
2003
- presets: node.presets.map((p) => ({
2004
- ...p,
2005
- ...p.color !== void 0 ? { color: resolve(p.color) } : {},
2006
- ...p.color2 !== void 0 ? { color2: resolve(p.color2) } : {}
2007
- }))
2008
- } : {}
2009
- };
2010
- case "draw-mesh":
2011
- return {
2012
- ...node,
2013
- material: themeMaterial(node.material, resolve),
2014
- ...node.outline ? { outline: { ...node.outline, ...node.outline.color !== void 0 ? { color: resolve(node.outline.color) } : {} } } : {},
2015
- ...node.animation ? {
2016
- animation: {
2017
- ...node.animation,
2018
- keyframes: node.animation.keyframes.map((k) => ({
2019
- ...k,
2020
- ...k.color !== void 0 ? { color: resolve(k.color) } : {},
2021
- ...k.emissive !== void 0 ? { emissive: opt(k.emissive, resolve) } : {}
2022
- }))
2023
- }
2024
- } : {}
2025
- };
2026
- default:
2027
- return node;
1782
+ function getOrLoadImage(url, onReady) {
1783
+ if (!url || typeof window === "undefined") return null;
1784
+ const existing = cache.get(url);
1785
+ if (existing) {
1786
+ if (existing.status === "loaded") return existing.img;
1787
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
1788
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
1789
+ existing.failedAt = Date.now();
1790
+ startLoad(url, onReady, existing);
2028
1791
  }
2029
- });
1792
+ return null;
1793
+ }
1794
+ const entry = startLoad(url, onReady);
1795
+ cache.set(url, entry);
1796
+ updateAssetStatus(url, "pending");
1797
+ return null;
2030
1798
  }
2031
- function applySceneMaterials(nodes, theme) {
2032
- return nodes.map((node) => {
2033
- if (node.type === "draw-group") return { ...node, items: applySceneMaterials(node.items, theme) };
2034
- if (node.type !== "draw-mesh") return node;
2035
- const m = node.material ?? {};
2036
- const pbr = (m.kind ?? "standard") === "standard" || m.kind === "physical";
2037
- return {
2038
- ...node,
2039
- material: {
2040
- ...m,
2041
- ...pbr && m.roughness === void 0 ? { roughness: theme.scene.roughness } : {},
2042
- ...pbr && m.metalness === void 0 ? { metalness: theme.scene.metalness } : {},
2043
- ...m.flatShading === void 0 && theme.scene.flat ? { flatShading: true } : {}
2044
- },
2045
- ...node.outline === void 0 && theme.scene.outline > 0 ? { outline: { color: theme.tones.ink, width: theme.scene.outline * 0.02 } } : {}
2046
- };
1799
+ function getImageStatus(url) {
1800
+ return cache.get(url)?.status;
1801
+ }
1802
+
1803
+ // lib/skinning.ts
1804
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
1805
+ function composeAffine(a, b) {
1806
+ return [
1807
+ a[0] * b[0] + a[2] * b[1],
1808
+ a[1] * b[0] + a[3] * b[1],
1809
+ a[0] * b[2] + a[2] * b[3],
1810
+ a[1] * b[2] + a[3] * b[3],
1811
+ a[0] * b[4] + a[2] * b[5] + a[4],
1812
+ a[1] * b[4] + a[3] * b[5] + a[5]
1813
+ ];
1814
+ }
1815
+ function invertAffine(m) {
1816
+ const det = m[0] * m[3] - m[1] * m[2];
1817
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return IDENTITY;
1818
+ const inv = 1 / det;
1819
+ return [
1820
+ m[3] * inv,
1821
+ -m[1] * inv,
1822
+ -m[2] * inv,
1823
+ m[0] * inv,
1824
+ (m[2] * m[5] - m[3] * m[4]) * inv,
1825
+ (m[1] * m[4] - m[0] * m[5]) * inv
1826
+ ];
1827
+ }
1828
+ function applyAffine(m, x, y) {
1829
+ return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
1830
+ }
1831
+ function rotationAbout(pivot, degrees) {
1832
+ const r = degrees * Math.PI / 180;
1833
+ const cos = Math.cos(r);
1834
+ const sin = Math.sin(r);
1835
+ const [px, py] = pivot;
1836
+ return [cos, sin, -sin, cos, px - cos * px + sin * py, py - sin * px - cos * py];
1837
+ }
1838
+ function computeWorldMatrices(bones, pose) {
1839
+ const indexByName = /* @__PURE__ */ new Map();
1840
+ bones.forEach((b, i) => indexByName.set(b.name, i));
1841
+ const world = new Array(bones.length);
1842
+ const visiting = /* @__PURE__ */ new Set();
1843
+ const resolve = (i) => {
1844
+ const cached = world[i];
1845
+ if (cached) return cached;
1846
+ if (visiting.has(i)) return IDENTITY;
1847
+ visiting.add(i);
1848
+ const b = bones[i];
1849
+ const parentIndex = b.parent === null ? void 0 : indexByName.get(b.parent);
1850
+ const parentWorld = parentIndex === void 0 ? IDENTITY : resolve(parentIndex);
1851
+ const local = rotationAbout(b.pivot, pose[b.name] ?? 0);
1852
+ const m = composeAffine(parentWorld, local);
1853
+ visiting.delete(i);
1854
+ world[i] = m;
1855
+ return m;
1856
+ };
1857
+ return bones.map((_, i) => resolve(i));
1858
+ }
1859
+ function skinVertices(mesh, bindWorld, curWorld) {
1860
+ const skinMats = /* @__PURE__ */ new Map();
1861
+ const skinMat = (bone) => {
1862
+ let m = skinMats.get(bone);
1863
+ if (!m) {
1864
+ m = composeAffine(curWorld[bone] ?? IDENTITY, invertAffine(bindWorld[bone] ?? IDENTITY));
1865
+ skinMats.set(bone, m);
1866
+ }
1867
+ return m;
1868
+ };
1869
+ return mesh.vertices.map((v) => {
1870
+ if (v.weights.length === 0) return [v.x, v.y];
1871
+ let x = 0;
1872
+ let y = 0;
1873
+ for (const { bone, w } of v.weights) {
1874
+ const [bx, by] = applyAffine(skinMat(bone), v.x, v.y);
1875
+ x += w * bx;
1876
+ y += w * by;
1877
+ }
1878
+ return [x, y];
2047
1879
  });
2048
1880
  }
2049
- var log2 = createLogger("almadar:ui:canvas-theme");
2050
- var toneVar = (tone) => `--color-${DIAGRAM_TONE_COLOR_KEYS[tone]}`;
2051
- var SERIES_TONES = ["series-1", "series-2", "series-3", "series-4", "series-5", "series-6", "series-7", "series-8"];
2052
- var TONE_SET = new Set(DIAGRAM_TONES);
2053
- function isDiagramTone(value) {
2054
- return TONE_SET.has(value);
1881
+ function triangleAffine(src, dst) {
1882
+ const [s0, s1, s2] = src;
1883
+ const det = s0[0] * (s1[1] - s2[1]) + s1[0] * (s2[1] - s0[1]) + s2[0] * (s0[1] - s1[1]);
1884
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
1885
+ const [d0, d1, d2] = dst;
1886
+ const a = (d0[0] * (s1[1] - s2[1]) + d1[0] * (s2[1] - s0[1]) + d2[0] * (s0[1] - s1[1])) / det;
1887
+ const c = (d0[0] * (s2[0] - s1[0]) + d1[0] * (s0[0] - s2[0]) + d2[0] * (s1[0] - s0[0])) / det;
1888
+ const e = (d0[0] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[0] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[0] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1889
+ const b = (d0[1] * (s1[1] - s2[1]) + d1[1] * (s2[1] - s0[1]) + d2[1] * (s0[1] - s1[1])) / det;
1890
+ const d = (d0[1] * (s2[0] - s1[0]) + d1[1] * (s0[0] - s2[0]) + d2[1] * (s1[0] - s0[0])) / det;
1891
+ const f = (d0[1] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[1] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[1] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1892
+ return [a, b, c, d, e, f];
2055
1893
  }
2056
- function colorCss(c) {
2057
- return c.a >= 1 ? c.css : `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
1894
+ var meshCache = /* @__PURE__ */ new Map();
1895
+ function isRigBundle(json) {
1896
+ return json.mesh !== void 0 || json.bones !== void 0;
2058
1897
  }
2059
- function oneOf(value, allowed, fallback, name) {
2060
- const hit = allowed.find((a) => a === value);
2061
- if (hit !== void 0) return hit;
2062
- if (value !== "") log2.warn("invalid-theme-keyword", { var: name, value, allowed });
2063
- return fallback;
1898
+ function getRigBundle(url, onReady) {
1899
+ if (meshCache.has(url)) return meshCache.get(url) ?? void 0;
1900
+ meshCache.set(url, void 0);
1901
+ fetch(url).then((r) => {
1902
+ if (!r.ok) throw new Error(`rigbundle fetch failed: HTTP ${r.status}`);
1903
+ return r.json();
1904
+ }).then((json) => {
1905
+ const ok = isRigBundle(json) || Array.isArray(json.vertices);
1906
+ if (!ok) throw new Error("rigbundle payload is neither a SkinMesh nor a rigbundle");
1907
+ meshCache.set(url, json);
1908
+ onReady();
1909
+ }).catch(() => {
1910
+ meshCache.set(url, null);
1911
+ onReady();
1912
+ });
1913
+ return void 0;
2064
1914
  }
2065
- var PLAIN_LENGTH = /^(-?[\d.]+)(px)?$/;
2066
- var ScopeReader = class {
2067
- constructor(scope) {
2068
- __publicField(this, "scope", scope);
2069
- __publicField(this, "style");
2070
- __publicField(this, "probe");
2071
- this.style = getComputedStyle(scope);
2072
- const doc = scope.ownerDocument;
2073
- this.probe = doc.createElement("span");
2074
- this.probe.style.position = "absolute";
2075
- this.probe.style.visibility = "hidden";
2076
- this.probe.style.pointerEvents = "none";
2077
- scope.appendChild(this.probe);
2078
- }
2079
- dispose() {
2080
- this.probe.remove();
2081
- }
2082
- raw(name) {
2083
- return this.style.getPropertyValue(name).trim();
2084
- }
2085
- /** A length var in px: a plain value parses directly, a derived one (`calc`, `var`) through the probe. */
2086
- length(name, fallback) {
2087
- const raw = this.raw(name);
2088
- const plain = PLAIN_LENGTH.exec(raw);
2089
- if (plain) return Number(plain[1]);
2090
- if (raw === "") return fallback;
2091
- this.probe.style.width = `var(${name})`;
2092
- const computed = PLAIN_LENGTH.exec(getComputedStyle(this.probe).width.trim());
2093
- this.probe.style.width = "";
2094
- if (computed) return Number(computed[1]);
2095
- log2.warn("unresolvable-theme-length", { var: name, raw });
2096
- return fallback;
2097
- }
2098
- number(name, fallback) {
2099
- const raw = this.raw(name);
2100
- if (raw === "") return fallback;
2101
- const n = Number(raw);
2102
- if (Number.isFinite(n)) return n;
2103
- log2.warn("unresolvable-theme-number", { var: name, raw });
2104
- return fallback;
2105
- }
2106
- /** A duration var in ms (`250ms`, `0.25s`, or a `var()` alias resolved through the probe). */
2107
- durationMs(name, fallback) {
2108
- const parse = (v) => {
2109
- const m = /^([\d.]+)(ms|s)$/.exec(v.trim());
2110
- return m ? Number(m[1]) * (m[2] === "s" ? 1e3 : 1) : null;
2111
- };
2112
- const raw = this.raw(name);
2113
- const direct = parse(raw);
2114
- if (direct !== null) return direct;
2115
- if (raw === "") return fallback;
2116
- this.probe.style.transitionDuration = `var(${name})`;
2117
- const computed = parse(getComputedStyle(this.probe).transitionDuration.split(",")[0] ?? "");
2118
- this.probe.style.transitionDuration = "";
2119
- return computed ?? fallback;
2120
- }
2121
- color(name) {
2122
- const raw = this.raw(name);
2123
- return parseComputedColor(raw) ?? resolveThemeColor(`var(${name})`, this.scope);
1915
+ function skinMeshFailed(url) {
1916
+ return meshCache.get(url) === null;
1917
+ }
1918
+ function vertexBounds(points) {
1919
+ if (points.length === 0) return null;
1920
+ let minX = Infinity;
1921
+ let minY = Infinity;
1922
+ let maxX = -Infinity;
1923
+ let maxY = -Infinity;
1924
+ for (const [x, y] of points) {
1925
+ if (x < minX) minX = x;
1926
+ if (y < minY) minY = y;
1927
+ if (x > maxX) maxX = x;
1928
+ if (y > maxY) maxY = y;
2124
1929
  }
2125
- shadow(name) {
2126
- const raw = this.raw(name);
2127
- if (raw === "" || raw === "none") return null;
2128
- this.probe.style.boxShadow = `var(${name})`;
2129
- const computed = getComputedStyle(this.probe).boxShadow.trim() || raw;
2130
- this.probe.style.boxShadow = "";
2131
- const colorMatch = /(rgba?\([^)]*\)|#[0-9a-f]{3,8}|oklch\([^)]*\))/i.exec(computed);
2132
- const lengths = computed.replace(colorMatch?.[0] ?? "", "").trim().split(/\s+/).map((t) => PLAIN_LENGTH.exec(t)).filter((m) => m !== null).map((m) => Number(m[1]));
2133
- if (lengths.length < 2) return null;
2134
- const color = colorMatch ? parseComputedColor(colorMatch[0]) : null;
2135
- return { offsetX: lengths[0], offsetY: lengths[1], blur: lengths[2] ?? 0, color: color ? colorCss(color) : "rgba(0, 0, 0, 0.25)" };
1930
+ return { minX, minY, maxX, maxY };
1931
+ }
1932
+
1933
+ // components/game/atoms/DrawSkinnedMesh.tsx
1934
+ var skinnedLog = createLogger("almadar:ui:draw-skinned-mesh");
1935
+ var loggedMissing = /* @__PURE__ */ new Set();
1936
+ function warnMissingOnce(reason, node) {
1937
+ const key = `${reason}:${node.asset.url}:${String(node.meshUrl)}`;
1938
+ if (loggedMissing.has(key)) return;
1939
+ loggedMissing.add(key);
1940
+ skinnedLog.warn("draw-skinned-mesh unresolvable \u2014 painting fallback", { reason, url: node.asset.url, meshUrl: node.meshUrl });
1941
+ }
1942
+ var WEIGHT_PALETTE = Array.from({ length: 10 }, (_, i) => `hsl(${i * 36 % 360}, 70%, 55%)`);
1943
+ function dominantBone(v) {
1944
+ let best = -1;
1945
+ let bestW = 0;
1946
+ for (const { bone, w } of v.weights) {
1947
+ if (w > bestW) {
1948
+ bestW = w;
1949
+ best = bone;
1950
+ }
2136
1951
  }
2137
- family(name, fallback) {
2138
- return this.raw(name) || fallback;
1952
+ return best;
1953
+ }
1954
+ function resolveSkinPose(node, clip) {
1955
+ const effectiveClip = clip ?? node.clip;
1956
+ if (effectiveClip && node.frame !== void 0 && effectiveClip.frames.length > 0) {
1957
+ return effectiveClip.frames[node.frame % effectiveClip.frames.length];
2139
1958
  }
2140
- };
2141
- function dashList(raw, fallback) {
2142
- if (raw === "") return fallback;
2143
- const parts = raw.split(/[\s,]+/).map(Number);
2144
- return parts.every((n) => Number.isFinite(n) && n >= 0) ? parts : fallback;
1959
+ return node.pose ?? {};
2145
1960
  }
2146
- function prefersReducedMotion(scope) {
2147
- const view = scope.ownerDocument.defaultView;
2148
- return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
1961
+ function resolveRig(node, dctx) {
1962
+ const fetched = node.meshUrl ? getRigBundle(node.meshUrl, dctx.invalidate) : void 0;
1963
+ if (node.meshUrl && fetched === void 0) return void 0;
1964
+ const rb = fetched && isRigBundle(fetched) ? fetched : void 0;
1965
+ const mesh = node.mesh ?? rb?.mesh ?? (!rb ? fetched : void 0);
1966
+ if (!mesh) return void 0;
1967
+ const bones = node.bones.length > 0 ? node.bones : rb?.bones ?? [];
1968
+ if (bones.length === 0) return void 0;
1969
+ const clip = node.clip ?? (node.clipName ? rb?.clips?.[node.clipName] : void 0);
1970
+ return { mesh, bones, clip };
2149
1971
  }
2150
- function resolveCanvasTheme(scope) {
2151
- const r = new ScopeReader(scope);
2152
- try {
2153
- const tones = {};
2154
- for (const tone of DIAGRAM_TONES) {
2155
- const c = r.color(toneVar(tone));
2156
- tones[tone] = c ? colorCss(c) : "currentColor";
1972
+ var pixelUVByMesh = /* @__PURE__ */ new WeakMap();
1973
+ function meshHasPixelUVs(mesh) {
1974
+ const cached = pixelUVByMesh.get(mesh);
1975
+ if (cached !== void 0) return cached;
1976
+ let pixel = false;
1977
+ for (const v of mesh.vertices) {
1978
+ if (v.u < 0 || v.u > 1 || v.v < 0 || v.v > 1) {
1979
+ pixel = true;
1980
+ break;
2157
1981
  }
2158
- const ground = r.color("--surface-diagram");
2159
- const text = {};
2160
- const TEXT_FALLBACK = { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24 };
2161
- for (const size of DIAGRAM_TEXT_SIZES) text[size] = r.length(`--text-${size}`, TEXT_FALLBACK[size]);
2162
- const body = r.family("--font-family-body", r.family("--font-family", "system-ui, sans-serif"));
2163
- return {
2164
- ground: ground ? colorCss(ground) : "transparent",
2165
- tones,
2166
- series: SERIES_TONES.map((t) => tones[t]),
2167
- strokes: {
2168
- thin: r.length("--diagram-stroke-thin", 1),
2169
- normal: r.length("--diagram-stroke-normal", 1.5),
2170
- bold: r.length("--diagram-stroke-bold", 3)
2171
- },
2172
- lineCap: oneOf(r.raw("--diagram-line-cap"), ["butt", "round", "square"], "round", "--diagram-line-cap"),
2173
- lineJoin: oneOf(r.raw("--diagram-line-join"), ["miter", "round", "bevel"], "round", "--diagram-line-join"),
2174
- dash: dashList(r.raw("--diagram-dash"), [6, 4]),
2175
- dot: dashList(r.raw("--diagram-dot"), [1, 4]),
2176
- fillStyle: oneOf(r.raw("--diagram-fill-style"), ["solid", "tint", "outline", "hatch"], "tint", "--diagram-fill-style"),
2177
- fillOpacity: r.number("--diagram-fill-opacity", 0.18),
2178
- roughness: r.number("--diagram-roughness", 0),
2179
- glow: r.number("--diagram-glow", 0),
2180
- shadow: r.shadow("--diagram-shadow"),
2181
- marker: oneOf(r.raw("--diagram-marker"), ["triangle", "open", "line", "dot"], "triangle", "--diagram-marker"),
2182
- faces: {
2183
- body,
2184
- display: r.family("--font-family-display", body),
2185
- mono: r.family("--font-family-mono", "ui-monospace, monospace")
2186
- },
2187
- label: {
2188
- font: oneOf(r.raw("--diagram-label-font"), ["body", "display", "mono"], "body", "--diagram-label-font"),
2189
- size: oneOf(r.raw("--diagram-label-size"), DIAGRAM_TEXT_SIZES, "sm", "--diagram-label-size"),
2190
- weight: r.raw("--diagram-label-weight") || "500",
2191
- uppercase: oneOf(r.raw("--diagram-label-case"), ["none", "uppercase"], "none", "--diagram-label-case") === "uppercase"
2192
- },
2193
- text,
2194
- corner: r.length("--diagram-corner", 4),
2195
- motion: {
2196
- enabled: r.raw("--motion-enable") !== "off" && !prefersReducedMotion(scope),
2197
- fastMs: r.durationMs("--duration-fast", 150),
2198
- normalMs: r.durationMs("--duration-normal", 250),
2199
- slowMs: r.durationMs("--duration-slow", 400),
2200
- easing: r.raw("--easing-standard") || "ease"
2201
- },
2202
- scene: {
2203
- roughness: r.number("--material-roughness", 0.6),
2204
- metalness: r.number("--material-metalness", 0),
2205
- flat: r.number("--material-flat", 0) >= 1,
2206
- outline: r.number("--material-outline", 0),
2207
- ambient: r.number("--light-ambient", 0.55),
2208
- key: r.number("--light-key", 1),
2209
- keyColor: (() => {
2210
- const c = r.color("--light-key-color");
2211
- return c ? colorCss(c) : "#ffffff";
2212
- })(),
2213
- fog: r.number("--scene-fog", 0)
2214
- }
2215
- };
2216
- } finally {
2217
- r.dispose();
2218
1982
  }
1983
+ pixelUVByMesh.set(mesh, pixel);
1984
+ return pixel;
1985
+ }
1986
+ function paintMeshPass(painter, node, bones, mesh, tex, dctx, pose, opacity) {
1987
+ const bindWorld = computeWorldMatrices(bones, {});
1988
+ const curWorld = computeWorldMatrices(bones, pose);
1989
+ const skinned = skinVertices(mesh, bindWorld, curWorld);
1990
+ const origin = dctx.projector.project(node.position);
1991
+ const tw = dctx.projector.tileWidth;
1992
+ painter.save();
1993
+ if (opacity !== 1) painter.setAlpha(opacity);
1994
+ for (const [i0, i1, i2] of mesh.triangles) {
1995
+ const v0 = mesh.vertices[i0];
1996
+ const v1 = mesh.vertices[i1];
1997
+ const v2 = mesh.vertices[i2];
1998
+ const p0 = skinned[i0];
1999
+ const p1 = skinned[i1];
2000
+ const p2 = skinned[i2];
2001
+ if (!v0 || !v1 || !v2 || !p0 || !p1 || !p2) continue;
2002
+ const dst = [
2003
+ [origin.x + p0[0] * tw, origin.y + p0[1] * tw],
2004
+ [origin.x + p1[0] * tw, origin.y + p1[1] * tw],
2005
+ [origin.x + p2[0] * tw, origin.y + p2[1] * tw]
2006
+ ];
2007
+ if (node.weightsOverlay || !tex) {
2008
+ if (!node.weightsOverlay) continue;
2009
+ const bone = dominantBone(v0);
2010
+ painter.fillPoly(
2011
+ dst.map(([x, y]) => ({ x, y })),
2012
+ WEIGHT_PALETTE[(bone % WEIGHT_PALETTE.length + WEIGHT_PALETTE.length) % WEIGHT_PALETTE.length]
2013
+ );
2014
+ continue;
2015
+ }
2016
+ const pixelUV = meshHasPixelUVs(mesh);
2017
+ const uv = pixelUV ? [[v0.u, v0.v], [v1.u, v1.v], [v2.u, v2.v]] : [
2018
+ [v0.u * tex.width, v0.v * tex.height],
2019
+ [v1.u * tex.width, v1.v * tex.height],
2020
+ [v2.u * tex.width, v2.v * tex.height]
2021
+ ];
2022
+ const affine = triangleAffine(uv, dst);
2023
+ if (!affine) continue;
2024
+ painter.save();
2025
+ painter.clipPath(`M ${dst[0][0]} ${dst[0][1]} L ${dst[1][0]} ${dst[1][1]} L ${dst[2][0]} ${dst[2][1]} Z`);
2026
+ painter.blitTransformed(tex, { x: 0, y: 0, w: tex.width, h: tex.height }, affine);
2027
+ painter.restore();
2028
+ }
2029
+ painter.restore();
2219
2030
  }
2220
- function withSeries(theme, series, scope) {
2221
- if (!theme || !series || series.length === 0 || !scope) return theme;
2222
- const colors = series.map((c, i) => markColor(theme, c, scope, SERIES_TONES[i % SERIES_TONES.length]));
2223
- const tones = { ...theme.tones };
2224
- SERIES_TONES.forEach((tone, i) => {
2225
- tones[tone] = colors[i % colors.length];
2226
- });
2227
- return { ...theme, tones, series: SERIES_TONES.map((t) => tones[t]) };
2228
- }
2229
- function markColor(theme, value, scope, fallback) {
2230
- if (value === void 0 || value === "") return theme.tones[fallback];
2231
- if (isDiagramTone(value)) return theme.tones[value];
2232
- if (value.startsWith("var(") || value.startsWith("color-mix(")) {
2233
- const resolved = resolveThemeColor(value, scope);
2234
- return resolved ? colorCss(resolved) : theme.tones[fallback];
2031
+ var paintSkinnedMesh = (painter, node, dctx) => {
2032
+ if (!isValidScenePos(node.position)) return;
2033
+ const rig = resolveRig(node, dctx);
2034
+ if (!rig) {
2035
+ if (node.meshUrl && skinMeshFailed(node.meshUrl)) warnMissingOnce("mesh-failed", node);
2036
+ else if (!node.mesh && !node.meshUrl) warnMissingOnce("mesh-missing", node);
2037
+ return;
2235
2038
  }
2236
- return value;
2237
- }
2238
- function markStrokeWidth(theme, literal, role, fallback = "normal") {
2239
- return literal ?? theme.strokes[role ?? fallback];
2039
+ const tex = node.weightsOverlay ? null : painter.resolveTexture(node.asset.url);
2040
+ if (!node.weightsOverlay && !tex) {
2041
+ if (getImageStatus(node.asset.url) === "failed") warnMissingOnce("texture-failed", node);
2042
+ return;
2043
+ }
2044
+ if (node.ghost && rig.clip && rig.clip.frames.length > 0) {
2045
+ paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, rig.clip.frames[node.ghost.frame % rig.clip.frames.length], node.ghost.opacity);
2046
+ }
2047
+ paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, resolveSkinPose(node, rig.clip), node.opacity ?? 1);
2048
+ };
2049
+ function DrawSkinnedMesh(props) {
2050
+ const register = useContext(DrawableRegistryContext);
2051
+ if (register) register({ ...props, type: "draw-skinned-mesh" });
2052
+ return null;
2240
2053
  }
2241
- function labelFont(theme, roles, literalPx, literalFamily) {
2242
- const px = literalPx ?? theme.text[roles.textSize ?? theme.label.size];
2243
- const family = literalFamily ?? theme.faces[roles.font ?? theme.label.font];
2244
- return { font: `${theme.label.weight} ${px}px ${family}`, px };
2054
+ var MESH_SEGMENTS_DEFAULT = 24;
2055
+ function polyhedronBounds(vertices) {
2056
+ if (!vertices || vertices.length < 3) return null;
2057
+ const min = [Infinity, Infinity, Infinity];
2058
+ const max = [-Infinity, -Infinity, -Infinity];
2059
+ for (const v of vertices) {
2060
+ if (v.length < 3 || !v.every((c) => Number.isFinite(c))) return null;
2061
+ for (let axis = 0; axis < 3; axis++) {
2062
+ min[axis] = Math.min(min[axis], v[axis]);
2063
+ max[axis] = Math.max(max[axis], v[axis]);
2064
+ }
2065
+ }
2066
+ return { min, max };
2245
2067
  }
2246
- function labelText(theme, text, textCase = "theme") {
2247
- return theme.label.uppercase && textCase === "theme" ? text.toUpperCase() : text;
2068
+ function clampSegments(segments) {
2069
+ const s = segments ?? MESH_SEGMENTS_DEFAULT;
2070
+ return Math.min(64, Math.max(3, Math.round(s)));
2248
2071
  }
2249
- function fillLabel(ctx, theme, text, x, y) {
2250
- if (!text) return;
2251
- ctx.save();
2252
- ctx.strokeStyle = theme.ground;
2253
- ctx.lineWidth = Math.max(2, theme.text.xs * 0.3);
2254
- ctx.lineJoin = "round";
2255
- ctx.strokeText(text, x, y);
2256
- ctx.restore();
2257
- ctx.fillText(text, x, y);
2072
+ function isAnimatedMesh(node) {
2073
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
2258
2074
  }
2259
- function outlineStroke(stroke, fill, style, fillTone) {
2260
- return outlineCarriesFill(fill, style, fillTone) && fill !== void 0 ? fill : stroke;
2075
+ var lerp2 = (a, b, k) => a + (b - a) * k;
2076
+ var NUMERIC_TRACKS2 = [
2077
+ "offsetX",
2078
+ "offsetY",
2079
+ "offsetZ",
2080
+ "rotateX",
2081
+ "rotateY",
2082
+ "rotateZ",
2083
+ "scale",
2084
+ "opacity",
2085
+ "emissiveIntensity"
2086
+ ];
2087
+ function applyMeshAnimation(node, timeMs) {
2088
+ const anim = node.animation;
2089
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
2090
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
2091
+ const cycle = timeMs / anim.durationMs;
2092
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
2093
+ const trackValue = (key) => {
2094
+ const defined = frames.filter((f) => f[key] !== void 0);
2095
+ if (defined.length === 0) return void 0;
2096
+ let prev;
2097
+ let next;
2098
+ for (const f of defined) {
2099
+ if (f.at <= t) prev = f;
2100
+ else if (!next) next = f;
2101
+ }
2102
+ if (!prev) return defined[0][key];
2103
+ if (!next) return prev[key];
2104
+ const span = next.at - prev.at;
2105
+ const k = span > 0 ? (t - prev.at) / span : 1;
2106
+ const a = prev[key];
2107
+ const b = next[key];
2108
+ if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
2109
+ return a;
2110
+ };
2111
+ const num = {};
2112
+ for (const key of NUMERIC_TRACKS2) {
2113
+ const v = trackValue(key);
2114
+ if (v !== void 0) num[key] = v;
2115
+ }
2116
+ return {
2117
+ offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
2118
+ rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
2119
+ scale: num.scale ?? 1,
2120
+ opacity: num.opacity,
2121
+ emissiveIntensity: num.emissiveIntensity,
2122
+ color: trackValue("color"),
2123
+ emissive: trackValue("emissive")
2124
+ };
2261
2125
  }
2262
- function outlineCarriesFill(fill, style, fillTone) {
2263
- return style === "outline" && fill !== void 0 && fillTone !== "fill";
2126
+ function DrawMesh(props) {
2127
+ const register = useContext(DrawableRegistryContext);
2128
+ if (register) register({ ...props, type: "draw-mesh" });
2129
+ return null;
2264
2130
  }
2265
- function applyLineCharacter(ctx, theme, strokeColor) {
2266
- ctx.lineCap = theme.lineCap;
2267
- ctx.lineJoin = theme.lineJoin;
2268
- if (theme.glow > 0) {
2269
- ctx.shadowColor = strokeColor;
2270
- ctx.shadowBlur = theme.glow;
2271
- ctx.shadowOffsetX = 0;
2272
- ctx.shadowOffsetY = 0;
2131
+
2132
+ // components/game/molecules/DrawFxLayer.tsx
2133
+ var DEFAULT_TICK_MS = 500;
2134
+ var DEFAULT_TEXT_COLOR = "#ffe066";
2135
+ var DEFAULT_SPARK_COLOR = "#ffffff";
2136
+ var easeOut = (t) => 1 - (1 - t) * (1 - t);
2137
+ function fxPosition(view, dim, ageSec) {
2138
+ const g = view.gravity ?? 0;
2139
+ const fall = 0.5 * g * ageSec * ageSec;
2140
+ if (dim === "2d") {
2141
+ const base2 = view.position ?? { x: view.x, y: view.z ?? view.y ?? 0 };
2142
+ if (!Number.isFinite(base2.x) || !Number.isFinite(base2.y)) return void 0;
2143
+ return {
2144
+ x: base2.x + (view.vx ?? 0) * ageSec,
2145
+ y: base2.y + (view.vz ?? 0) * ageSec + fall
2146
+ };
2273
2147
  }
2148
+ const base = view.position ?? { x: view.x, y: view.z ?? 0, z: view.y };
2149
+ if (!Number.isFinite(base.x) || !Number.isFinite(base.y)) return void 0;
2150
+ return {
2151
+ x: base.x + (view.vx ?? 0) * ageSec,
2152
+ y: base.y + (view.vz ?? 0) * ageSec,
2153
+ ...base.z !== void 0 || view.vy !== void 0 || g !== 0 ? { z: (base.z ?? 0) + (view.vy ?? 0) * ageSec - fall } : {}
2154
+ };
2274
2155
  }
2275
- function markDash(theme, dash) {
2276
- if (dash === "dashed") return theme.dash;
2277
- if (dash === "dotted") return theme.dot;
2278
- return [];
2279
- }
2280
- function wobble(x, y, k) {
2281
- const s = Math.sin(x * 12.9898 + y * 78.233 + k * 37.719) * 43758.5453;
2282
- return (s - Math.floor(s)) * 2 - 1;
2156
+ function sparkShape(view, pos, i, fade, progress) {
2157
+ const size = view.size ?? 0.5;
2158
+ const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
2159
+ const dist = size * (0.4 + 0.6 * fxHash01(view.id, i * 3 + 1)) * easeOut(progress);
2160
+ const r = size * (0.06 + 0.06 * fxHash01(view.id, i * 3 + 2));
2161
+ const color = view.color ?? DEFAULT_SPARK_COLOR;
2162
+ return {
2163
+ type: "draw-shape",
2164
+ shape: "ellipse",
2165
+ position: { ...pos, x: pos.x + Math.cos(angle) * dist, y: pos.y + Math.sin(angle) * dist },
2166
+ anchor: "center",
2167
+ radiusX: r,
2168
+ fill: color,
2169
+ blendMode: "lighter",
2170
+ opacity: fade,
2171
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2172
+ };
2283
2173
  }
2284
- function tracePolyline(ctx, theme, points, closed) {
2285
- ctx.beginPath();
2286
- if (points.length === 0) return;
2287
- const amp = theme.roughness;
2288
- const pts = closed ? [...points, points[0]] : [...points];
2289
- ctx.moveTo(pts[0].x + amp * wobble(pts[0].x, pts[0].y, 0), pts[0].y + amp * wobble(pts[0].y, pts[0].x, 1));
2290
- for (let i = 1; i < pts.length; i++) {
2291
- const a = pts[i - 1];
2292
- const b = pts[i];
2293
- if (amp === 0) {
2294
- ctx.lineTo(b.x, b.y);
2295
- continue;
2174
+ function proceduralShapes(view, pos, fade, progress) {
2175
+ const size = view.size ?? 0.5;
2176
+ const color = view.color ?? DEFAULT_SPARK_COLOR;
2177
+ switch (view.shape ?? "spark") {
2178
+ case "ring": {
2179
+ return [
2180
+ {
2181
+ type: "draw-shape",
2182
+ shape: "ellipse",
2183
+ position: pos,
2184
+ anchor: "center",
2185
+ radiusX: size * easeOut(progress),
2186
+ stroke: view.color2 ?? color,
2187
+ strokeWidth: 2,
2188
+ blendMode: "lighter",
2189
+ opacity: fade,
2190
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2191
+ }
2192
+ ];
2193
+ }
2194
+ case "puff": {
2195
+ const count = view.count ?? 3;
2196
+ return Array.from({ length: count }, (_, i) => {
2197
+ const angle = fxHash01(view.id, i * 5) * Math.PI * 2;
2198
+ const drift = size * 0.3 * fxHash01(view.id, i * 5 + 1) * easeOut(progress);
2199
+ return {
2200
+ type: "draw-shape",
2201
+ shape: "ellipse",
2202
+ position: { ...pos, x: pos.x + Math.cos(angle) * drift, y: pos.y + Math.sin(angle) * drift },
2203
+ anchor: "center",
2204
+ radiusX: size * (0.15 + 0.35 * progress) * (0.7 + 0.6 * fxHash01(view.id, i * 5 + 2)),
2205
+ fill: i === 0 && view.color2 ? view.color2 : color,
2206
+ opacity: fade * 0.6,
2207
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2208
+ };
2209
+ });
2210
+ }
2211
+ case "streak": {
2212
+ const count = view.count ?? 5;
2213
+ return Array.from({ length: count }, (_, i) => {
2214
+ const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
2215
+ const inner = size * (0.2 + 0.8 * easeOut(progress)) * (0.6 + 0.4 * fxHash01(view.id, i * 3 + 1));
2216
+ const len = size * 0.35;
2217
+ return {
2218
+ type: "draw-shape",
2219
+ shape: "ellipse",
2220
+ position: pos,
2221
+ anchor: "center",
2222
+ offsetX: inner + len / 2,
2223
+ offsetY: 0,
2224
+ radiusX: len / 2,
2225
+ radiusY: size * 0.04,
2226
+ rotate: angle,
2227
+ fill: color,
2228
+ blendMode: "lighter",
2229
+ opacity: fade,
2230
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2231
+ };
2232
+ });
2296
2233
  }
2297
- const steps = Math.max(1, Math.round(Math.hypot(b.x - a.x, b.y - a.y) / 24));
2298
- for (let s = 1; s <= steps; s++) {
2299
- const t = s / steps;
2300
- const x = a.x + (b.x - a.x) * t;
2301
- const y = a.y + (b.y - a.y) * t;
2302
- ctx.lineTo(x + amp * wobble(x, y, 2), y + amp * wobble(y, x, 3));
2234
+ default: {
2235
+ const count = view.count ?? 6;
2236
+ return Array.from({ length: count }, (_, i) => sparkShape(view, pos, i, fade, progress));
2303
2237
  }
2304
2238
  }
2305
- if (closed) ctx.closePath();
2306
2239
  }
2307
- function arcPoints(cx, cy, rx, ry, startRad = 0, endRad = Math.PI * 2) {
2308
- const steps = Math.max(12, Math.round(Math.abs(endRad - startRad) * Math.max(rx, ry) / 8));
2240
+ function expandFxItem(view, node, epochNowMs, dim, projector, fontFamily) {
2241
+ if (view.space === "screen") return [];
2242
+ const tickMs = node.tickMs ?? DEFAULT_TICK_MS;
2243
+ const { ageMs, progress, fade } = fxLifecycle(view, epochNowMs, tickMs);
2244
+ if (progress >= 1) return [];
2245
+ const pos = fxPosition(view, dim, ageMs / 1e3);
2246
+ if (!pos) return [];
2309
2247
  const out = [];
2310
- for (let i = 0; i <= steps; i++) {
2311
- const t = startRad + (endRad - startRad) * i / steps;
2312
- out.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
2248
+ const artItems = node.art?.[view.type]?.["idle"];
2249
+ const sprite = node.sprites?.[view.type];
2250
+ if (Array.isArray(artItems)) {
2251
+ out.push({
2252
+ type: "draw-group",
2253
+ position: pos,
2254
+ opacity: fade,
2255
+ ...view.size !== void 0 ? { scale: view.size } : {},
2256
+ items: artItems
2257
+ });
2258
+ } else if (sprite?.url) {
2259
+ const spriteSize = view.size ?? 0.6;
2260
+ out.push({
2261
+ type: "draw-sprite",
2262
+ position: pos,
2263
+ asset: sprite,
2264
+ anchor: "center",
2265
+ width: spriteSize,
2266
+ height: spriteSize,
2267
+ opacity: fade,
2268
+ animation: view.animation ?? sprite.animations?.[0],
2269
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
2270
+ clockMs: ageMs
2271
+ });
2272
+ } else {
2273
+ out.push(...proceduralShapes(view, pos, fade, progress));
2274
+ }
2275
+ if (view.message) {
2276
+ const pxSize = projector && view.size !== void 0 ? Math.max(10, Math.round(view.size * projector.tileWidth)) : void 0;
2277
+ const family = fontFamily ?? "system-ui, sans-serif";
2278
+ const text = {
2279
+ type: "draw-text",
2280
+ text: view.message,
2281
+ position: pos,
2282
+ offsetY: -(0.15 + 0.55 * progress),
2283
+ color: view.color ?? node.textColor ?? DEFAULT_TEXT_COLOR,
2284
+ opacity: fade,
2285
+ font: `bold ${pxSize ?? 14}px ${family}`
2286
+ };
2287
+ out.push(text);
2313
2288
  }
2314
2289
  return out;
2315
2290
  }
2316
- function fillMark(ctx, theme, color, style = theme.fillStyle) {
2317
- if (style === "outline") return;
2318
- ctx.save();
2319
- ctx.shadowBlur = 0;
2320
- ctx.shadowColor = "transparent";
2321
- if (theme.shadow) {
2322
- ctx.shadowColor = theme.shadow.color;
2323
- ctx.shadowOffsetX = theme.shadow.offsetX;
2324
- ctx.shadowOffsetY = theme.shadow.offsetY;
2325
- ctx.shadowBlur = theme.shadow.blur;
2326
- }
2327
- if (style === "hatch") {
2328
- ctx.clip();
2329
- const span = ctx.canvas.width + ctx.canvas.height;
2330
- ctx.strokeStyle = color;
2331
- ctx.lineWidth = theme.strokes.thin;
2332
- ctx.setLineDash([]);
2333
- ctx.beginPath();
2334
- for (let d = -span; d < span; d += 6) {
2335
- ctx.moveTo(d, 0);
2336
- ctx.lineTo(d + span, span);
2337
- }
2338
- ctx.stroke();
2339
- } else {
2340
- ctx.globalAlpha *= style === "tint" ? theme.fillOpacity : 1;
2341
- ctx.fillStyle = color;
2342
- ctx.fill();
2291
+ function expandFxLayer(node, epochNowMs, dim, projector, fontFamily) {
2292
+ if (!Array.isArray(node.items)) return [];
2293
+ const out = [];
2294
+ for (const item of node.items) {
2295
+ if (!item || typeof item.id !== "string") continue;
2296
+ out.push(...expandFxItem(resolveFxView(item, node.presets), node, epochNowMs, dim, projector, fontFamily));
2343
2297
  }
2344
- ctx.restore();
2298
+ return out;
2345
2299
  }
2346
- function drawMarker(ctx, theme, x1, y1, x2, y2, color, width) {
2347
- const size = Math.max(8, width * 4);
2348
- const angle = Math.atan2(y2 - y1, x2 - x1);
2349
- const left = { x: x2 - size * Math.cos(angle - Math.PI / 6), y: y2 - size * Math.sin(angle - Math.PI / 6) };
2350
- const right = { x: x2 - size * Math.cos(angle + Math.PI / 6), y: y2 - size * Math.sin(angle + Math.PI / 6) };
2351
- ctx.save();
2352
- ctx.strokeStyle = color;
2353
- ctx.fillStyle = color;
2354
- ctx.lineWidth = width;
2355
- ctx.setLineDash([]);
2356
- switch (theme.marker) {
2357
- case "triangle": {
2358
- ctx.beginPath();
2359
- ctx.moveTo(x2, y2);
2360
- ctx.lineTo(left.x, left.y);
2361
- ctx.lineTo(right.x, right.y);
2362
- ctx.closePath();
2363
- ctx.fill();
2364
- break;
2365
- }
2366
- case "open":
2367
- case "line": {
2368
- ctx.beginPath();
2369
- ctx.moveTo(left.x, left.y);
2370
- ctx.lineTo(x2, y2);
2371
- ctx.lineTo(right.x, right.y);
2372
- if (theme.marker === "open") ctx.stroke();
2373
- else {
2374
- ctx.moveTo(x2 - size * 0.5 * Math.cos(angle + Math.PI / 2), y2 - size * 0.5 * Math.sin(angle + Math.PI / 2));
2375
- ctx.lineTo(x2 + size * 0.5 * Math.cos(angle + Math.PI / 2), y2 + size * 0.5 * Math.sin(angle + Math.PI / 2));
2376
- ctx.stroke();
2300
+ function DrawFxLayer(_props) {
2301
+ return null;
2302
+ }
2303
+
2304
+ // lib/drawable/themeDrawables.ts
2305
+ function opt(value, resolve) {
2306
+ return value === void 0 ? void 0 : resolve(value);
2307
+ }
2308
+ function themeShape(node, resolve) {
2309
+ return {
2310
+ ...node,
2311
+ ...node.fill !== void 0 ? { fill: resolve(node.fill) } : {},
2312
+ ...node.stroke !== void 0 ? { stroke: resolve(node.stroke) } : {},
2313
+ ...node.gradient ? { gradient: { ...node.gradient, stops: node.gradient.stops.map((s) => ({ ...s, color: resolve(s.color) })) } } : {},
2314
+ ...node.fillPattern?.color !== void 0 ? { fillPattern: { ...node.fillPattern, color: resolve(node.fillPattern.color) } } : {},
2315
+ ...node.shadow ? { shadow: { ...node.shadow, color: resolve(node.shadow.color) } } : {},
2316
+ ...node.animation ? {
2317
+ animation: {
2318
+ ...node.animation,
2319
+ keyframes: node.animation.keyframes.map((k) => ({
2320
+ ...k,
2321
+ ...k.fill !== void 0 ? { fill: resolve(k.fill) } : {},
2322
+ ...k.stroke !== void 0 ? { stroke: resolve(k.stroke) } : {}
2323
+ }))
2377
2324
  }
2378
- break;
2379
- }
2380
- case "dot": {
2381
- ctx.beginPath();
2382
- ctx.arc(x2, y2, Math.max(2.5, width * 1.5), 0, Math.PI * 2);
2383
- ctx.fill();
2384
- break;
2325
+ } : {}
2326
+ };
2327
+ }
2328
+ function themeText(node, resolve) {
2329
+ return { ...node, color: resolve(node.color) };
2330
+ }
2331
+ function themeMaterial(material, resolve) {
2332
+ if (!material) return material;
2333
+ return {
2334
+ ...material,
2335
+ ...material.color !== void 0 ? { color: resolve(material.color) } : {},
2336
+ ...material.emissive !== void 0 ? { emissive: resolve(material.emissive) } : {}
2337
+ };
2338
+ }
2339
+ function resolveDrawableColors(nodes, resolve) {
2340
+ return nodes.map((node) => {
2341
+ switch (node.type) {
2342
+ case "draw-shape":
2343
+ return themeShape(node, resolve);
2344
+ case "draw-text":
2345
+ return themeText(node, resolve);
2346
+ case "draw-group":
2347
+ return { ...node, items: resolveDrawableColors(node.items, resolve) };
2348
+ case "draw-shape-layer":
2349
+ return { ...node, items: node.items.map((item) => themeShape(item, resolve)) };
2350
+ case "draw-text-layer":
2351
+ return { ...node, items: node.items.map((item) => themeText(item, resolve)) };
2352
+ case "draw-fx-layer":
2353
+ return {
2354
+ ...node,
2355
+ ...node.textColor !== void 0 ? { textColor: resolve(node.textColor) } : {},
2356
+ ...node.presets ? {
2357
+ presets: node.presets.map((p) => ({
2358
+ ...p,
2359
+ ...p.color !== void 0 ? { color: resolve(p.color) } : {},
2360
+ ...p.color2 !== void 0 ? { color2: resolve(p.color2) } : {}
2361
+ }))
2362
+ } : {}
2363
+ };
2364
+ case "draw-mesh":
2365
+ return {
2366
+ ...node,
2367
+ material: themeMaterial(node.material, resolve),
2368
+ ...node.outline ? { outline: { ...node.outline, ...node.outline.color !== void 0 ? { color: resolve(node.outline.color) } : {} } } : {},
2369
+ ...node.animation ? {
2370
+ animation: {
2371
+ ...node.animation,
2372
+ keyframes: node.animation.keyframes.map((k) => ({
2373
+ ...k,
2374
+ ...k.color !== void 0 ? { color: resolve(k.color) } : {},
2375
+ ...k.emissive !== void 0 ? { emissive: opt(k.emissive, resolve) } : {}
2376
+ }))
2377
+ }
2378
+ } : {}
2379
+ };
2380
+ default:
2381
+ return node;
2385
2382
  }
2386
- }
2387
- ctx.restore();
2383
+ });
2388
2384
  }
2389
- function useCanvasTheme(scopeRef) {
2390
- const { appliedTheme } = useTheme();
2391
- const scopeKey = JSON.stringify(useThemeScope());
2392
- const [state, setState] = useState({ theme: null, version: 0 });
2393
- useLayoutEffect(() => {
2394
- const scope = scopeRef.current;
2395
- if (!scope) return;
2396
- const theme = resolveCanvasTheme(scope);
2397
- setState((prev) => ({ theme, version: prev.version + 1 }));
2398
- }, [appliedTheme, scopeKey, scopeRef]);
2399
- return state;
2385
+ function applySceneMaterials(nodes, theme) {
2386
+ return nodes.map((node) => {
2387
+ if (node.type === "draw-group") return { ...node, items: applySceneMaterials(node.items, theme) };
2388
+ if (node.type !== "draw-mesh") return node;
2389
+ const m = node.material ?? {};
2390
+ const pbr = (m.kind ?? "standard") === "standard" || m.kind === "physical";
2391
+ return {
2392
+ ...node,
2393
+ material: {
2394
+ ...m,
2395
+ ...pbr && m.roughness === void 0 ? { roughness: theme.scene.roughness } : {},
2396
+ ...pbr && m.metalness === void 0 ? { metalness: theme.scene.metalness } : {},
2397
+ ...m.flatShading === void 0 && theme.scene.flat ? { flatShading: true } : {}
2398
+ },
2399
+ ...node.outline === void 0 && theme.scene.outline > 0 ? { outline: { color: theme.tones.ink, width: theme.scene.outline * 0.02 } } : {}
2400
+ };
2401
+ });
2400
2402
  }
2401
2403
 
2402
2404
  // lib/drawable/hitTest.ts