lecodes-cli 0.18.0 → 0.18.1

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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1222 -293
  3. package/package.json +4 -4
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk/compile/aspectMacro.ts +52 -8
  6. package/runtime/sdk/compile/assetMacro.ts +116 -15
  7. package/runtime/sdk/compile/bundler.ts +38 -3
  8. package/runtime/sdk/compile/index.ts +15 -0
  9. package/runtime/sdk/compile/sceneEditor.ts +11 -1
  10. package/runtime/sdk/compile/shaderSchema.ts +202 -0
  11. package/runtime/sdk/compile/shaderTargets.ts +42 -3
  12. package/runtime/sdk/core/Aspect.ts +512 -255
  13. package/runtime/sdk/core/compWrite.ts +42 -0
  14. package/runtime/sdk/core/fields.ts +1 -1
  15. package/runtime/sdk/core/time.ts +81 -0
  16. package/runtime/sdk/g2/Camera2D.ts +8 -1
  17. package/runtime/sdk/g2/CharacterController2D.ts +253 -53
  18. package/runtime/sdk/g2/Node2D.ts +80 -10
  19. package/runtime/sdk/g2/OneWay2D.ts +66 -0
  20. package/runtime/sdk/g2/Physics2D.ts +240 -30
  21. package/runtime/sdk/g2/Scene2D.ts +33 -1
  22. package/runtime/sdk/g2/Shape2D.ts +218 -22
  23. package/runtime/sdk/g2/Trigger2D.ts +42 -12
  24. package/runtime/sdk/g2/groups2d.ts +106 -0
  25. package/runtime/sdk/g2/loop.ts +15 -4
  26. package/runtime/sdk/gl/Camera.ts +1 -1
  27. package/runtime/sdk/gl/CameraPlace.ts +4 -3
  28. package/runtime/sdk/gl/CharacterController.ts +184 -56
  29. package/runtime/sdk/gl/Gearbox.ts +212 -0
  30. package/runtime/sdk/gl/Geometry.ts +70 -9
  31. package/runtime/sdk/gl/Light.ts +64 -2
  32. package/runtime/sdk/gl/Lightmap.ts +179 -0
  33. package/runtime/sdk/gl/Material.ts +25 -0
  34. package/runtime/sdk/gl/Mesh.ts +6 -23
  35. package/runtime/sdk/gl/Model.ts +16 -2
  36. package/runtime/sdk/gl/Node.ts +119 -39
  37. package/runtime/sdk/gl/Physics.ts +75 -24
  38. package/runtime/sdk/gl/Scene.ts +148 -5
  39. package/runtime/sdk/gl/Shape.ts +42 -3
  40. package/runtime/sdk/gl/Trigger.ts +1 -6
  41. package/runtime/sdk/gl/Vehicle.ts +276 -322
  42. package/runtime/sdk/gl/Wheel.ts +240 -0
  43. package/runtime/sdk/gl/scenarios.ts +291 -317
  44. package/runtime/sdk/inject.ts +186 -171
  45. package/runtime/sdk/scene/defineScene.ts +1227 -1172
  46. package/runtime/sdk/scene/gizmos.ts +148 -128
  47. package/runtime/sdk/scene/material.ts +188 -0
  48. package/runtime/sdk-types.json +1 -1
package/dist/index.js CHANGED
@@ -418,7 +418,7 @@ node_modules/
418
418
  return "updated";
419
419
  };
420
420
  var init_ignore = __esm(() => {
421
- BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/"];
421
+ BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/", "dist/"];
422
422
  });
423
423
 
424
424
  // src/localFiles.ts
@@ -564,45 +564,212 @@ var init_distRoot = __esm(() => {
564
564
  });
565
565
 
566
566
  // ../sdk/src/compile/assetMacro.ts
567
- var ASSET_RE, hasAssetMacro = (src) => src.includes("asset("), desugarAssetMacros = (src, filePath) => {
568
- const byPath = new Map;
567
+ var ASSET_RE, ASSET_CALL_RE, hasAssetMacro = (src) => src.includes("asset("), maskNonCode = (src) => {
568
+ const out = src.split("");
569
+ const blank = (from, to) => {
570
+ for (let k = from;k < to && k < out.length; k++)
571
+ if (out[k] !== `
572
+ `)
573
+ out[k] = " ";
574
+ };
575
+ const tpl = [];
576
+ let braces = 0;
569
577
  let i = 0;
570
- const out = src.replace(ASSET_RE, (_match, _quote, path) => {
571
- let name = byPath.get(path);
572
- if (!name) {
573
- name = `__asset_${i++}`;
574
- byPath.set(path, name);
578
+ const scanTemplate = () => {
579
+ const start = i;
580
+ while (i < src.length) {
581
+ const c2 = src[i];
582
+ if (c2 === "\\") {
583
+ i += 2;
584
+ continue;
585
+ }
586
+ if (c2 === "`") {
587
+ blank(start, i);
588
+ i++;
589
+ return;
590
+ }
591
+ if (c2 === "$" && src[i + 1] === "{") {
592
+ blank(start, i);
593
+ tpl.push(braces);
594
+ i += 2;
595
+ return;
596
+ }
597
+ i++;
575
598
  }
576
- return name;
577
- });
578
- if (/(?<![.\w$])asset\(/.test(out)) {
579
- throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
599
+ blank(start, i);
600
+ };
601
+ while (i < src.length) {
602
+ const c2 = src[i], d = src[i + 1];
603
+ if (c2 === "/" && d === "/") {
604
+ const e = src.indexOf(`
605
+ `, i);
606
+ const stop = e === -1 ? src.length : e;
607
+ blank(i, stop);
608
+ i = stop;
609
+ continue;
610
+ }
611
+ if (c2 === "/" && d === "*") {
612
+ const e = src.indexOf("*/", i + 2);
613
+ const stop = e === -1 ? src.length : e + 2;
614
+ blank(i, stop);
615
+ i = stop;
616
+ continue;
617
+ }
618
+ if (c2 === "'" || c2 === '"') {
619
+ const start = i;
620
+ i++;
621
+ while (i < src.length) {
622
+ const q = src[i];
623
+ if (q === "\\") {
624
+ i += 2;
625
+ continue;
626
+ }
627
+ i++;
628
+ if (q === c2 || q === `
629
+ `)
630
+ break;
631
+ }
632
+ blank(start, i);
633
+ continue;
634
+ }
635
+ if (c2 === "`") {
636
+ i++;
637
+ scanTemplate();
638
+ continue;
639
+ }
640
+ if (c2 === "{") {
641
+ braces++;
642
+ i++;
643
+ continue;
644
+ }
645
+ if (c2 === "}") {
646
+ if (tpl.length > 0 && braces === tpl[tpl.length - 1]) {
647
+ tpl.pop();
648
+ i++;
649
+ scanTemplate();
650
+ continue;
651
+ }
652
+ if (braces > 0)
653
+ braces--;
654
+ i++;
655
+ continue;
656
+ }
657
+ i++;
580
658
  }
581
- if (byPath.size === 0)
659
+ return out.join("");
660
+ }, collectAssetMacros = (src, filePath) => {
661
+ const mask = maskNonCode(src);
662
+ const refs = [];
663
+ const literal = new Set;
664
+ let scanned = 0;
665
+ let line = 1;
666
+ for (const m of src.matchAll(ASSET_RE)) {
667
+ const index = m.index ?? 0;
668
+ if (mask[index] !== "a")
669
+ continue;
670
+ for (;scanned < index; scanned++)
671
+ if (src[scanned] === `
672
+ `)
673
+ line++;
674
+ refs.push({ path: m[2], index, length: m[0].length, line });
675
+ literal.add(index);
676
+ }
677
+ for (const m of src.matchAll(ASSET_CALL_RE)) {
678
+ const index = m.index ?? 0;
679
+ if (mask[index] === "a" && !literal.has(index)) {
680
+ throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
681
+ }
682
+ }
683
+ return refs;
684
+ }, desugarAssetMacros = (src, filePath, found) => {
685
+ const refs = found ?? collectAssetMacros(src, filePath);
686
+ if (refs.length === 0)
582
687
  return src;
688
+ const byPath = new Map;
689
+ const parts = [];
690
+ let at = 0;
691
+ for (const ref of refs) {
692
+ let name = byPath.get(ref.path);
693
+ if (!name) {
694
+ name = `__asset_${byPath.size}`;
695
+ byPath.set(ref.path, name);
696
+ }
697
+ parts.push(src.slice(at, ref.index), name);
698
+ at = ref.index + ref.length;
699
+ }
700
+ parts.push(src.slice(at));
583
701
  const imports = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
584
702
  `);
585
- return out + `
703
+ return parts.join("") + `
586
704
  ` + imports;
587
705
  };
588
706
  var init_assetMacro = __esm(() => {
589
707
  ASSET_RE = /(?<![.\w$])asset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
708
+ ASSET_CALL_RE = /(?<![.\w$])asset\(/g;
590
709
  });
591
710
 
592
711
  // ../sdk/src/compile/aspectMacro.ts
593
- var ASPECT_RE, hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src), injectAspectNames = (src) => {
712
+ var HEAD_RE, hasAspectExtends = (src) => /\bextends\s+(?:Aspect|System)\s*</.test(src), injectAspectNames = (src) => {
594
713
  let out = "";
595
714
  let last = 0;
596
- ASPECT_RE.lastIndex = 0;
715
+ HEAD_RE.lastIndex = 0;
597
716
  let m;
598
- while ((m = ASPECT_RE.exec(src)) !== null) {
599
- const braceEnd = m.index + m[0].length;
717
+ while ((m = HEAD_RE.exec(src)) !== null) {
718
+ const braceEnd = classBodyStart(src, m.index + m[0].length);
719
+ if (braceEnd < 0)
720
+ continue;
721
+ HEAD_RE.lastIndex = braceEnd;
600
722
  if (!classBodyDeclaresAspect(src, braceEnd)) {
601
723
  out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`;
602
724
  last = braceEnd;
603
725
  }
604
726
  }
605
727
  return last === 0 ? src : out + src.slice(last);
728
+ }, classBodyStart = (src, from) => {
729
+ let angle = 1;
730
+ let i = from;
731
+ for (;i < src.length && angle > 0; i++) {
732
+ const c2 = src[i];
733
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
734
+ i = skipString(src, i) - 1;
735
+ continue;
736
+ }
737
+ if (c2 === "=" && src[i + 1] === ">") {
738
+ i++;
739
+ continue;
740
+ }
741
+ if (c2 === "<")
742
+ angle++;
743
+ else if (c2 === ">")
744
+ angle--;
745
+ }
746
+ if (angle !== 0)
747
+ return -1;
748
+ angle = 0;
749
+ for (;i < src.length; i++) {
750
+ const c2 = src[i];
751
+ if (c2 === "<")
752
+ angle++;
753
+ else if (c2 === ">")
754
+ angle--;
755
+ else if (c2 === "{" && angle === 0)
756
+ return i + 1;
757
+ else if (c2 === ";" || c2 === "}")
758
+ return -1;
759
+ }
760
+ return -1;
761
+ }, skipString = (src, at) => {
762
+ const q = src[at];
763
+ let i = at + 1;
764
+ for (;i < src.length; i++) {
765
+ if (src[i] === "\\") {
766
+ i++;
767
+ continue;
768
+ }
769
+ if (src[i] === q)
770
+ return i + 1;
771
+ }
772
+ return i;
606
773
  }, classBodyDeclaresAspect = (src, from) => {
607
774
  let depth = 1;
608
775
  let i = from;
@@ -616,7 +783,7 @@ var ASPECT_RE, hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src), i
616
783
  return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i));
617
784
  };
618
785
  var init_aspectMacro = __esm(() => {
619
- ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g;
786
+ HEAD_RE = /\bclass\s+\w+\s+extends\s+(?:Aspect|System)\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1/g;
620
787
  });
621
788
 
622
789
  // ../sdk/src/compile/libraryImports.ts
@@ -723,18 +890,37 @@ var resolveInject = () => {
723
890
  } catch {
724
891
  return null;
725
892
  }
893
+ }, assertAssetsExist = (refs, files) => {
894
+ const missing = [];
895
+ for (const { file, ref } of refs) {
896
+ if (!ref.path.startsWith("./") && !ref.path.startsWith("../") && !ref.path.startsWith("/"))
897
+ continue;
898
+ const dir = file.slice(0, file.lastIndexOf("/"));
899
+ const base = ref.path.startsWith("/") ? normalize(ref.path) : normalize(`${dir}/${ref.path}`);
900
+ const found = [base, `${base}.ts`, `${base}.js`, `${base}/index.ts`].some((c2) => files[c2] !== undefined);
901
+ if (!found)
902
+ missing.push(`asset not found: ${ref.path} (${file.replace(/^\//, "")}:${ref.line})`);
903
+ }
904
+ if (missing.length > 0)
905
+ throw new Error(missing.join(`
906
+ `));
726
907
  }, bundleProjectChisel = async (entrypoint, files, format, sourcemap, minifyOverride, editor, libraries, inject = CLIENT_INJECT) => {
727
908
  const entry = normalize(entrypoint);
728
909
  const slugs = new Set(Object.keys(libraries ?? {}));
729
910
  const projFiles = {};
911
+ const assetRefs = [];
730
912
  for (const [k, v] of Object.entries(files)) {
731
913
  const path = normalize(k);
732
914
  let src = v;
733
915
  if (/\.[tj]sx?$/.test(path)) {
734
916
  if (hasImportStatement(src))
735
917
  src = rewriteLibraryImports(src, slugs);
736
- if (hasAssetMacro(src))
737
- src = desugarAssetMacros(src, path);
918
+ if (hasAssetMacro(src)) {
919
+ const found = collectAssetMacros(src, path);
920
+ for (const ref of found)
921
+ assetRefs.push({ file: path, ref });
922
+ src = desugarAssetMacros(src, path, found);
923
+ }
738
924
  if (hasAspectExtends(src))
739
925
  src = injectAspectNames(src);
740
926
  }
@@ -747,14 +933,19 @@ var resolveInject = () => {
747
933
  const path = normalize(`/__lib/${slug}/${k}`);
748
934
  let src = v;
749
935
  if (/\.[tj]sx?$/.test(path)) {
750
- if (hasAssetMacro(src))
751
- src = desugarAssetMacros(src, path);
936
+ if (hasAssetMacro(src)) {
937
+ const found = collectAssetMacros(src, path);
938
+ for (const ref of found)
939
+ assetRefs.push({ file: path, ref });
940
+ src = desugarAssetMacros(src, path, found);
941
+ }
752
942
  if (hasAspectExtends(src))
753
943
  src = injectAspectNames(src);
754
944
  }
755
945
  projFiles[path] = src;
756
946
  }
757
947
  }
948
+ assertAssetsExist(assetRefs, projFiles);
758
949
  const sdkFiles = collectSdkFiles(resolveSdkSrc());
759
950
  const minify = minifyOverride ?? process.env.LECODES_CHISEL_MINIFY !== "0";
760
951
  const input = JSON.stringify({
@@ -98214,23 +98405,23 @@ ${lanes.join(`
98214
98405
  const call = cast(aliasAssignment ? skipOuterExpressions(aliasAssignment.right) : initializer, isCallExpression);
98215
98406
  const func = cast(skipOuterExpressions(call.expression), isFunctionExpression);
98216
98407
  const funcStatements = func.body.statements;
98217
- let classBodyStart = 0;
98408
+ let classBodyStart2 = 0;
98218
98409
  let classBodyEnd = -1;
98219
98410
  const statements = [];
98220
98411
  if (aliasAssignment) {
98221
- const extendsCall = tryCast(funcStatements[classBodyStart], isExpressionStatement);
98412
+ const extendsCall = tryCast(funcStatements[classBodyStart2], isExpressionStatement);
98222
98413
  if (extendsCall) {
98223
98414
  statements.push(extendsCall);
98224
- classBodyStart++;
98415
+ classBodyStart2++;
98225
98416
  }
98226
- statements.push(funcStatements[classBodyStart]);
98227
- classBodyStart++;
98417
+ statements.push(funcStatements[classBodyStart2]);
98418
+ classBodyStart2++;
98228
98419
  statements.push(factory2.createExpressionStatement(factory2.createAssignment(aliasAssignment.left, cast(variable.name, isIdentifier))));
98229
98420
  }
98230
98421
  while (!isReturnStatement(elementAt(funcStatements, classBodyEnd))) {
98231
98422
  classBodyEnd--;
98232
98423
  }
98233
- addRange(statements, funcStatements, classBodyStart, classBodyEnd);
98424
+ addRange(statements, funcStatements, classBodyStart2, classBodyEnd);
98234
98425
  if (classBodyEnd < -1) {
98235
98426
  addRange(statements, funcStatements, classBodyEnd + 1);
98236
98427
  }
@@ -171460,11 +171651,11 @@ var init_detectEntry = __esm(() => {
171460
171651
  });
171461
171652
 
171462
171653
  // ../sdk/src/compile/shaderTargets.ts
171463
- var externalSamplerReg, PLATFORM_BUILD, matcPlatformsFor = (text) => {
171654
+ var externalSamplerReg, PLATFORM_BUILD, LOCAL_PLATFORM_BUILD, ALL_PLATFORM_BUILD, LOCAL_ONLY_PLATFORMS, TARGET_MATRIX_GENERATION = 1, matcPlatformsFor = (text) => {
171464
171655
  externalSamplerReg.lastIndex = 0;
171465
171656
  return externalSamplerReg.test(text) ? ["opengl", "metal", "webgl", "desktop"] : ["opengl", "metal", "desktop"];
171466
171657
  }, matcArgs = (platform, outPath, inPath) => {
171467
- const build = PLATFORM_BUILD[platform];
171658
+ const build = ALL_PLATFORM_BUILD[platform];
171468
171659
  return ["-p", build.profile, "--no-essl1", "-a", build.api, "-o", outPath, inPath];
171469
171660
  };
171470
171661
  var init_shaderTargets = __esm(() => {
@@ -171475,6 +171666,11 @@ var init_shaderTargets = __esm(() => {
171475
171666
  webgl: { profile: "mobile", api: "opengl", replaceExternal: true },
171476
171667
  desktop: { profile: "desktop", api: "opengl", replaceExternal: true }
171477
171668
  };
171669
+ LOCAL_PLATFORM_BUILD = {
171670
+ "desktop-vulkan": { profile: "desktop", api: "vulkan", replaceExternal: true }
171671
+ };
171672
+ ALL_PLATFORM_BUILD = { ...PLATFORM_BUILD, ...LOCAL_PLATFORM_BUILD };
171673
+ LOCAL_ONLY_PLATFORMS = ["desktop-vulkan"];
171478
171674
  });
171479
171675
 
171480
171676
  // ../sdk/src/compile/liteMaterial.ts
@@ -171717,6 +171913,191 @@ var init_liteMaterial = __esm(() => {
171717
171913
  SAMPLER_TYPES = new Set(["sampler2d", "samplerExternal"]);
171718
171914
  });
171719
171915
 
171916
+ // ../sdk/src/compile/shaderSchema.ts
171917
+ var SAMPLER_TYPES2, COLOR_NAME, zeroFor = (type, editor) => {
171918
+ switch (editor) {
171919
+ case "number":
171920
+ return 0;
171921
+ case "int":
171922
+ return 0;
171923
+ case "switch":
171924
+ return false;
171925
+ case "vec2":
171926
+ return [0, 0];
171927
+ case "vec3":
171928
+ return [0, 0, 0];
171929
+ case "vec4":
171930
+ return [0, 0, 0, 0];
171931
+ case "color":
171932
+ return type === "float4" ? "#ffffffff" : "#ffffff";
171933
+ case "texture":
171934
+ return null;
171935
+ default:
171936
+ return null;
171937
+ }
171938
+ }, parseDefault = (raw, editor) => {
171939
+ const s = raw.trim();
171940
+ if (s === "true")
171941
+ return true;
171942
+ if (s === "false")
171943
+ return false;
171944
+ if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(s))
171945
+ return s.toLowerCase();
171946
+ if (/^-?\d+(\.\d+)?$/.test(s))
171947
+ return editor === "switch" ? Number(s) !== 0 : Number(s);
171948
+ const arr = /^\[(.*)\]$/.exec(s);
171949
+ if (arr) {
171950
+ const nums = arr[1].split(",").map((p) => Number(p.trim()));
171951
+ if (nums.length > 0 && nums.every((n) => Number.isFinite(n)))
171952
+ return nums;
171953
+ }
171954
+ return;
171955
+ }, parseAnnotation = (comment) => {
171956
+ const out = { tooltip: "", errors: [] };
171957
+ let rest = comment;
171958
+ rest = rest.replace(/@(color|vec)\b/g, (_m, tag) => {
171959
+ out[tag] = true;
171960
+ return " ";
171961
+ });
171962
+ rest = rest.replace(/@(range|step|default)\s*\(([^)]*)\)/g, (_m, tag, body) => {
171963
+ if (tag === "range") {
171964
+ const parts = body.split(",").map((p) => Number(p.trim()));
171965
+ if (parts.length === 2 && parts.every((n) => Number.isFinite(n)))
171966
+ out.range = [parts[0], parts[1]];
171967
+ else
171968
+ out.errors.push(`@range expects two numbers, got "${body.trim()}"`);
171969
+ } else if (tag === "step") {
171970
+ const n = Number(body.trim());
171971
+ if (Number.isFinite(n) && n > 0)
171972
+ out.step = n;
171973
+ else
171974
+ out.errors.push(`@step expects a positive number, got "${body.trim()}"`);
171975
+ } else {
171976
+ out.default = body;
171977
+ }
171978
+ return " ";
171979
+ });
171980
+ rest = rest.replace(/@\w+(\([^)]*\))?/g, " ");
171981
+ out.tooltip = rest.replace(/\s+/g, " ").trim();
171982
+ return out;
171983
+ }, collectComments = (materialBlock) => {
171984
+ const out = new Map;
171985
+ const start = materialBlock.search(/\bparameters\s*[:=]/);
171986
+ if (start < 0)
171987
+ return out;
171988
+ const lines = materialBlock.slice(start).split(`
171989
+ `);
171990
+ let above = [];
171991
+ for (const line of lines) {
171992
+ const trimmed = line.trim();
171993
+ const name = /\bname\s*[:=]\s*"?([A-Za-z_]\w*)"?/.exec(line)?.[1];
171994
+ if (name) {
171995
+ const trailing = /\}[^/\n]*\/\/(.*)$/.exec(line)?.[1] ?? /\/\/(.*)$/.exec(line.slice(line.indexOf("}") + 1))?.[1];
171996
+ const text = trailing !== undefined ? trailing : above.join(" ");
171997
+ if (text.trim() !== "")
171998
+ out.set(name, text.trim());
171999
+ above = [];
172000
+ } else if (trimmed.startsWith("//")) {
172001
+ above.push(trimmed.slice(2).trim());
172002
+ } else if (trimmed !== "" && trimmed !== "[" && trimmed !== "],") {
172003
+ above = [];
172004
+ }
172005
+ }
172006
+ return out;
172007
+ }, parseShaderSchema = (text) => {
172008
+ const schema = { name: "", shadingModel: "lit", blending: "opaque", params: [], errors: [] };
172009
+ let block;
172010
+ let def;
172011
+ try {
172012
+ block = extractMatBlocks(text).material;
172013
+ if (block === undefined) {
172014
+ schema.errors.push("no material block");
172015
+ return schema;
172016
+ }
172017
+ def = parseJsonish("{" + block + "}");
172018
+ } catch (e) {
172019
+ schema.errors.push(e instanceof Error ? e.message : String(e));
172020
+ return schema;
172021
+ }
172022
+ schema.name = typeof def.name === "string" ? def.name : "";
172023
+ schema.shadingModel = typeof def.shadingModel === "string" ? def.shadingModel : "lit";
172024
+ schema.blending = typeof def.blending === "string" ? def.blending : "opaque";
172025
+ const comments = collectComments(block);
172026
+ const rawParams = def.parameters === undefined ? [] : Array.isArray(def.parameters) ? def.parameters : [def.parameters];
172027
+ for (const raw of rawParams) {
172028
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
172029
+ schema.errors.push("malformed parameter entry");
172030
+ continue;
172031
+ }
172032
+ const entry = raw;
172033
+ const name = String(entry.name ?? "");
172034
+ const typeRaw = String(entry.type ?? "");
172035
+ const typeMatch = /^(\w+)(?:\[(\d+)\])?$/.exec(typeRaw);
172036
+ if (!name || !typeMatch) {
172037
+ schema.errors.push(`malformed parameter "${name || typeRaw}"`);
172038
+ continue;
172039
+ }
172040
+ const type = typeMatch[1];
172041
+ const size = typeMatch[2] ? Number(typeMatch[2]) : undefined;
172042
+ const ann = parseAnnotation(comments.get(name) ?? "");
172043
+ for (const e of ann.errors)
172044
+ schema.errors.push(`${name}: ${e}`);
172045
+ let editor;
172046
+ if (size !== undefined)
172047
+ editor = "array";
172048
+ else if (SAMPLER_TYPES2.has(type))
172049
+ editor = "texture";
172050
+ else if (type === "float")
172051
+ editor = "number";
172052
+ else if (type === "int" || type === "uint")
172053
+ editor = "int";
172054
+ else if (type === "bool")
172055
+ editor = "switch";
172056
+ else if (type === "float2")
172057
+ editor = "vec2";
172058
+ else if (type === "float3" || type === "float4") {
172059
+ const isColor = ann.color === true || ann.vec !== true && COLOR_NAME.test(name);
172060
+ editor = isColor ? "color" : type === "float3" ? "vec3" : "vec4";
172061
+ } else if (/^(int|uint|bool)[234]$/.test(type))
172062
+ editor = `vec${type.slice(-1)}`;
172063
+ else if (/^float[234]x[234]$/.test(type))
172064
+ editor = "matrix";
172065
+ else
172066
+ editor = "array";
172067
+ const param = { name, type, editor, default: zeroFor(type, editor) };
172068
+ if (size !== undefined)
172069
+ param.size = size;
172070
+ if (ann.range) {
172071
+ param.min = ann.range[0];
172072
+ param.max = ann.range[1];
172073
+ }
172074
+ if (ann.step !== undefined)
172075
+ param.step = ann.step;
172076
+ if (ann.default !== undefined) {
172077
+ const v = parseDefault(ann.default, editor);
172078
+ if (v === undefined)
172079
+ schema.errors.push(`${name}: unreadable @default(${ann.default.trim()})`);
172080
+ else
172081
+ param.default = v;
172082
+ }
172083
+ if (ann.tooltip)
172084
+ param.tooltip = ann.tooltip;
172085
+ schema.params.push(param);
172086
+ }
172087
+ return schema;
172088
+ }, shaderDefaults = (schema) => {
172089
+ const out = {};
172090
+ for (const p of schema.params)
172091
+ if (p.editor !== "texture" && p.default !== null)
172092
+ out[p.name] = p.default;
172093
+ return out;
172094
+ };
172095
+ var init_shaderSchema = __esm(() => {
172096
+ init_liteMaterial();
172097
+ SAMPLER_TYPES2 = new Set(["sampler2d", "samplerExternal", "sampler2dArray", "samplerCubemap", "sampler3d"]);
172098
+ COLOR_NAME = /colou?r|tint|albedo|emissive|glare|glow/i;
172099
+ });
172100
+
171720
172101
  // ../sdk/src/compile/sourcemap.ts
171721
172102
  var offsetSourceMap = (mapJson, lines) => {
171722
172103
  if (lines <= 0)
@@ -171865,13 +172246,18 @@ export {}
171865
172246
  });
171866
172247
  const editorImports = projectPaths.filter((p) => p.endsWith(".editor.ts")).map((p) => `import "${p}"
171867
172248
  `).join("");
172249
+ const materialPaths = projectPaths.filter((p) => p.endsWith(".material.ts"));
172250
+ const materialImports = materialPaths.map((p, i) => `import __mat${i} from "${p}"
172251
+ `).join("");
172252
+ const materialRegister = materialPaths.length === 0 ? "" : `;(globalThis as any).__lecodesMaterials = { ${materialPaths.map((p, i) => `${JSON.stringify(p)}: __mat${i}`).join(", ")} }
172253
+ `;
171868
172254
  entries.push({
171869
172255
  path: SCENE_ENTRY_PATH,
171870
172256
  type: "text",
171871
172257
  text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"
171872
- ` + `import "${scenePath}"
172258
+ ` + materialImports + `import "${scenePath}"
171873
172259
  ` + editorImports + `import "${SCENE_HARNESS_DIR}/main.ts"
171874
- `
172260
+ ` + materialRegister
171875
172261
  });
171876
172262
  return entries;
171877
172263
  };
@@ -172002,7 +172388,7 @@ var TAG_TTF = 65536, TAG_OTTO = 1330926671, TAG_TTCF = 1953784678, TAG_WOFF = 20
172002
172388
  };
172003
172389
 
172004
172390
  // ../sdk/src/compile/fontMacro.ts
172005
- var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart = (c2) => isIdentStart(c2) || c2 >= "0" && c2 <= "9", skipString = (src, start) => {
172391
+ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart = (c2) => isIdentStart(c2) || c2 >= "0" && c2 <= "9", skipString2 = (src, start) => {
172006
172392
  const quote = src[start];
172007
172393
  const n = src.length;
172008
172394
  let i = start + 1;
@@ -172028,7 +172414,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
172028
172414
  while (i < n) {
172029
172415
  const c2 = src[i];
172030
172416
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172031
- i = skipString(src, i);
172417
+ i = skipString2(src, i);
172032
172418
  continue;
172033
172419
  }
172034
172420
  if (c2 === "{")
@@ -172048,7 +172434,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
172048
172434
  while (i < n) {
172049
172435
  const c2 = src[i];
172050
172436
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172051
- i = skipString(src, i);
172437
+ i = skipString2(src, i);
172052
172438
  continue;
172053
172439
  }
172054
172440
  if (c2 === "/" && src[i + 1] === "/") {
@@ -172084,7 +172470,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
172084
172470
  while (i < n) {
172085
172471
  const c2 = text[i];
172086
172472
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172087
- i = skipString(text, i);
172473
+ i = skipString2(text, i);
172088
172474
  continue;
172089
172475
  }
172090
172476
  if (c2 === "(" || c2 === "[" || c2 === "{")
@@ -172124,7 +172510,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
172124
172510
  while (i < n) {
172125
172511
  const c2 = source[i];
172126
172512
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172127
- i = skipString(source, i);
172513
+ i = skipString2(source, i);
172128
172514
  continue;
172129
172515
  }
172130
172516
  if (c2 === "/" && source[i + 1] === "/") {
@@ -172412,7 +172798,7 @@ var init_fontMacro = __esm(() => {
172412
172798
  });
172413
172799
 
172414
172800
  // ../sdk/src/compile/assetIconMacro.ts
172415
- var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart2 = (c2) => isIdentStart2(c2) || c2 >= "0" && c2 <= "9", skipString2 = (src, start) => {
172801
+ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart2 = (c2) => isIdentStart2(c2) || c2 >= "0" && c2 <= "9", skipString3 = (src, start) => {
172416
172802
  const quote = src[start];
172417
172803
  const n = src.length;
172418
172804
  let i = start + 1;
@@ -172438,7 +172824,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
172438
172824
  while (i < n) {
172439
172825
  const c2 = src[i];
172440
172826
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172441
- i = skipString2(src, i);
172827
+ i = skipString3(src, i);
172442
172828
  continue;
172443
172829
  }
172444
172830
  if (c2 === "{")
@@ -172458,7 +172844,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
172458
172844
  while (i < n) {
172459
172845
  const c2 = src[i];
172460
172846
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172461
- i = skipString2(src, i);
172847
+ i = skipString3(src, i);
172462
172848
  continue;
172463
172849
  }
172464
172850
  if (c2 === "/" && src[i + 1] === "/") {
@@ -172494,7 +172880,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
172494
172880
  while (i < n) {
172495
172881
  const c2 = text[i];
172496
172882
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172497
- i = skipString2(text, i);
172883
+ i = skipString3(text, i);
172498
172884
  continue;
172499
172885
  }
172500
172886
  if (c2 === "(" || c2 === "[" || c2 === "{")
@@ -172529,7 +172915,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
172529
172915
  while (i < n) {
172530
172916
  const c2 = source[i];
172531
172917
  if (c2 === '"' || c2 === "'" || c2 === "`") {
172532
- i = skipString2(source, i);
172918
+ i = skipString3(source, i);
172533
172919
  continue;
172534
172920
  }
172535
172921
  if (c2 === "/" && source[i + 1] === "/") {
@@ -172731,6 +173117,7 @@ var exports_compile = {};
172731
173117
  __export(exports_compile, {
172732
173118
  suggestIconNames: () => suggestIconNames,
172733
173119
  stubServerEntries: () => stubServerEntries,
173120
+ shaderDefaults: () => shaderDefaults,
172734
173121
  serverStubSource: () => serverStubSource,
172735
173122
  serverModuleId: () => serverModuleId,
172736
173123
  serverEntrySource: () => serverEntrySource,
@@ -172743,6 +173130,7 @@ __export(exports_compile, {
172743
173130
  scanAssetIconCalls: () => scanAssetIconCalls,
172744
173131
  sanitizeAssetName: () => sanitizeAssetName,
172745
173132
  rewriteAssetIcons: () => rewriteAssetIcons,
173133
+ parseShaderSchema: () => parseShaderSchema,
172746
173134
  parseSfnt: () => parseSfnt,
172747
173135
  parseJsonish: () => parseJsonish,
172748
173136
  offsetSourceMap: () => offsetSourceMap,
@@ -172771,17 +173159,21 @@ __export(exports_compile, {
172771
173159
  buildFontsHeaderValue: () => buildFontsHeaderValue,
172772
173160
  applyFontMacro: () => applyFontMacro,
172773
173161
  applyAssetIconMacro: () => applyAssetIconMacro,
173162
+ TARGET_MATRIX_GENERATION: () => TARGET_MATRIX_GENERATION,
172774
173163
  SERVER_INJECT: () => SERVER_INJECT,
172775
173164
  SERVER_ENTRY_PATH: () => SERVER_ENTRY_PATH,
172776
173165
  SCENE_HARNESS_DIR: () => SCENE_HARNESS_DIR,
172777
173166
  SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH,
172778
173167
  PLATFORM_BUILD: () => PLATFORM_BUILD,
173168
+ LOCAL_PLATFORM_BUILD: () => LOCAL_PLATFORM_BUILD,
173169
+ LOCAL_ONLY_PLATFORMS: () => LOCAL_ONLY_PLATFORMS,
172779
173170
  ICON_ID: () => ICON_ID,
172780
173171
  FONT_REGISTRY: () => FONT_REGISTRY,
172781
173172
  EndpointTypeError: () => EndpointTypeError,
172782
173173
  ENTRY_CONVENTION: () => ENTRY_CONVENTION,
172783
173174
  DEFAULT_FONT_REGISTRY_BASE: () => DEFAULT_FONT_REGISTRY_BASE,
172784
- CLIENT_INJECT: () => CLIENT_INJECT
173175
+ CLIENT_INJECT: () => CLIENT_INJECT,
173176
+ ALL_PLATFORM_BUILD: () => ALL_PLATFORM_BUILD
172785
173177
  });
172786
173178
  var init_compile = __esm(() => {
172787
173179
  init_bundler();
@@ -172791,6 +173183,7 @@ var init_compile = __esm(() => {
172791
173183
  init_assetName();
172792
173184
  init_shaderTargets();
172793
173185
  init_liteMaterial();
173186
+ init_shaderSchema();
172794
173187
  init_compileProject();
172795
173188
  init_sceneEditor();
172796
173189
  init_fontMacro();
@@ -214358,6 +214751,10 @@ lecodes design snapshot <id> --state empty # one state → design/.snapshots/<i
214358
214751
  lecodes design snapshot <id> --all-states # every state → design/.snapshots/<id>@<state>.png|.json
214359
214752
  lecodes design snapshot # all screens → design/.snapshots/
214360
214753
  lecodes design check # concept + flow coherence (spec.md links, states, untagged)
214754
+
214755
+ lecodes render <id> [--state <s>] # the same screen through the full render CLI, when you
214756
+ lecodes render <id> --png card.png --clip <name> # need one element cropped out, a device's
214757
+ lecodes render <id> --device iphone --png x.png # real safe-area insets, or a --script run
214361
214758
  \`\`\`
214362
214759
 
214363
214760
  Either way: after every screen you create or change, look at it and fix what's wrong (overflow,
@@ -214433,14 +214830,37 @@ var init_designTemplates = __esm(() => {
214433
214830
  init_templates();
214434
214831
  });
214435
214832
 
214833
+ // src/designMeta.ts
214834
+ import { readFileSync as readFileSync19 } from "node:fs";
214835
+ import { join as join23 } from "node:path";
214836
+ var PHONE_SIZE, readMetaFrames = (designDir) => {
214837
+ try {
214838
+ const meta = JSON.parse(readFileSync19(join23(designDir, "meta.json"), "utf8"));
214839
+ const d = meta?.defaultSize;
214840
+ const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
214841
+ return {
214842
+ screens: meta?.screens ?? {},
214843
+ defaultSize: valid ? [Math.round(d[0]), Math.round(d[1])] : PHONE_SIZE
214844
+ };
214845
+ } catch {
214846
+ return { screens: {}, defaultSize: PHONE_SIZE };
214847
+ }
214848
+ }, screenFrame = (designDir, screenId) => {
214849
+ const frames = readMetaFrames(designDir);
214850
+ return frames.screens[screenId]?.size ?? frames.defaultSize;
214851
+ };
214852
+ var init_designMeta = __esm(() => {
214853
+ PHONE_SIZE = [390, 844];
214854
+ });
214855
+
214436
214856
  // src/commands/design.ts
214437
214857
  var exports_design = {};
214438
214858
  __export(exports_design, {
214439
214859
  flushDesignComments: () => flushDesignComments,
214440
214860
  design: () => design
214441
214861
  });
214442
- import { existsSync as existsSync23, mkdirSync as mkdirSync17, readFileSync as readFileSync19, readdirSync as readdirSync12, writeFileSync as writeFileSync18, appendFileSync } from "node:fs";
214443
- import { basename as basename9, join as join23 } from "node:path";
214862
+ import { existsSync as existsSync23, mkdirSync as mkdirSync17, readFileSync as readFileSync20, readdirSync as readdirSync12, writeFileSync as writeFileSync18, appendFileSync } from "node:fs";
214863
+ import { basename as basename9, join as join24 } from "node:path";
214444
214864
  var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214445
214865
  let root;
214446
214866
  let manifest;
@@ -214457,7 +214877,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214457
214877
  manifest,
214458
214878
  name: manifest?.name ?? basename9(root),
214459
214879
  dirName,
214460
- designDir: join23(root, dirName),
214880
+ designDir: join24(root, dirName),
214461
214881
  designKey: "/" + dirName.replace(/\\/g, "/")
214462
214882
  };
214463
214883
  }, loadDesignServer = async (ctx, needs = []) => {
@@ -214493,29 +214913,17 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214493
214913
  `).find((l2) => l2.trim())?.trim() ?? "";
214494
214914
  return line.length > max ? line.slice(0, max - 1) + "…" : line;
214495
214915
  }, listScreens = (designDir) => {
214496
- const dir = join23(designDir, "screens");
214916
+ const dir = join24(designDir, "screens");
214497
214917
  if (!existsSync23(dir))
214498
214918
  return [];
214499
214919
  return readdirSync12(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
214500
- }, PHONE_SIZE, readMetaFrames = (designDir) => {
214501
- try {
214502
- const meta = JSON.parse(readFileSync19(join23(designDir, "meta.json"), "utf8"));
214503
- const d = meta?.defaultSize;
214504
- const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
214505
- return {
214506
- screens: meta?.screens ?? {},
214507
- defaultSize: valid ? [Math.round(d[0]), Math.round(d[1])] : PHONE_SIZE
214508
- };
214509
- } catch {
214510
- return { screens: {}, defaultSize: PHONE_SIZE };
214511
- }
214512
214920
  }, writeMcpConfig = (ctx, port) => {
214513
- const path2 = join23(ctx.root, ".mcp.json");
214921
+ const path2 = join24(ctx.root, ".mcp.json");
214514
214922
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
214515
214923
  let config = {};
214516
214924
  if (existsSync23(path2)) {
214517
214925
  try {
214518
- config = JSON.parse(readFileSync19(path2, "utf8"));
214926
+ config = JSON.parse(readFileSync20(path2, "utf8"));
214519
214927
  } catch {
214520
214928
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
214521
214929
  return;
@@ -214537,15 +214945,15 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214537
214945
  ["spec.md", SPEC_MD],
214538
214946
  ["README.md", README_HINT],
214539
214947
  ["CLAUDE.md", CLAUDE_MD],
214540
- [join23("shared", "tokens.ts"), TOKENS_TS],
214541
- [join23("shared", "tabs.ts"), TABS_TS],
214542
- [join23("screens", "home.ts"), HOME_SCREEN_TS]
214948
+ [join24("shared", "tokens.ts"), TOKENS_TS],
214949
+ [join24("shared", "tabs.ts"), TABS_TS],
214950
+ [join24("screens", "home.ts"), HOME_SCREEN_TS]
214543
214951
  ];
214544
- mkdirSync17(join23(ctx.designDir, "screens"), { recursive: true });
214545
- mkdirSync17(join23(ctx.designDir, "shared"), { recursive: true });
214952
+ mkdirSync17(join24(ctx.designDir, "screens"), { recursive: true });
214953
+ mkdirSync17(join24(ctx.designDir, "shared"), { recursive: true });
214546
214954
  let created = 0;
214547
214955
  for (const [rel, content] of files) {
214548
- const abs = join23(ctx.designDir, rel);
214956
+ const abs = join24(ctx.designDir, rel);
214549
214957
  if (existsSync23(abs)) {
214550
214958
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214551
214959
  continue;
@@ -214554,9 +214962,9 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214554
214962
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
214555
214963
  created++;
214556
214964
  }
214557
- const ignorePath = join23(ctx.root, ".lecodesignore");
214965
+ const ignorePath = join24(ctx.root, ".lecodesignore");
214558
214966
  const ignoreLines = [`${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/`];
214559
- const current = existsSync23(ignorePath) ? readFileSync19(ignorePath, "utf8") : "";
214967
+ const current = existsSync23(ignorePath) ? readFileSync20(ignorePath, "utf8") : "";
214560
214968
  const present = new Set(current.split(/\r?\n/));
214561
214969
  const missing = ignoreLines.filter((line) => !present.has(line));
214562
214970
  if (missing.length) {
@@ -214571,8 +214979,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214571
214979
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
214572
214980
  const wroteTypes = materializeTypesLocal(ctx.designDir);
214573
214981
  writeTsconfig(ctx.designDir);
214574
- mkdirSync17(join23(ctx.designDir, ".lecodes", "types"), { recursive: true });
214575
- writeFileSync18(join23(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214982
+ mkdirSync17(join24(ctx.designDir, ".lecodes", "types"), { recursive: true });
214983
+ writeFileSync18(join24(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
214576
214984
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
214577
214985
  if (!wroteTypes)
214578
214986
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -214620,7 +215028,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214620
215028
  killer.unref?.();
214621
215029
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
214622
215030
  const frames = readMetaFrames(ctx.designDir);
214623
- const outDir = flagStr(args, "out-dir") ?? join23(ctx.designDir, ".snapshots");
215031
+ const outDir = flagStr(args, "out-dir") ?? join24(ctx.designDir, ".snapshots");
214624
215032
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
214625
215033
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
214626
215034
  if (!single)
@@ -214629,7 +215037,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214629
215037
  const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
214630
215038
  let plan = [{ state: undefined, bases: [id] }];
214631
215039
  if (discover) {
214632
- const { states, canonical } = discover(readFileSync19(join23(ctx.designDir, "screens", `${id}.ts`), "utf8"));
215040
+ const { states, canonical } = discover(readFileSync20(join24(ctx.designDir, "screens", `${id}.ts`), "utf8"));
214633
215041
  if (stateFlag !== undefined) {
214634
215042
  if (!states.includes(stateFlag)) {
214635
215043
  if (requested.length === 1)
@@ -214670,11 +215078,11 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
214670
215078
  for (const w of pngResult.warnings)
214671
215079
  warnErr(`${tag}: ${w}`);
214672
215080
  for (const base of bases) {
214673
- writeFileSync18(join23(outDir, `${base}.json`), result.text);
214674
- log(`${c.green("✓")} ${tag} → ${join23(outDir, `${base}.json`)}`);
215081
+ writeFileSync18(join24(outDir, `${base}.json`), result.text);
215082
+ log(`${c.green("✓")} ${tag} → ${join24(outDir, `${base}.json`)}`);
214675
215083
  if (pngResult) {
214676
- writeFileSync18(join23(outDir, `${base}.png`), pngResult.png);
214677
- log(`${c.green("✓")} ${tag} → ${join23(outDir, `${base}.png`)}`);
215084
+ writeFileSync18(join24(outDir, `${base}.png`), pngResult.png);
215085
+ log(`${c.green("✓")} ${tag} → ${join24(outDir, `${base}.png`)}`);
214678
215086
  }
214679
215087
  }
214680
215088
  }
@@ -215096,7 +215504,7 @@ var init_design = __esm(() => {
215096
215504
  init_config();
215097
215505
  init_api();
215098
215506
  init_types4();
215099
- PHONE_SIZE = [390, 844];
215507
+ init_designMeta();
215100
215508
  });
215101
215509
 
215102
215510
  // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
@@ -216118,9 +216526,9 @@ var require_main = __commonJS((exports, module) => {
216118
216526
  // package.json
216119
216527
  var package_default = {
216120
216528
  name: "lecodes-cli",
216121
- version: "0.18.0",
216529
+ version: "0.18.1",
216122
216530
  dependencies: {
216123
- "@letary/chisel": "^0.8.0",
216531
+ "@letary/chisel": "^0.10.0",
216124
216532
  jimp: "^1.6.1"
216125
216533
  },
216126
216534
  devDependencies: {
@@ -216134,8 +216542,8 @@ var package_default = {
216134
216542
  },
216135
216543
  peerDependencies: {
216136
216544
  "lecodes-design": "^0.8.1",
216137
- "lecodes-renderer": "^0.8.8",
216138
- "lecodes-3d-editor": "^0.4.0",
216545
+ "lecodes-renderer": "^0.8.9",
216546
+ "lecodes-3d-editor": "^0.5.0",
216139
216547
  "lecodes-assets": "^0.1.0"
216140
216548
  },
216141
216549
  bin: {
@@ -216486,15 +216894,27 @@ var matcIdentity = (exe) => {
216486
216894
  const stat = statSync2(exe);
216487
216895
  return `${stat.size}:${Math.trunc(stat.mtimeMs)}`;
216488
216896
  };
216489
- var shaderHash = (text, matcId) => createHash4("sha256").update(matcId + "\x00" + text).digest("hex").slice(0, 16);
216897
+ var shaderHash = (text, matcId) => createHash4("sha256").update(`${TARGET_MATRIX_GENERATION}\x00${matcId}\x00${text}`).digest("hex").slice(0, 16);
216490
216898
  var artifactFamily = (text) => {
216491
- const matcPlatforms = matcPlatformsFor(text);
216899
+ const matcPlatforms = [...matcPlatformsFor(text), ...LOCAL_ONLY_PLATFORMS];
216492
216900
  return [...matcPlatforms, ...matcPlatforms.includes("webgl") ? [] : ["webgl"], "webgl-lite"];
216493
216901
  };
216494
216902
  var shaderIsCompiled = (root, text, matcId) => {
216495
216903
  const hash = shaderHash(text, matcId);
216496
216904
  return artifactFamily(text).every((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216497
216905
  };
216906
+ var shaderCompiledPlatforms = (root, text, matcId) => {
216907
+ const hash = shaderHash(text, matcId);
216908
+ return artifactFamily(text).filter((p) => existsSync11(join13(shadersCacheDir(root), `${hash}_${p}.filamat`)));
216909
+ };
216910
+ var localShaderArtifact = (root, text, platform) => {
216911
+ const matcExe = resolveMatcExeCached();
216912
+ if (!matcExe)
216913
+ return null;
216914
+ const file = join13(shadersCacheDir(root), `${shaderHash(text, matcIdentity(matcExe))}_${platform}.filamat`);
216915
+ return existsSync11(file) ? file : null;
216916
+ };
216917
+ var allShaderPlatforms = () => [...Object.keys(ALL_PLATFORM_BUILD), "webgl-lite"];
216498
216918
  var runMatc = (exe, args) => new Promise((resolve2) => {
216499
216919
  execFile(exe, args, { windowsHide: true }, (error, _stdout, stderr) => {
216500
216920
  const text = String(stderr ?? "") || (error ? error.message : "");
@@ -216509,6 +216929,8 @@ var cleanMatcOutput = (err, tempName, realName) => {
216509
216929
  return (cut >= 0 ? output.slice(0, cut) : output).trim();
216510
216930
  };
216511
216931
  var NATIVE_PLATFORMS = ["opengl", "metal", "desktop"];
216932
+ var ALL_NATIVE_PLATFORMS = ["opengl", "metal", "desktop", "desktop-vulkan"];
216933
+ var desktopShaderPlatform = (platform, renderer) => platform === "win32" ? renderer === "vulkan" ? "desktop-vulkan" : "desktop" : "opengl";
216512
216934
  var applyLocalShaders = async (entries, opts) => {
216513
216935
  const shaderEntries = entries.filter((e) => e.type === "shader" && e.absPath && (opts.local || !e.shaders?.length));
216514
216936
  if (shaderEntries.length === 0)
@@ -216528,14 +216950,21 @@ var applyLocalShaders = async (entries, opts) => {
216528
216950
  for (const entry of shaderEntries) {
216529
216951
  const text = readFileSync10(entry.absPath, "utf8");
216530
216952
  const hash = shaderHash(text, matcId);
216531
- const matcPlatforms = matcPlatformsFor(text);
216953
+ const family = artifactFamily(text);
216532
216954
  const artifact = (platform) => `${hash}_${platform}.filamat`;
216533
- if (!artifactFamily(text).every((p) => existsSync11(join13(cacheDir, artifact(p))))) {
216955
+ const has = (platform) => existsSync11(join13(cacheDir, artifact(platform)));
216956
+ const wanted = opts.platforms ? family.filter((p) => opts.platforms.includes(p)) : family;
216957
+ if (wanted.includes("webgl") && !matcPlatformsFor(text).includes("webgl") && !wanted.includes("opengl")) {
216958
+ wanted.push("opengl");
216959
+ }
216960
+ const missing = wanted.filter((p) => !has(p));
216961
+ const matcMissing = missing.filter((p) => p !== "webgl-lite" && !(p === "webgl" && !matcPlatformsFor(text).includes("webgl")));
216962
+ if (matcMissing.length > 0) {
216534
216963
  const tempName = `lecodes-matc-${randomBytes(6).toString("hex")}.mat`;
216535
216964
  const tempFile = join13(tmpdir(), tempName);
216536
216965
  try {
216537
- for (const platform of matcPlatforms) {
216538
- const build = PLATFORM_BUILD[platform];
216966
+ for (const platform of matcMissing) {
216967
+ const build = ALL_PLATFORM_BUILD[platform];
216539
216968
  writeFileSync7(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text);
216540
216969
  const out = join13(cacheDir, artifact(platform));
216541
216970
  const { ok, stderr } = await runMatc(matcExe, matcArgs(platform, out, tempFile));
@@ -216548,15 +216977,21 @@ ${cleanMatcOutput(stderr, tempName, basename3(entry.path))}`);
216548
216977
  } finally {
216549
216978
  rmSync2(tempFile, { force: true });
216550
216979
  }
216551
- if (!matcPlatforms.includes("webgl")) {
216552
- copyFileSync(join13(cacheDir, artifact("opengl")), join13(cacheDir, artifact("webgl")));
216553
- }
216980
+ }
216981
+ if (missing.includes("webgl") && !matcPlatformsFor(text).includes("webgl")) {
216982
+ copyFileSync(join13(cacheDir, artifact("opengl")), join13(cacheDir, artifact("webgl")));
216983
+ }
216984
+ if (missing.includes("webgl-lite")) {
216554
216985
  writeFileSync7(join13(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)));
216555
216986
  }
216556
216987
  if (opts.local) {
216557
- entry.shaders = NATIVE_PLATFORMS.map((platform) => ({ platform, src: artifact(platform) }));
216988
+ const staged = opts.platforms ? ALL_NATIVE_PLATFORMS.filter((p) => opts.platforms.includes(p)) : NATIVE_PLATFORMS;
216989
+ entry.shaders = [
216990
+ { platform: "opengl", src: artifact("opengl") },
216991
+ ...staged.filter((p) => p !== "opengl").map((platform) => ({ platform, src: artifact(platform) }))
216992
+ ];
216558
216993
  entry.shadersLocal = true;
216559
- for (const platform of NATIVE_PLATFORMS) {
216994
+ for (const platform of staged) {
216560
216995
  artifacts.push({ name: artifact(platform), absPath: join13(cacheDir, artifact(platform)) });
216561
216996
  }
216562
216997
  } else {
@@ -216587,6 +217022,17 @@ var FIRST_PARTY_PLUGINS = {
216587
217022
  };
216588
217023
  var readAppConfigAt = (root) => readJson(join15(root, "app.json"));
216589
217024
  var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
217025
+ var resolveDesktopRenderer = (root, args) => {
217026
+ const raw = (args ? flagStr(args, "renderer") : undefined) ?? process.env.LECODES_DESKTOP_RENDERER ?? readAppConfigAt(root)?.desktop?.renderer;
217027
+ if (raw === undefined)
217028
+ return "gl";
217029
+ const value = String(raw).toLowerCase();
217030
+ if (value === "gl" || value === "opengl")
217031
+ return "gl";
217032
+ if (value === "vulkan" || value === "vk")
217033
+ return "vulkan";
217034
+ throw new CliError(`Unknown desktop renderer "${raw}". Use "gl" or "vulkan".`);
217035
+ };
216590
217036
  var desktopDefaultViewport = (root) => {
216591
217037
  const cfg = readAppConfigAt(root);
216592
217038
  if (!cfg?.desktop || cfg.ios || cfg.android)
@@ -216654,6 +217100,14 @@ var detectEngines = (bundleJs) => {
216654
217100
  `);
216655
217101
  return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
216656
217102
  };
217103
+ var assertRendererSupportsBundle = (renderer, bundleJs) => {
217104
+ if (renderer !== "vulkan")
217105
+ return;
217106
+ if (!detectEngines(bundleJs).g2)
217107
+ return;
217108
+ throw new CliError("This project uses the 2D engine, which the Vulkan desktop host doesn't include " + `(creator-2d renders through OpenGL and has no Vulkan path yet).
217109
+ ` + " Build with the default renderer: drop `desktop.renderer` from app.json, or pass `--renderer gl`.");
217110
+ };
216657
217111
  var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
216658
217112
  const uses = detectEngines(bundleJs);
216659
217113
  const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
@@ -216866,7 +217320,8 @@ var buildBundle = async (args, shell, opts = {}) => {
216866
217320
  }
216867
217321
  let stagedShaders = [];
216868
217322
  if (localAssets && (opts.localShaders ?? !shell)) {
216869
- const local = await applyLocalShaders(entries, { root, local: true });
217323
+ const platforms = opts.desktopRenderer ? [desktopShaderPlatform(process.platform, opts.desktopRenderer)] : undefined;
217324
+ const local = await applyLocalShaders(entries, { root, local: true, platforms });
216870
217325
  warnings.push(...local.warnings);
216871
217326
  stagedShaders = local.artifacts;
216872
217327
  }
@@ -216878,9 +217333,11 @@ var buildBundle = async (args, shell, opts = {}) => {
216878
217333
  publicUrl: shell ? "" : publicUrl,
216879
217334
  makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURI(path), absPath })
216880
217335
  });
217336
+ if (opts.extraEntries)
217337
+ entries.push(...opts.extraEntries);
216881
217338
  const iconsHeader = await applyAppAssetIcons(entries);
216882
217339
  note("Compiling…");
216883
- const entryFlag = flagStr(args, "entry");
217340
+ const entryFlag = opts.entryOverride ?? flagStr(args, "entry");
216884
217341
  if (!entryFlag) {
216885
217342
  const ambiguous = ambiguousEntryWarning(entries);
216886
217343
  if (ambiguous)
@@ -219373,9 +219830,13 @@ var makeStackDemangler = (bundleJs) => {
219373
219830
 
219374
219831
  // src/desktopRenderer.ts
219375
219832
  var DESKTOP_RENDERER_VERSION = "1.1.0";
219376
- var platformKey2 = () => {
219833
+ var DESKTOP_RENDERER_MIN_VERSION = { gl: "0.0.0", vulkan: "1.4.0" };
219834
+ var platformKey2 = (renderer = "gl") => {
219377
219835
  if (process.platform === "win32")
219378
- return "windows-x64";
219836
+ return renderer === "vulkan" ? "windows-x64-vulkan" : "windows-x64";
219837
+ if (renderer === "vulkan") {
219838
+ throw new CliError(`The Vulkan desktop host is Windows-only (this is ${process.platform}). Use the default renderer.`);
219839
+ }
219379
219840
  if (process.platform === "linux")
219380
219841
  return "ubuntu-linux-x64";
219381
219842
  throw new CliError(process.platform === "darwin" ? "`lecodes render --desktop` isn't available on macOS yet (the macOS host ships separately)." : `\`lecodes render --desktop\` isn't available on ${process.platform}.`);
@@ -219413,11 +219874,15 @@ var selectedDesktopVersion = () => {
219413
219874
  return DESKTOP_RENDERER_VERSION;
219414
219875
  };
219415
219876
  var fetchDesktopManifest = () => fetchReleaseProduct("desktop", "desktop");
219416
- var downloadDesktop = async (version, dest) => {
219417
- const key = platformKey2();
219877
+ var downloadDesktop = async (version, dest, renderer = "gl") => {
219878
+ const key = platformKey2(renderer);
219418
219879
  const entry = (await fetchDesktopManifest()).versions.find((v) => v.version === version);
219419
219880
  const file = entry?.files[key];
219420
219881
  if (!file) {
219882
+ if (renderer === "vulkan") {
219883
+ throw new CliError(`The Vulkan desktop host isn't published for LeCodes Desktop ${version} [${key}].
219884
+ ` + ` Vulkan needs desktop >= ${DESKTOP_RENDERER_MIN_VERSION.vulkan}; run \`lecodes desktop update\`, ` + `set LECODES_DESKTOP_EXE to a locally built host, or drop desktop.renderer from app.json.`);
219885
+ }
219421
219886
  throw new CliError(`LeCodes Desktop ${version} [${key}] isn't published yet — \`--desktop\` needs it for the ` + `headless render contract.
219422
219887
  Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
219423
219888
  }
@@ -219428,18 +219893,20 @@ Set LECODES_DESKTOP_EXE to a locally built host, or update lecodes-cli.`);
219428
219893
  validate: (dir) => findExe(dir) ? null : "The downloaded archive doesn't contain the desktop host binary."
219429
219894
  });
219430
219895
  };
219431
- var resolveDesktopExe = async () => {
219896
+ var resolveDesktopExe = async (renderer = "gl") => {
219432
219897
  const override = process.env.LECODES_DESKTOP_EXE;
219433
219898
  if (override) {
219434
219899
  if (!existsSync17(override))
219435
219900
  throw new CliError(`LECODES_DESKTOP_EXE points at "${override}" which doesn't exist.`);
219436
219901
  return resolve5(override);
219437
219902
  }
219903
+ if (renderer === "vulkan")
219904
+ platformKey2(renderer);
219438
219905
  const version = selectedDesktopVersion();
219439
- const dir = join18(cacheRoot2(), `desktop-${version}`);
219906
+ const dir = join18(cacheRoot2(), `desktop-${version}${renderer === "vulkan" ? "-vulkan" : ""}`);
219440
219907
  let exe = existsSync17(dir) ? findExe(dir) : null;
219441
219908
  if (!exe) {
219442
- await downloadDesktop(version, dir);
219909
+ await downloadDesktop(version, dir, renderer);
219443
219910
  exe = findExe(dir);
219444
219911
  if (!exe)
219445
219912
  throw new CliError(`Renderer cache at ${dir} is broken — delete the folder and retry.`);
@@ -219488,6 +219955,8 @@ var windowEnv = (opts) => {
219488
219955
  if (opts.maxRenderSize && opts.maxRenderSize[0] > 0 && opts.maxRenderSize[1] > 0) {
219489
219956
  env.CREATOR_RENDER_MAX = `${Math.round(opts.maxRenderSize[0])}x${Math.round(opts.maxRenderSize[1])}`;
219490
219957
  }
219958
+ if (opts.fps)
219959
+ env.CREATOR_FPS = "1";
219491
219960
  return env;
219492
219961
  };
219493
219962
  var stageBundle = (js, resources) => {
@@ -219517,7 +219986,7 @@ var pipeThroughDemangler = (src, dst, map) => {
219517
219986
  });
219518
219987
  };
219519
219988
  var runDesktopApp = async (js, resources, opts = {}) => {
219520
- const exe = await resolveDesktopExe();
219989
+ const exe = await resolveDesktopExe(opts.renderer);
219521
219990
  const runDir = stageBundle(js, resources);
219522
219991
  try {
219523
219992
  const env = { ...process.env, ...windowEnv(opts) };
@@ -219541,7 +220010,7 @@ var runDesktopApp = async (js, resources, opts = {}) => {
219541
220010
  }
219542
220011
  };
219543
220012
  var runDesktopDev = async (url, opts = {}) => {
219544
- const exe = await resolveDesktopExe();
220013
+ const exe = await resolveDesktopExe(opts.renderer);
219545
220014
  const env = { ...process.env, ...windowEnv(opts) };
219546
220015
  await new Promise((done, fail) => {
219547
220016
  const child = spawn2(exe, [url], { env, stdio: ["ignore", "ignore", "inherit"] });
@@ -219550,7 +220019,7 @@ var runDesktopDev = async (url, opts = {}) => {
219550
220019
  });
219551
220020
  };
219552
220021
  var runDesktopRender = async (js, resources, opts) => {
219553
- const exe = await resolveDesktopExe();
220022
+ const exe = await resolveDesktopExe(opts.renderer);
219554
220023
  const runDir = stageBundle(js, resources);
219555
220024
  try {
219556
220025
  const appJs = join18(runDir, "app.js");
@@ -219577,11 +220046,16 @@ var runDesktopRender = async (js, resources, opts) => {
219577
220046
  return await new Promise((done, fail) => {
219578
220047
  const child = spawn2(exe, [appJs], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] });
219579
220048
  const chunks = [];
220049
+ let stopped = false;
219580
220050
  const sink = (d) => {
219581
220051
  const text = String(d);
219582
220052
  chunks.push(text);
219583
220053
  if (opts.logs)
219584
220054
  process.stderr.write(demangle(text));
220055
+ if (!stopped && opts.stopOnLine?.(text)) {
220056
+ stopped = true;
220057
+ child.kill();
220058
+ }
219585
220059
  };
219586
220060
  child.stdout.on("data", sink);
219587
220061
  child.stderr.on("data", sink);
@@ -219596,6 +220070,8 @@ var runDesktopRender = async (js, resources, opts) => {
219596
220070
  child.on("exit", (code) => {
219597
220071
  clearTimeout(killer);
219598
220072
  const output = demangle(chunks.join(""));
220073
+ if (stopped)
220074
+ return done({ output, exitCode: 0 });
219599
220075
  if (code === 0 && existsSync17(outAbs))
219600
220076
  return done({ output, exitCode: code });
219601
220077
  const tail = demangle(chunks.slice(-80).join("")).trimEnd();
@@ -219634,6 +220110,8 @@ var initDesktop = async (args) => {
219634
220110
  log(`Next: ${c.bold("lecodes dev --desktop")} to develop, ${c.bold("lecodes app build desktop")} to package.`);
219635
220111
  };
219636
220112
  var cleanFileName = (name) => name.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "").replace(/[. ]+$/, "").trim() || "app";
220113
+ var ASSETS_DIR = "assets";
220114
+ var RUNTIME_DIR = "runtime";
219637
220115
  var buildDesktop = async (args) => {
219638
220116
  const root = projectRoot();
219639
220117
  const config = readJson(join19(root, "app.json")) ?? {};
@@ -219649,8 +220127,10 @@ var buildDesktop = async (args) => {
219649
220127
  }
219650
220128
  if (config.desktop?.entry && flagStr(args, "entry") === undefined)
219651
220129
  args.flags.entry = config.desktop.entry;
219652
- note(`Building "${name}" for desktop…`);
219653
- const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] }, { localShaders: true });
220130
+ const renderer = resolveDesktopRenderer(root, args);
220131
+ note(`Building "${name}" for desktop${renderer === "vulkan" ? " (Vulkan)" : ""}…`);
220132
+ const { js, resources } = await buildBundle(args, { remotePatterns: config.resources?.remote ?? [] }, { localShaders: true, desktopRenderer: renderer });
220133
+ assertRendererSupportsBundle(renderer, js);
219654
220134
  const wanted = new Set([...js.matchAll(/fetchLocal\("([^"]+)"\)/g)].map((m) => m[1]));
219655
220135
  const shipped = resources.filter((r2) => !r2.remote && (r2.shader || wanted.has(r2.name)));
219656
220136
  const byName = new Map;
@@ -219669,7 +220149,7 @@ var buildDesktop = async (args) => {
219669
220149
  `)).join(`
219670
220150
  `));
219671
220151
  }
219672
- const exe = await resolveDesktopExe();
220152
+ const exe = await resolveDesktopExe(renderer);
219673
220153
  const hostDir = dirname11(exe);
219674
220154
  const IMPLICIT_PRELOADS = new Set(["uberarchive", "capture"]);
219675
220155
  const hostProvided = new Set;
@@ -219686,11 +220166,15 @@ var buildDesktop = async (args) => {
219686
220166
  rmSync5(out, { recursive: true, force: true });
219687
220167
  mkdirSync12(out, { recursive: true });
219688
220168
  for (const entry of readdirSync7(hostDir, { withFileTypes: true })) {
219689
- const keep = entry.isFile() && entry.name.toLowerCase().endsWith(".dll") || entry.isDirectory() && (entry.name === "assets" || entry.name === "fonts");
219690
- if (keep)
220169
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".dll")) {
220170
+ cpSync2(join19(hostDir, entry.name), join19(out, entry.name));
220171
+ } else if (entry.isDirectory() && entry.name === "assets") {
220172
+ cpSync2(join19(hostDir, entry.name), join19(out, RUNTIME_DIR), { recursive: true });
220173
+ } else if (entry.isDirectory() && entry.name === "fonts") {
219691
220174
  cpSync2(join19(hostDir, entry.name), join19(out, entry.name), { recursive: true });
220175
+ }
219692
220176
  }
219693
- if (!existsSync18(join19(out, "assets", "initial.js"))) {
220177
+ if (!existsSync18(join19(out, RUNTIME_DIR, "initial.js"))) {
219694
220178
  throw new CliError(`The host at ${hostDir} has no assets/initial.js next to it — a staged build can't boot. ` + `(LECODES_DESKTOP_EXE must point at an exe with its runtime assets/ beside it.)`);
219695
220179
  }
219696
220180
  const exeName2 = process.platform === "win32" ? `${cleanFileName(name)}.exe` : cleanFileName(name);
@@ -219698,16 +220182,18 @@ var buildDesktop = async (args) => {
219698
220182
  note(` host: LeCodes Desktop ${version}`);
219699
220183
  writeFileSync14(join19(out, "app.js"), js);
219700
220184
  let assetBytes = 0, assetCount = 0;
220185
+ if (shipped.length > 0)
220186
+ mkdirSync12(join19(out, ASSETS_DIR), { recursive: true });
219701
220187
  for (const r2 of shipped) {
219702
220188
  if (!r2.absPath || !existsSync18(r2.absPath) || !statSync5(r2.absPath).isFile()) {
219703
220189
  warn(`asset "${r2.name}" (${r2.path}) has no local file — skipped`);
219704
220190
  continue;
219705
220191
  }
219706
- cpSync2(r2.absPath, join19(out, r2.name));
220192
+ cpSync2(r2.absPath, join19(out, ASSETS_DIR, r2.name));
219707
220193
  assetBytes += statSync5(r2.absPath).size;
219708
220194
  assetCount++;
219709
220195
  }
219710
- note(` assets: ${assetCount} files, ${mb(assetBytes)}`);
220196
+ note(` assets: ${assetCount} files, ${mb(assetBytes)} → ${ASSETS_DIR}/`);
219711
220197
  writeJson(join19(out, "app.json"), { name, ...config.desktop ? { desktop: { window: config.desktop.window } } : {} });
219712
220198
  if (config.icon) {
219713
220199
  await writeIco(root, config, join19(out, "app.ico"));
@@ -219818,7 +220304,7 @@ var readmeText = (name, exeName2, version) => `${name}
219818
220304
  ${"=".repeat(Math.max(name.length, 4))}
219819
220305
 
219820
220306
  A LeCodes desktop app. Nothing to install: unpack the WHOLE folder anywhere and run ${exeName2}.
219821
- Keep the files together — the app reads its assets from this folder.
220307
+ Keep the files together: assets/ holds the app's own content, runtime/ the engine it needs.
219822
220308
 
219823
220309
  On first run Windows may show a SmartScreen prompt (any unsigned app gets one):
219824
220310
  choose "More info", then "Run anyway".
@@ -220654,6 +221140,9 @@ from \`main.ts\`.
220654
221140
  lecodes render # semantic JSON of the launched UI (structure, text, names, values)
220655
221141
  lecodes render --png shot.png # pixels (needs @napi-rs/canvas installed)
220656
221142
  lecodes render --png card.png --clip card # ONE component/region: a node name or x,y,w,h
221143
+ lecodes render src/screens/cart.ts # ONE screen module, without booting the app: any file that
221144
+ # \`export default\`s a UIScreen (or a (state) => UIScreen —
221145
+ # --state <name> picks the branch). Same flags as above.
220657
221146
  lecodes render --desktop --png shot.png # NATIVE render: real Filament 3D + UI + 2D composited —
220658
221147
  # use this to SEE 3D scenes / procedural geometry (headless
220659
221148
  # renders record 3D as JSON only). Windows/Linux; downloads
@@ -220828,9 +221317,12 @@ var run = async (args) => {
220828
221317
  }
220829
221318
  const appCfg = readAppConfigAt(root);
220830
221319
  const win = appCfg?.desktop?.window;
220831
- const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
220832
- note("Launching LeCodes Desktop… (close the window to return)");
221320
+ const renderer = resolveDesktopRenderer(root, args);
221321
+ const { js, resources } = await buildBundle(args, undefined, { localAssets: true, desktopRenderer: renderer });
221322
+ assertRendererSupportsBundle(renderer, js);
221323
+ note(`Launching LeCodes Desktop${renderer === "vulkan" ? " (Vulkan)" : ""}… (close the window to return)`);
220833
221324
  await runDesktopApp(js, resources, {
221325
+ renderer,
220834
221326
  width: Number(flagStr(args, "width")) || win?.width,
220835
221327
  height: Number(flagStr(args, "height")) || win?.height,
220836
221328
  minWidth: win?.minWidth,
@@ -220840,7 +221332,8 @@ var run = async (args) => {
220840
221332
  fullscreen: flagBool(args, "fullscreen") ? true : flagBool(args, "windowed") ? false : win?.fullscreen,
220841
221333
  scale: resolveScale(flagStr(args, "scale"), win?.scale),
220842
221334
  renderScale: resolveRenderScale(flagStr(args, "render-scale"), appCfg?.desktop?.renderScale),
220843
- maxRenderSize: appCfg?.desktop?.maxRenderSize
221335
+ maxRenderSize: appCfg?.desktop?.maxRenderSize,
221336
+ fps: flagBool(args, "fps")
220844
221337
  });
220845
221338
  };
220846
221339
  var status = async () => {
@@ -220865,9 +221358,11 @@ var desktop = async (args) => {
220865
221358
  return status();
220866
221359
  if (sub === "run")
220867
221360
  return run(args);
221361
+ if (sub === "build")
221362
+ return buildDesktop(args);
220868
221363
  if (sub === "update")
220869
221364
  return updateDesktop();
220870
- throw new CliError(`Unknown subcommand "desktop ${sub}". Use \`lecodes desktop\` (status), \`lecodes desktop run\` or \`lecodes desktop update\`.`);
221365
+ throw new CliError(`Unknown subcommand "desktop ${sub}". Use \`lecodes desktop\` (status), \`lecodes desktop run\`, \`lecodes desktop build\` or \`lecodes desktop update\`.`);
220871
221366
  };
220872
221367
 
220873
221368
  // src/index.ts
@@ -220876,16 +221371,16 @@ init_design();
220876
221371
  // src/commands/dev.ts
220877
221372
  init_manifest();
220878
221373
  init_ignore();
220879
- import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync20, watch, writeFileSync as writeFileSync19 } from "node:fs";
221374
+ import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync21, watch, writeFileSync as writeFileSync19 } from "node:fs";
220880
221375
  import { randomBytes as randomBytes3 } from "node:crypto";
220881
221376
  import { networkInterfaces } from "node:os";
220882
- import { basename as basename10, join as join25 } from "node:path";
221377
+ import { basename as basename10, join as join26 } from "node:path";
220883
221378
 
220884
221379
  // src/dev/devServer.ts
220885
221380
  init_localFiles();
220886
221381
  import { createServer as createServer2 } from "node:http";
220887
221382
  import { createReadStream, statSync as statSync8 } from "node:fs";
220888
- import { join as join24 } from "node:path";
221383
+ import { join as join25 } from "node:path";
220889
221384
  init_projectCompile();
220890
221385
  init_fonts();
220891
221386
  init_assetIcons();
@@ -221271,7 +221766,7 @@ var startDevServer = async (options) => {
221271
221766
  const name2 = route.slice("/shaders/".length);
221272
221767
  if (!isShaderArtifactName(name2))
221273
221768
  return sendText(res, 400, "text/plain", "Bad path");
221274
- const abs = join24(shadersCacheDir(root), name2);
221769
+ const abs = join25(shadersCacheDir(root), name2);
221275
221770
  try {
221276
221771
  if (!statSync8(abs).isFile())
221277
221772
  throw new Error("not a file");
@@ -221385,14 +221880,14 @@ var lanCandidates = () => {
221385
221880
  return out.sort((x2, y2) => x2.rank - y2.rank).map(({ address, iface }) => ({ address, iface }));
221386
221881
  };
221387
221882
  var loadDevToken = (root) => {
221388
- const path2 = join25(root, LECODES_DIR, "dev.json");
221883
+ const path2 = join26(root, LECODES_DIR, "dev.json");
221389
221884
  try {
221390
- const token2 = JSON.parse(readFileSync20(path2, "utf8")).token;
221885
+ const token2 = JSON.parse(readFileSync21(path2, "utf8")).token;
221391
221886
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
221392
221887
  return token2;
221393
221888
  } catch {}
221394
221889
  const token = randomBytes3(6).toString("hex");
221395
- mkdirSync18(join25(root, LECODES_DIR), { recursive: true });
221890
+ mkdirSync18(join26(root, LECODES_DIR), { recursive: true });
221396
221891
  writeFileSync19(path2, JSON.stringify({ token }, null, 2) + `
221397
221892
  `);
221398
221893
  return token;
@@ -221510,7 +222005,7 @@ ${result.error}`);
221510
222005
  if (alternatives.length > 0) {
221511
222006
  note(`Device can't reach it? This machine also has ${alternatives.map((a2) => `${a2.address} (${a2.iface})`).join(", ")} — retry with --host <ip>.`);
221512
222007
  }
221513
- if (existsSync24(join25(root, LECODES_DIR, "manifest.json")))
222008
+ if (existsSync24(join26(root, LECODES_DIR, "manifest.json")))
221514
222009
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
221515
222010
  log("");
221516
222011
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
@@ -221525,7 +222020,8 @@ ${result.error}`);
221525
222020
  minHeight: win?.minHeight,
221526
222021
  resizable: win?.resizable,
221527
222022
  title: win?.title ?? appCfg?.name ?? name,
221528
- scale: resolveScale(undefined, win?.scale)
222023
+ scale: resolveScale(undefined, win?.scale),
222024
+ fps: flagBool(args, "fps")
221529
222025
  }).then(() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."), (e2) => warnErr(`Desktop host: ${e2 instanceof Error ? e2.message : String(e2)}`));
221530
222026
  }
221531
222027
  };
@@ -221538,7 +222034,7 @@ init_localFiles();
221538
222034
 
221539
222035
  // src/serverDiff.ts
221540
222036
  init_localFiles();
221541
- import { readFileSync as readFileSync21 } from "node:fs";
222037
+ import { readFileSync as readFileSync22 } from "node:fs";
221542
222038
  import { basename as basename11 } from "node:path";
221543
222039
  var isTextAsset = (t2) => t2 === "text" || t2 === "shader" || t2 === "scene";
221544
222040
  var computeServerDiff = (project, scan2) => {
@@ -221557,7 +222053,7 @@ var computeServerDiff = (project, scan2) => {
221557
222053
  const local = localByPath.get(path2);
221558
222054
  if (local && !server2) {
221559
222055
  const text = isTextKind(classifyKind(basename11(path2)));
221560
- entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync21(local.absPath, "utf8") : undefined });
222056
+ entries.push({ path: path2, status: "added", binary: !text, serverText: "", localText: text ? readFileSync22(local.absPath, "utf8") : undefined });
221561
222057
  continue;
221562
222058
  }
221563
222059
  if (server2 && !local) {
@@ -221568,12 +222064,12 @@ var computeServerDiff = (project, scan2) => {
221568
222064
  if (server2 && local) {
221569
222065
  if (isTextAsset(server2.type)) {
221570
222066
  const serverText = server2.text ?? "";
221571
- const localText = readFileSync21(local.absPath, "utf8");
222067
+ const localText = readFileSync22(local.absPath, "utf8");
221572
222068
  if (serverText !== localText)
221573
222069
  entries.push({ path: path2, status: "modified", binary: false, serverText, localText });
221574
222070
  } else {
221575
222071
  const serverSize = server2.file?.size;
221576
- const localSize = Buffer.byteLength(readFileSync21(local.absPath));
222072
+ const localSize = Buffer.byteLength(readFileSync22(local.absPath));
221577
222073
  if (serverSize !== undefined && serverSize !== localSize) {
221578
222074
  entries.push({ path: path2, status: "modified", binary: true });
221579
222075
  }
@@ -221732,7 +222228,7 @@ init_manifest();
221732
222228
  init_types4();
221733
222229
  init_ignore();
221734
222230
  import { existsSync as existsSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync20 } from "node:fs";
221735
- import { basename as basename12, dirname as dirname14, join as join26, resolve as resolve10 } from "node:path";
222231
+ import { basename as basename12, dirname as dirname14, join as join27, resolve as resolve10 } from "node:path";
221736
222232
  init_util();
221737
222233
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
221738
222234
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
@@ -221845,7 +222341,7 @@ var init2 = async (args) => {
221845
222341
  mkdirSync19(root, { recursive: true });
221846
222342
  let created = 0;
221847
222343
  const write = (rel, content) => {
221848
- const abs = join26(root, rel);
222344
+ const abs = join27(root, rel);
221849
222345
  if (existsSync25(abs)) {
221850
222346
  note(`kept ${rel}`);
221851
222347
  return;
@@ -221899,7 +222395,7 @@ init_ignore();
221899
222395
  init_localFiles();
221900
222396
  init_manifest();
221901
222397
  import { existsSync as existsSync26 } from "node:fs";
221902
- import { basename as basename14, join as join27, resolve as resolve11 } from "node:path";
222398
+ import { basename as basename14, join as join28, resolve as resolve11 } from "node:path";
221903
222399
  init_types4();
221904
222400
  init_util();
221905
222401
 
@@ -221909,7 +222405,7 @@ init_config();
221909
222405
  init_manifest();
221910
222406
  init_localFiles();
221911
222407
  import { basename as basename13 } from "node:path";
221912
- import { readFileSync as readFileSync22 } from "node:fs";
222408
+ import { readFileSync as readFileSync23 } from "node:fs";
221913
222409
  init_util();
221914
222410
  var push = async (args) => {
221915
222411
  const root = findProjectRoot(process.cwd());
@@ -221970,9 +222466,9 @@ var push = async (args) => {
221970
222466
  if (source)
221971
222467
  entry.assetId = source.id;
221972
222468
  if (isTextKind(kind)) {
221973
- entry.text = readFileSync22(file.absPath, "utf8");
222469
+ entry.text = readFileSync23(file.absPath, "utf8");
221974
222470
  } else if (!source || source.sha !== file.sha) {
221975
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename13(file.path), readFileSync22(file.absPath));
222471
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename13(file.path), readFileSync23(file.absPath));
221976
222472
  entry.blobFileId = blob.id;
221977
222473
  uploaded++;
221978
222474
  }
@@ -222082,7 +222578,7 @@ var link = async (args) => {
222082
222578
  const root = dirArg ? resolve11(dirArg) : process.cwd();
222083
222579
  if (!existsSync26(root))
222084
222580
  throw new CliError(`No such directory: ${root}`);
222085
- if (existsSync26(join27(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
222581
+ if (existsSync26(join28(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
222086
222582
  throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.");
222087
222583
  }
222088
222584
  let enclosing = null;
@@ -222144,6 +222640,173 @@ var link = async (args) => {
222144
222640
  log(` ${c.dim("(add --publish to deploy it live in the same step)")}`);
222145
222641
  };
222146
222642
 
222643
+ // src/commands/lightmap.ts
222644
+ init_manifest();
222645
+ import { existsSync as existsSync27, mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync3, rmSync as rmSync8, statSync as statSync9, unlinkSync, writeFileSync as writeFileSync21 } from "node:fs";
222646
+ import { spawnSync as spawnSync4 } from "node:child_process";
222647
+ import { tmpdir as tmpdir3 } from "node:os";
222648
+ import { join as join29, relative as relative4, resolve as resolve12 } from "node:path";
222649
+ init_util();
222650
+ var num = (args, key, fallback, min, max) => {
222651
+ const raw = flagStr(args, key);
222652
+ if (raw === undefined)
222653
+ return fallback;
222654
+ const n2 = Number(raw);
222655
+ if (!Number.isFinite(n2) || n2 < min || n2 > max)
222656
+ throw new CliError(`--${key} wants a number in ${min}..${max}, got "${raw}".`);
222657
+ return n2;
222658
+ };
222659
+ var findToktx = () => {
222660
+ const env = process.env.KTX_TOKTX;
222661
+ if (env && existsSync27(env))
222662
+ return env;
222663
+ for (const base of [process.env.LOCALAPPDATA && join29(process.env.LOCALAPPDATA, "Programs"), process.env.ProgramFiles]) {
222664
+ if (!base)
222665
+ continue;
222666
+ const guess = join29(base, "KTX-Software", "bin", "toktx.exe");
222667
+ if (existsSync27(guess))
222668
+ return guess;
222669
+ }
222670
+ return "toktx";
222671
+ };
222672
+ var checkToktx = (toktx) => {
222673
+ try {
222674
+ const p2 = spawnSync4(toktx, ["--version"], { encoding: "utf8" });
222675
+ if (p2.status === 0)
222676
+ return (p2.stdout ?? "").toString().trim() || "toktx";
222677
+ } catch {}
222678
+ return null;
222679
+ };
222680
+ var requireToktx = () => {
222681
+ const toktx = findToktx();
222682
+ if (!checkToktx(toktx)) {
222683
+ throw new CliError(`lecodes lightmap bake needs KTX-Software's toktx — the atlas ships as KTX2 (GPU-compressed, mipmapped).
222684
+ Install it from https://github.com/KhronosGroup/KTX-Software/releases (make sure toktx is on PATH) or set KTX_TOKTX=<path to toktx>.`);
222685
+ }
222686
+ return toktx;
222687
+ };
222688
+ var encodeKtx2 = (toktx, png2, ktx2) => {
222689
+ const args = ["--t2", "--encode", "uastc", "--uastc_quality", "2", "--zcmp", "18", "--genmipmap", "--assign_oetf", "srgb", ktx2, png2];
222690
+ const p2 = spawnSync4(toktx, args, { encoding: "utf8" });
222691
+ if (p2.status !== 0) {
222692
+ throw new CliError(`toktx failed (${p2.status ?? p2.error?.message ?? "?"}) while encoding ${ktx2}:
222693
+ ${(p2.stderr || p2.stdout || "").trim()}`);
222694
+ }
222695
+ };
222696
+ var bake = async (args) => {
222697
+ let root;
222698
+ try {
222699
+ root = findProjectRoot(process.cwd());
222700
+ } catch {
222701
+ root = process.cwd();
222702
+ }
222703
+ const toktx = requireToktx();
222704
+ const outDir = resolve12(root, flagStr(args, "out") ?? "assets/lightmap");
222705
+ const entryFlag = flagStr(args, "entry");
222706
+ const entryFile = entryFlag ?? "main.ts";
222707
+ if (!existsSync27(resolve12(root, entryFile))) {
222708
+ throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the scene and calls Lightmap.load).`);
222709
+ }
222710
+ const cfg = {
222711
+ outDir: outDir.replace(/\\/g, "/"),
222712
+ size: num(args, "size", 4096, 256, 16384),
222713
+ texel: num(args, "texel", 0.02, 0.001, 2),
222714
+ sunRays: num(args, "sun-rays", 32, 1, 1024),
222715
+ aoRays: num(args, "ao-rays", 64, 1, 4096),
222716
+ aoDistance: num(args, "ao-distance", 2, 0.01, 1000),
222717
+ bias: num(args, "bias", 0.02, 0.0001, 1),
222718
+ sunAngle: num(args, "sun-angle", 0.5, 0.01, 30)
222719
+ };
222720
+ const frames = num(args, "frames", 900, 30, 1e5);
222721
+ const win = readAppConfigAt(root)?.desktop?.window;
222722
+ const width = Number(flagStr(args, "width")) || win?.width || 1280;
222723
+ const height = Number(flagStr(args, "height")) || win?.height || 720;
222724
+ const logs = flagBool(args, "logs");
222725
+ const importPath = "./" + entryFile.replace(/\\/g, "/").replace(/\.tsx?$/, "");
222726
+ const entryPath = "/.lightmap-entry.ts";
222727
+ const entryText = `globalThis.__lecodesLightmap = ${JSON.stringify(cfg)}
222728
+ import ${JSON.stringify(importPath)}
222729
+ `;
222730
+ mkdirSync20(outDir, { recursive: true });
222731
+ ensurePlaceholders(outDir);
222732
+ const desktopRenderer = resolveDesktopRenderer(root, args);
222733
+ const { js, resources } = await buildBundle(args, undefined, {
222734
+ localAssets: true,
222735
+ desktopRenderer,
222736
+ extraEntries: [{ path: entryPath, type: "text", text: entryText }],
222737
+ entryOverride: entryPath
222738
+ });
222739
+ const scratch = mkdtempSync3(join29(tmpdir3(), "lecodes-lightmap-"));
222740
+ const outPng = join29(scratch, "frame.png");
222741
+ note(`Baking ${cfg.size}² at ${(cfg.texel * 100).toFixed(1)} cm texels (${cfg.sunRays} sun + ${cfg.aoRays} AO rays) → ${relative4(root, outDir) || "."}…`);
222742
+ let output = "";
222743
+ try {
222744
+ const timeoutMs = Number(flagStr(args, "timeout")) || 600000;
222745
+ const res = await runDesktopRender(js, resources, {
222746
+ renderer: desktopRenderer,
222747
+ width,
222748
+ height,
222749
+ frames,
222750
+ outPng,
222751
+ logs,
222752
+ timeoutMs,
222753
+ stopOnLine: (chunk) => chunk.includes("[lightmap] done") || chunk.includes("[lightmap] error")
222754
+ });
222755
+ output = res.output;
222756
+ } catch (e2) {
222757
+ const msg = e2 instanceof Error ? e2.message : String(e2);
222758
+ if (!/\[lightmap\] done/.test(msg))
222759
+ throw e2;
222760
+ output = msg;
222761
+ } finally {
222762
+ rmSync8(scratch, { recursive: true, force: true });
222763
+ }
222764
+ const lines = output.split(/\r?\n/).filter((l2) => l2.startsWith("[lightmap]"));
222765
+ for (const l2 of lines) {
222766
+ const text = l2.slice("[lightmap]".length).trim();
222767
+ if (text === "done")
222768
+ continue;
222769
+ logErr(text.startsWith("error") ? c.red(`✗ ${text}`) : c.dim(` ${text}`));
222770
+ }
222771
+ if (/\[lightmap\] done/.test(output)) {
222772
+ const png2 = join29(outDir, "lightmap.png");
222773
+ const ktx2 = join29(outDir, "lightmap.ktx2");
222774
+ if (!existsSync27(png2))
222775
+ throw new CliError(`The host reported a bake but ${png2} is missing.`);
222776
+ const t0 = Date.now();
222777
+ encodeKtx2(toktx, png2, ktx2);
222778
+ const pngKb = Math.round(statSync9(png2).size / 1024), ktxKb = Math.round(statSync9(ktx2).size / 1024);
222779
+ unlinkSync(png2);
222780
+ logErr(c.dim(` encoded lightmap.ktx2 (UASTC + mips, ${ktxKb} KB from a ${pngKb} KB raster) in ${((Date.now() - t0) / 1000).toFixed(1)} s`));
222781
+ logErr(c.green(`✓ baked → ${relative4(root, outDir) || "."}/lightmap.ktx2 + lightmap.bake`));
222782
+ logErr(c.dim(` apply it with Lightmap.load(scene, { data: asset('<rel>/${relative4(root, join29(outDir, "lightmap.bake")).replace(/\\/g, "/")}'), texture: asset('<rel>/${relative4(root, ktx2).replace(/\\/g, "/")}') }) — asset() paths are relative to the calling file`));
222783
+ return;
222784
+ }
222785
+ if (lines.some((l2) => /no bake module/.test(l2))) {
222786
+ throw new CliError("This desktop host has no bake module. Build packages/desktop (CREATOR_GL_LIGHTMAP is on there) and point LECODES_DESKTOP_EXE at build/lecodes-desktop.exe, or update: lecodes desktop update.");
222787
+ }
222788
+ if (lines.some((l2) => /error/.test(l2)))
222789
+ throw new CliError("The bake failed — see the lines above.");
222790
+ warnErr("The host ran to its frame cap without baking. Make sure the app calls Lightmap.load(...) after the static scene is built, and skips menus when Lightmap.baking is true (a title screen never reaches the scene). More load headroom: --frames <n>. Host output: --logs.");
222791
+ throw new CliError("Nothing was baked.");
222792
+ };
222793
+ var PLACEHOLDER_KTX2 = Buffer.from("q0tUWCAyMLsNChoKAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAaAAAACwAAACUAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAABAAAAAAAAAAEAAAAAAAAAAsAAAAAAAAAAIAKACmAQIAAwMAABAAAAAAAAAAAAB/AAAAAAAAAAAA/////xEAAABLVFhvcmllbnRhdGlvbgByAAAAACcAAABLVFh3cml0ZXIAdG9rdHggdjQuNC4yIC8gbGlia3R4IHY0LjQuMgAAMwAAAEtUWHdyaXRlclNjUGFyYW1zAC0tZW5jb2RlIHVhc3RjIC0tdWFzdGNfcXVhbGl0eSAwAAD3////H3zvAQAAAAAAAAAA", "base64");
222794
+ var ensurePlaceholders = (outDir) => {
222795
+ const bake2 = join29(outDir, "lightmap.bake");
222796
+ const ktx2 = join29(outDir, "lightmap.ktx2");
222797
+ if (!existsSync27(bake2))
222798
+ writeFileSync21(bake2, JSON.stringify({ version: 1, placeholder: true, size: 1, texel: 0, instances: [] }) + `
222799
+ `);
222800
+ if (!existsSync27(ktx2))
222801
+ writeFileSync21(ktx2, PLACEHOLDER_KTX2);
222802
+ };
222803
+ var lightmap = async (args) => {
222804
+ const sub = args._[0];
222805
+ if (sub === "bake")
222806
+ return bake({ ...args, _: args._.slice(1) });
222807
+ throw new CliError("Usage: lecodes lightmap bake [--out assets/lightmap] [--size 4096] [--texel 0.02] [--sun-rays 32] [--ao-rays 64] [--ao-distance 2] [--bias 0.02] [--sun-angle 0.5] [--frames 900] [--entry main.ts] [--logs] (needs KTX-Software's toktx)");
222808
+ };
222809
+
222147
222810
  // src/commands/login.ts
222148
222811
  init_api();
222149
222812
  init_config();
@@ -222161,8 +222824,9 @@ var login = async (args) => {
222161
222824
 
222162
222825
  // src/commands/shaders.ts
222163
222826
  init_manifest();
222164
- import { readFileSync as readFileSync23 } from "node:fs";
222827
+ import { readFileSync as readFileSync24 } from "node:fs";
222165
222828
  init_util();
222829
+ init_compile();
222166
222830
  var collectShaders = () => {
222167
222831
  let root;
222168
222832
  let manifest;
@@ -222188,8 +222852,27 @@ var status2 = async () => {
222188
222852
  log(`shaders ${c.dim(`(compiled = up-to-date artifacts in ${shadersCacheDir(root)})`)}`);
222189
222853
  const width = Math.max(...shaders.map((s2) => s2.path.length));
222190
222854
  for (const shader of shaders) {
222191
- const state = matcId === null ? c.dim("unknown (matc not downloaded)") : shaderIsCompiled(root, readFileSync23(shader.absPath, "utf8"), matcId) ? "compiled" : c.yellow("not compiled — run `lecodes shaders compile`");
222855
+ let state;
222856
+ if (matcId === null)
222857
+ state = c.dim("unknown (matc not downloaded)");
222858
+ else {
222859
+ const text = readFileSync24(shader.absPath, "utf8");
222860
+ if (shaderIsCompiled(root, text, matcId))
222861
+ state = "compiled";
222862
+ else {
222863
+ const present = shaderCompiledPlatforms(root, text, matcId);
222864
+ state = present.length === 0 ? c.yellow("not compiled — run `lecodes shaders compile`") : c.yellow(`partial (${present.join(", ")}) — \`lecodes shaders compile\` for the rest`);
222865
+ }
222866
+ }
222192
222867
  log(` ${shader.path.padEnd(width)} ${state}`);
222868
+ const schema = parseShaderSchema(readFileSync24(shader.absPath, "utf8"));
222869
+ for (const p2 of schema.params) {
222870
+ const range = p2.min !== undefined ? ` [${p2.min}..${p2.max}]` : "";
222871
+ const def = p2.default !== null && p2.default !== undefined ? ` = ${JSON.stringify(p2.default)}` : "";
222872
+ log(` ${c.dim(p2.type.padEnd(10))} ${p2.name}${c.dim(` ${p2.editor}${range}${def}`)}${p2.tooltip ? c.dim(` — ${p2.tooltip}`) : ""}`);
222873
+ }
222874
+ for (const e2 of schema.errors)
222875
+ log(` ${c.yellow(e2)}`);
222193
222876
  }
222194
222877
  }
222195
222878
  try {
@@ -222201,7 +222884,23 @@ var status2 = async () => {
222201
222884
  log(`published ${c.dim(e2 instanceof CliError ? e2.message : "unknown (couldn't reach the release manifest)")}`);
222202
222885
  }
222203
222886
  };
222204
- var compile3 = async () => {
222887
+ var parsePlatforms = (args) => {
222888
+ const raw = flagStr(args, "platform", "platforms");
222889
+ if (raw === undefined)
222890
+ return;
222891
+ const known = allShaderPlatforms();
222892
+ const asked = raw.split(",").map((p2) => p2.trim()).filter(Boolean);
222893
+ if (asked.length === 0)
222894
+ throw new CliError(`--platform needs at least one target. Known: ${known.join(", ")}`);
222895
+ const bad = asked.filter((p2) => !known.includes(p2));
222896
+ if (bad.length > 0) {
222897
+ throw new CliError(`Unknown shader platform${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}
222898
+ Known: ${known.join(", ")}`);
222899
+ }
222900
+ return asked;
222901
+ };
222902
+ var compile3 = async (args) => {
222903
+ const platforms = parsePlatforms(args);
222205
222904
  const { root, entries, shaders } = collectShaders();
222206
222905
  if (shaders.length === 0) {
222207
222906
  note("No .mat shaders in this project.");
@@ -222209,11 +222908,11 @@ var compile3 = async () => {
222209
222908
  }
222210
222909
  const matcExe = await resolveMatcExe();
222211
222910
  const matcId = matcIdentity(matcExe);
222212
- const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync23(s2.absPath, "utf8"), matcId)]));
222911
+ const wasCompiled = new Map(shaders.map((s2) => [s2.path, shaderIsCompiled(root, readFileSync24(s2.absPath, "utf8"), matcId)]));
222213
222912
  const errors2 = [];
222214
222913
  for (const shader of shaders) {
222215
222914
  try {
222216
- const { warnings } = await applyLocalShaders([shader], { root, local: true });
222915
+ const { warnings } = await applyLocalShaders([shader], { root, local: true, platforms });
222217
222916
  for (const w of warnings)
222218
222917
  warnErr(w);
222219
222918
  log(` ${shader.path} ${wasCompiled.get(shader.path) ? c.dim("compiled (cached)") : "compiled"}`);
@@ -222228,22 +222927,23 @@ var compile3 = async () => {
222228
222927
  ${c.red("✗")} ${e2}`);
222229
222928
  if (errors2.length > 0)
222230
222929
  throw new CliError(`${errors2.length} of ${shaders.length} shader${shaders.length === 1 ? "" : "s"} failed to compile.`);
222231
- note(`${shaders.length} shader${shaders.length === 1 ? "" : "s"} compiled → ${shadersCacheDir(root)}`);
222930
+ const scope = platforms ? ` [${platforms.join(", ")}]` : "";
222931
+ note(`${shaders.length} shader${shaders.length === 1 ? "" : "s"} compiled${scope} → ${shadersCacheDir(root)}`);
222232
222932
  };
222233
222933
  var shaders = async (args) => {
222234
222934
  const sub = args._[0];
222235
222935
  if (sub === undefined)
222236
222936
  return status2();
222237
222937
  if (sub === "compile")
222238
- return compile3();
222938
+ return compile3(args);
222239
222939
  if (sub === "update")
222240
222940
  return updateMatc();
222241
- throw new CliError(`Unknown subcommand "shaders ${sub}". Use \`lecodes shaders\` (status), \`lecodes shaders compile\` or \`lecodes shaders update\`.`);
222941
+ throw new CliError(`Unknown subcommand "shaders ${sub}". Use \`lecodes shaders\` (status), \`lecodes shaders compile [--platform a,b]\` or \`lecodes shaders update\`.`);
222242
222942
  };
222243
222943
 
222244
222944
  // src/commands/render.ts
222245
- import { mkdirSync as mkdirSync20, readFileSync as readFileSync24, writeFileSync as writeFileSync21 } from "node:fs";
222246
- import { dirname as dirname15, resolve as resolve13 } from "node:path";
222945
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync22 } from "node:fs";
222946
+ import { dirname as dirname15, resolve as resolve15 } from "node:path";
222247
222947
 
222248
222948
  // src/compile/headlessBundle.ts
222249
222949
  init_api();
@@ -222254,7 +222954,7 @@ init_fonts();
222254
222954
  init_assetIcons();
222255
222955
  init_util();
222256
222956
  var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
222257
- var compileHeadlessBundle = async (args) => {
222957
+ var compileHeadlessBundle = async (args, opts = {}) => {
222258
222958
  let root;
222259
222959
  let manifest;
222260
222960
  try {
@@ -222289,7 +222989,9 @@ var compileHeadlessBundle = async (args) => {
222289
222989
  warnings.push("This is a local project (no server) — assets always load from the working tree.");
222290
222990
  }
222291
222991
  }
222292
- const compileEntries = useLocalAssets ? entries.map((e2) => e2.type === "resource" && e2.absPath ? { ...e2, fileSrc: encodeURIComponent(e2.absPath) } : e2) : entries;
222992
+ const compileEntries = useLocalAssets ? entries.map((e2) => e2.type === "resource" && e2.absPath ? { ...e2, fileSrc: encodeURIComponent(e2.absPath) } : e2) : [...entries];
222993
+ if (opts.screen)
222994
+ compileEntries.push(opts.screen.entry);
222293
222995
  await applyProjectFonts(compileEntries, useLocalAssets ? {
222294
222996
  mode: "vendor",
222295
222997
  root,
@@ -222299,7 +223001,7 @@ var compileHeadlessBundle = async (args) => {
222299
223001
  } : { mode: "cdn", root });
222300
223002
  await applyAppAssetIcons(compileEntries);
222301
223003
  note("Compiling…");
222302
- const entryFlag = flagStr(args, "entry");
223004
+ const entryFlag = opts.screen?.entryPath ?? flagStr(args, "entry");
222303
223005
  if (!entryFlag) {
222304
223006
  const ambiguous = ambiguousEntryWarning(compileEntries);
222305
223007
  if (ambiguous)
@@ -222322,12 +223024,84 @@ var compileHeadlessBundle = async (args) => {
222322
223024
  return { js, root, warnings, useLocalAssets };
222323
223025
  };
222324
223026
 
223027
+ // src/compile/screenEntry.ts
223028
+ init_designMeta();
223029
+ init_util();
223030
+ import { existsSync as existsSync28, readFileSync as readFileSync25, statSync as statSync10 } from "node:fs";
223031
+ import { isAbsolute, join as join30, relative as relative5, resolve as resolve13 } from "node:path";
223032
+ var SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
223033
+ var SCREEN_ENTRY_PATH = "/.screen-entry.ts";
223034
+ var DEFAULT_DESIGN_DIR = "design";
223035
+ var screenArg = (args) => args._[0];
223036
+ var hasSeparator = (file) => file.includes("/") || file.includes("\\");
223037
+ var findScreenFile = (root, file, dirFlag) => {
223038
+ const candidates = [resolve13(process.cwd(), file)];
223039
+ if (!hasSeparator(file)) {
223040
+ candidates.push(join30(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file));
223041
+ }
223042
+ for (const base of candidates) {
223043
+ if (existsSync28(base) && statSync10(base).isFile())
223044
+ return base;
223045
+ for (const ext2 of SCREEN_EXTS)
223046
+ if (existsSync28(base + ext2))
223047
+ return base + ext2;
223048
+ }
223049
+ throw new CliError(`No screen file at "${file}".` + (hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`));
223050
+ };
223051
+ var designDirOf = (root, key) => {
223052
+ const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key);
223053
+ if (!m)
223054
+ return null;
223055
+ const dir = join30(root, m[1]);
223056
+ return existsSync28(join30(dir, "meta.json")) ? { dir, id: m[2] } : null;
223057
+ };
223058
+ var resolveScreenTarget = (root, file, opts = {}) => {
223059
+ const abs = findScreenFile(root, file, opts.dir);
223060
+ const rel = relative5(root, abs);
223061
+ if (rel.startsWith("..") || isAbsolute(rel)) {
223062
+ throw new CliError(`"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` + `imports and asset() files resolve from the same tree.`);
223063
+ }
223064
+ const key = toKey(rel);
223065
+ if (!/^\s*export\s+default\b/m.test(readFileSync25(abs, "utf8"))) {
223066
+ throw new CliError(`${key.slice(1)} has no default export. A screen file ends with ` + `\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`);
223067
+ }
223068
+ const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()";
223069
+ const spec = "." + key.replace(/\.[^./]+$/, "");
223070
+ const design2 = designDirOf(root, key);
223071
+ return {
223072
+ entryPath: SCREEN_ENTRY_PATH,
223073
+ entry: {
223074
+ path: SCREEN_ENTRY_PATH,
223075
+ type: "text",
223076
+ text: `import screen from ${JSON.stringify(spec)}
223077
+ ` + `const __screen = typeof screen === "function" ? ${invoke} : screen
223078
+ ` + `__screen.open()
223079
+ `
223080
+ },
223081
+ frame: design2 ? screenFrame(design2.dir, design2.id) : undefined,
223082
+ label: (design2 ? design2.id : key.slice(1)) + (opts.state !== undefined ? `@${opts.state}` : "")
223083
+ };
223084
+ };
223085
+ var screenTargetFromArgs = (root, args) => {
223086
+ const file = screenArg(args);
223087
+ if (file === undefined) {
223088
+ if (flagStr(args, "state") !== undefined) {
223089
+ throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>");
223090
+ }
223091
+ return;
223092
+ }
223093
+ if (flagStr(args, "entry") !== undefined) {
223094
+ throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.");
223095
+ }
223096
+ return resolveScreenTarget(root, file, { state: flagStr(args, "state"), dir: flagStr(args, "dir") });
223097
+ };
223098
+
222325
223099
  // src/commands/render.ts
222326
223100
  init_manifest();
222327
223101
 
222328
223102
  // src/desktopScript.ts
222329
223103
  init_util();
222330
- import { resolve as resolve12 } from "node:path";
223104
+ import { resolve as resolve14 } from "node:path";
222331
223105
  var isPoint = (v) => typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number";
222332
223106
  var FRAME_MS = 16;
222333
223107
  var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "look", "mouseTo", "wheel", "stick", "gamepad", "camera", "view"];
@@ -222355,7 +223129,7 @@ var translateScenario = (rawSteps, opts) => {
222355
223129
  }
222356
223130
  throw new CliError(`${at}: "${key}" must be { "x", "y" }.`);
222357
223131
  };
222358
- const num = (at, key, v, def) => {
223132
+ const num2 = (at, key, v, def) => {
222359
223133
  if (v === undefined && def !== undefined)
222360
223134
  return def;
222361
223135
  if (typeof v !== "number")
@@ -222382,7 +223156,7 @@ var translateScenario = (rawSteps, opts) => {
222382
223156
  }
222383
223157
  if (action === "view") {
222384
223158
  const v = s2.view;
222385
- const fov = s2.fov !== undefined ? num(at, "fov", s2.fov) : undefined;
223159
+ const fov = s2.fov !== undefined ? num2(at, "fov", s2.fov) : undefined;
222386
223160
  if (v === "off") {
222387
223161
  plan.lines.push(emit("view off"));
222388
223162
  inspecting = false;
@@ -222402,7 +223176,7 @@ var translateScenario = (rawSteps, opts) => {
222402
223176
  parts.push("camera off");
222403
223177
  } else if (typeof cam === "object" && cam !== null && isVec3(cam.at) && isVec3(cam.lookAt)) {
222404
223178
  const { at: eye, lookAt, fov } = cam;
222405
- const fovN = fov !== undefined ? num(at, "camera.fov", fov) : undefined;
223179
+ const fovN = fov !== undefined ? num2(at, "camera.fov", fov) : undefined;
222406
223180
  plan.lines.push(emit(`camera ${eye.join(" ")} ${lookAt.join(" ")}${fovN !== undefined ? ` ${fovN}` : ""}`));
222407
223181
  inspecting = true;
222408
223182
  parts.push(`camera at (${eye.join(",")}) → (${lookAt.join(",")})`);
@@ -222419,20 +223193,20 @@ var translateScenario = (rawSteps, opts) => {
222419
223193
  parts.push(`tap ${pt(p2)}`);
222420
223194
  } else if (action === "drag") {
222421
223195
  const a2 = coord(at, "drag", s2.drag), b = coord(at, "to", s2.to);
222422
- const ms = num(at, "ms", s2.ms, 300);
223196
+ const ms = num2(at, "ms", s2.ms, 300);
222423
223197
  plan.lines.push(emit(`drag ${a2.x} ${a2.y} ${b.x} ${b.y} ${Math.round(ms)}`));
222424
223198
  budgetMs += ms + 4 * FRAME_MS;
222425
223199
  parts.push(`drag ${pt(a2)} → ${pt(b)} over ${ms}ms`);
222426
223200
  } else if (action === "hold") {
222427
223201
  const p2 = coord(at, "hold", s2.hold);
222428
- const ms = num(at, "ms", s2.ms);
223202
+ const ms = num2(at, "ms", s2.ms);
222429
223203
  plan.lines.push(emit(`hold ${p2.x} ${p2.y} ${Math.round(ms)}`));
222430
223204
  budgetMs += ms + 4 * FRAME_MS;
222431
223205
  parts.push(`hold ${pt(p2)} ${ms}ms`);
222432
223206
  } else if (action === "key") {
222433
223207
  if (typeof s2.key !== "string")
222434
223208
  throw new CliError(`${at}: "key" must be a KeyboardEvent.code string.`);
222435
- const ms = num(at, "ms", s2.ms);
223209
+ const ms = num2(at, "ms", s2.ms);
222436
223210
  plan.lines.push(emit(`key ${s2.key} ${Math.round(ms)}`));
222437
223211
  budgetMs += ms + 2 * FRAME_MS;
222438
223212
  parts.push(`key ${s2.key} ${ms}ms`);
@@ -222447,7 +223221,7 @@ var translateScenario = (rawSteps, opts) => {
222447
223221
  const v = s2.look;
222448
223222
  if (!Array.isArray(v) || v.length !== 2 || !v.every((n2) => typeof n2 === "number"))
222449
223223
  throw new CliError(`${at}: "look" must be [dx, dy] (logical px of mouse motion).`);
222450
- const ms = num(at, "ms", s2.ms, FRAME_MS);
223224
+ const ms = num2(at, "ms", s2.ms, FRAME_MS);
222451
223225
  plan.lines.push(emit(`look ${v[0]} ${v[1]} ${Math.round(ms)}`));
222452
223226
  budgetMs += ms + 2 * FRAME_MS;
222453
223227
  parts.push(`look ${v[0]},${v[1]} over ${ms}ms`);
@@ -222493,12 +223267,12 @@ var translateScenario = (rawSteps, opts) => {
222493
223267
  budgetMs += 2 * FRAME_MS;
222494
223268
  parts.push(`gamepad ${s2.gamepad} #${pad}`);
222495
223269
  } else if (action === "wait") {
222496
- const ms = num(at, "wait", s2.wait);
223270
+ const ms = num2(at, "wait", s2.wait);
222497
223271
  plan.lines.push(emit(`wait ${Math.round(ms)}`));
222498
223272
  budgetMs += ms + FRAME_MS;
222499
223273
  parts.push(`wait ${ms}ms`);
222500
223274
  } else if (action === "frames") {
222501
- const n2 = num(at, "frames", s2.frames);
223275
+ const n2 = num2(at, "frames", s2.frames);
222502
223276
  plan.lines.push(emit(`frames ${Math.round(n2)}`));
222503
223277
  budgetMs += n2 * FRAME_MS + FRAME_MS;
222504
223278
  parts.push(`advance ${n2} frames`);
@@ -222528,7 +223302,7 @@ var translateScenario = (rawSteps, opts) => {
222528
223302
  if (s2.screenshot !== undefined) {
222529
223303
  if (typeof s2.screenshot !== "string")
222530
223304
  throw new CliError(`${at}: "screenshot" must be a path.`);
222531
- plan.screenshot = resolve12(opts.outDir, s2.screenshot);
223305
+ plan.screenshot = resolve14(opts.outDir, s2.screenshot);
222532
223306
  plan.lines.push(emit(`frames 2`));
222533
223307
  plan.lines.push(emit(`shot ${plan.screenshot}`));
222534
223308
  budgetMs += 3 * FRAME_MS;
@@ -222637,9 +223411,9 @@ var requireRenderer = async () => {
222637
223411
  };
222638
223412
  var fileScenarioIO = (dir) => ({
222639
223413
  write: (relPath, data) => {
222640
- const abs = resolve13(dir, relPath);
222641
- mkdirSync20(dirname15(abs), { recursive: true });
222642
- writeFileSync21(abs, data);
223414
+ const abs = resolve15(dir, relPath);
223415
+ mkdirSync21(dirname15(abs), { recursive: true });
223416
+ writeFileSync22(abs, data);
222643
223417
  }
222644
223418
  });
222645
223419
  var resolveFixtures = (value, baseDir) => {
@@ -222647,9 +223421,9 @@ var resolveFixtures = (value, baseDir) => {
222647
223421
  return;
222648
223422
  let map = value;
222649
223423
  if (typeof value === "string") {
222650
- const abs = resolve13(baseDir, value);
223424
+ const abs = resolve15(baseDir, value);
222651
223425
  try {
222652
- map = JSON.parse(readFileSync24(abs, "utf8"));
223426
+ map = JSON.parse(readFileSync26(abs, "utf8"));
222653
223427
  } catch (e2) {
222654
223428
  throw new CliError(`Can't read fixtures "${value}": ${e2 instanceof Error ? e2.message : String(e2)}`);
222655
223429
  }
@@ -222662,7 +223436,7 @@ var resolveFixtures = (value, baseDir) => {
222662
223436
  var readScenarioFile = (path2) => {
222663
223437
  let raw;
222664
223438
  try {
222665
- raw = JSON.parse(readFileSync24(resolve13(path2), "utf8"));
223439
+ raw = JSON.parse(readFileSync26(resolve15(path2), "utf8"));
222666
223440
  } catch (e2) {
222667
223441
  throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
222668
223442
  }
@@ -222697,10 +223471,11 @@ var renderDesktop = async (args) => {
222697
223471
  root = process.cwd();
222698
223472
  }
222699
223473
  const win = readAppConfigAt(root)?.desktop?.window;
223474
+ const screen2 = screenTargetFromArgs(root, args);
222700
223475
  const scriptFlag = flagStr(args, "script");
222701
223476
  const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
222702
- const width = Number(flagStr(args, "width")) || scenario?.width || win?.width || 390;
222703
- const height = Number(flagStr(args, "height")) || scenario?.height || win?.height || 844;
223477
+ const width = Number(flagStr(args, "width")) || scenario?.width || screen2?.frame?.[0] || win?.width || 390;
223478
+ const height = Number(flagStr(args, "height")) || scenario?.height || screen2?.frame?.[1] || win?.height || 844;
222704
223479
  const frames = Number(flagStr(args, "frames")) || 60;
222705
223480
  const pngFlag = flagStr(args, "png") ?? "screenshot.png";
222706
223481
  const logs = flagBool(args, "logs");
@@ -222711,17 +223486,24 @@ var renderDesktop = async (args) => {
222711
223486
  const { camera, view } = resolveInspectionCamera(args);
222712
223487
  if ((camera || view) && scenario)
222713
223488
  throw new CliError('--camera/--view apply to a plain render; in a --script run use a { "view": "iso" } or { "camera": { "at", "lookAt" } } step before the screenshot.');
222714
- const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
222715
- mkdirSync20(dirname15(resolve13(pngFlag)), { recursive: true });
223489
+ const desktopRenderer = resolveDesktopRenderer(root, args);
223490
+ const { js, resources } = await buildBundle(args, undefined, {
223491
+ localAssets: true,
223492
+ desktopRenderer,
223493
+ extraEntries: screen2 ? [screen2.entry] : undefined,
223494
+ entryOverride: screen2?.entryPath
223495
+ });
223496
+ mkdirSync21(dirname15(resolve15(pngFlag)), { recursive: true });
222716
223497
  if (scenario) {
222717
223498
  const outDir = flagStr(args, "out-dir") ?? ".";
222718
223499
  const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
222719
223500
  for (const st of plan.steps)
222720
223501
  if (st.screenshot)
222721
- mkdirSync20(dirname15(st.screenshot), { recursive: true });
223502
+ mkdirSync21(dirname15(st.screenshot), { recursive: true });
222722
223503
  const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
222723
223504
  note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
222724
223505
  const { output } = await runDesktopRender(js, resources, {
223506
+ renderer: desktopRenderer,
222725
223507
  width,
222726
223508
  height,
222727
223509
  frames: plan.frameCap,
@@ -222751,8 +223533,9 @@ var renderDesktop = async (args) => {
222751
223533
  process.exit(result.ok ? 0 : 1);
222752
223534
  }
222753
223535
  const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
222754
- note("Rendering (native)…");
223536
+ note(screen2 ? `Rendering ${screen2.label} (native)…` : "Rendering (native)…");
222755
223537
  await runDesktopRender(js, resources, {
223538
+ renderer: desktopRenderer,
222756
223539
  width,
222757
223540
  height,
222758
223541
  frames,
@@ -222798,6 +223581,13 @@ var render = async (args) => {
222798
223581
  return renderDesktop(args);
222799
223582
  const scriptFlag = flagStr(args, "script");
222800
223583
  const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
223584
+ let projectRoot2;
223585
+ try {
223586
+ projectRoot2 = findProjectRoot(process.cwd());
223587
+ } catch {
223588
+ projectRoot2 = process.cwd();
223589
+ }
223590
+ const screen2 = screenTargetFromArgs(projectRoot2, args);
222801
223591
  const stepCount = scenario && Array.isArray(scenario.steps) ? scenario.steps.length : 0;
222802
223592
  const timeoutMs = Number(flagStr(args, "timeout")) || 15000 + stepCount * 3000;
222803
223593
  const killer = setTimeout(() => {
@@ -222805,15 +223595,15 @@ var render = async (args) => {
222805
223595
  process.exit(1);
222806
223596
  }, timeoutMs);
222807
223597
  killer.unref?.();
222808
- const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
223598
+ const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args, { screen: screen2 });
222809
223599
  const renderer = await requireRenderer();
222810
223600
  const projectViewport = desktopDefaultViewport(root);
222811
- const width = Number(flagStr(args, "width")) || scenario?.width || projectViewport?.width || 390;
222812
- const height = Number(flagStr(args, "height")) || scenario?.height || projectViewport?.height || 844;
223601
+ const width = Number(flagStr(args, "width")) || scenario?.width || screen2?.frame?.[0] || projectViewport?.width || 390;
223602
+ const height = Number(flagStr(args, "height")) || scenario?.height || screen2?.frame?.[1] || projectViewport?.height || 844;
222813
223603
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
222814
223604
  const timeStr = flagStr(args, "time");
222815
223605
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
222816
- const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve13(scriptFlag)) : ".");
223606
+ const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve15(scriptFlag)) : ".");
222817
223607
  const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
222818
223608
  const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
222819
223609
  if (fixtures) {
@@ -222852,7 +223642,7 @@ var render = async (args) => {
222852
223642
  ${c.red(st.error ?? "failed")}`);
222853
223643
  }
222854
223644
  if (!result2.ok && result2.failureArtifacts.length > 0) {
222855
- logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve13(outDir, p2)).join(", ")}`));
223645
+ logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve15(outDir, p2)).join(", ")}`));
222856
223646
  }
222857
223647
  logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
222858
223648
  return done((cb) => cb(), result2.ok ? 0 : 1);
@@ -222862,7 +223652,7 @@ var render = async (args) => {
222862
223652
  }
222863
223653
  const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
222864
223654
  if (pngFlag !== undefined) {
222865
- note("Rendering PNG…");
223655
+ note(screen2 ? `Rendering ${screen2.label} (PNG)…` : "Rendering PNG…");
222866
223656
  const clip = parseClip(flagStr(args, "clip"));
222867
223657
  const { png: png2, width: outW, height: outH, warnings: pngWarnings } = await renderer.renderToPng(js, {
222868
223658
  width,
@@ -222876,12 +223666,13 @@ var render = async (args) => {
222876
223666
  if (isNullSink(pngFlag)) {
222877
223667
  note(`Discarded PNG (${pngFlag})`);
222878
223668
  } else {
222879
- writeFileSync21(resolve13(pngFlag), png2);
223669
+ mkdirSync21(dirname15(resolve15(pngFlag)), { recursive: true });
223670
+ writeFileSync22(resolve15(pngFlag), png2);
222880
223671
  note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
222881
223672
  }
222882
223673
  return done((cb) => cb());
222883
223674
  }
222884
- note("Rendering…");
223675
+ note(screen2 ? `Rendering ${screen2.label}…` : "Rendering…");
222885
223676
  const result = await renderer.renderToJson(js, { width, height, ...timing });
222886
223677
  for (const w of [...warnings, ...result.warnings])
222887
223678
  warnErr(w);
@@ -222891,7 +223682,7 @@ var render = async (args) => {
222891
223682
  return done((cb) => cb());
222892
223683
  }
222893
223684
  if (out) {
222894
- writeFileSync21(resolve13(out), result.text);
223685
+ writeFileSync22(resolve15(out), result.text);
222895
223686
  note(`Wrote ${out} (${result.text.length} bytes)`);
222896
223687
  return done((cb) => cb());
222897
223688
  }
@@ -222906,8 +223697,8 @@ init_api();
222906
223697
  init_config();
222907
223698
  init_manifest();
222908
223699
  init_util();
222909
- import { appendFileSync as appendFileSync2, existsSync as existsSync27, readFileSync as readFileSync25, writeFileSync as writeFileSync22 } from "node:fs";
222910
- import { join as join28 } from "node:path";
223700
+ import { appendFileSync as appendFileSync2, existsSync as existsSync29, readFileSync as readFileSync27, writeFileSync as writeFileSync23 } from "node:fs";
223701
+ import { join as join31 } from "node:path";
222911
223702
  var ENV_VAR = "LECODES_PUSH_KEY";
222912
223703
  var context = () => {
222913
223704
  const root = findProjectRoot(process.cwd());
@@ -223051,11 +223842,11 @@ var pnKeysList = async (ctx) => {
223051
223842
  };
223052
223843
  var writeEnv = (root, key) => {
223053
223844
  const touched = [];
223054
- const envPath = join28(root, ".env");
223845
+ const envPath = join31(root, ".env");
223055
223846
  const line = `${ENV_VAR}=${key}`;
223056
- const existing = existsSync27(envPath) ? readFileSync25(envPath, "utf8") : "";
223847
+ const existing = existsSync29(envPath) ? readFileSync27(envPath, "utf8") : "";
223057
223848
  if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
223058
- writeFileSync22(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
223849
+ writeFileSync23(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
223059
223850
  } else {
223060
223851
  const separator = !existing || existing.endsWith(`
223061
223852
  `) ? "" : `
@@ -223064,8 +223855,8 @@ var writeEnv = (root, key) => {
223064
223855
  `);
223065
223856
  }
223066
223857
  touched.push(".env");
223067
- const ignorePath = join28(root, ".gitignore");
223068
- const ignore = existsSync27(ignorePath) ? readFileSync25(ignorePath, "utf8") : "";
223858
+ const ignorePath = join31(root, ".gitignore");
223859
+ const ignore = existsSync29(ignorePath) ? readFileSync27(ignorePath, "utf8") : "";
223069
223860
  if (!ignore.split(`
223070
223861
  `).some((l2) => l2.trim() === ".env")) {
223071
223862
  const separator = !ignore || ignore.endsWith(`
@@ -223164,8 +223955,8 @@ var pn = async (args) => {
223164
223955
  init_api();
223165
223956
  init_config();
223166
223957
  init_manifest();
223167
- import { existsSync as existsSync28, mkdirSync as mkdirSync21, rmSync as rmSync8, writeFileSync as writeFileSync23 } from "node:fs";
223168
- import { dirname as dirname16, join as join29 } from "node:path";
223958
+ import { existsSync as existsSync30, mkdirSync as mkdirSync22, rmSync as rmSync9, writeFileSync as writeFileSync24 } from "node:fs";
223959
+ import { dirname as dirname16, join as join32 } from "node:path";
223169
223960
  init_types4();
223170
223961
  init_localFiles();
223171
223962
  init_util();
@@ -223188,9 +223979,9 @@ var pull = async (args) => {
223188
223979
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a2) => a2.id === id)?.type !== "folder").map(([, path2]) => path2));
223189
223980
  for (const path2 of Object.keys(manifest.files)) {
223190
223981
  if (!remotePaths.has(path2)) {
223191
- const abs = join29(root, ...path2.replace(/^\//, "").split("/"));
223192
- if (existsSync28(abs))
223193
- rmSync8(abs);
223982
+ const abs = join32(root, ...path2.replace(/^\//, "").split("/"));
223983
+ if (existsSync30(abs))
223984
+ rmSync9(abs);
223194
223985
  }
223195
223986
  }
223196
223987
  const files = await materializeAssets(root, project, apiUrl, token, progress);
@@ -223227,18 +224018,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
223227
224018
  const files = { ...manifest.files };
223228
224019
  for (const asset of targets) {
223229
224020
  const key = pathById.get(asset.id);
223230
- const abs = join29(root, ...key.replace(/^\//, "").split("/"));
223231
- mkdirSync21(dirname16(abs), { recursive: true });
224021
+ const abs = join32(root, ...key.replace(/^\//, "").split("/"));
224022
+ mkdirSync22(dirname16(abs), { recursive: true });
223232
224023
  let sha;
223233
224024
  if (asset.type === "file") {
223234
224025
  if (!asset.file?.src)
223235
224026
  continue;
223236
224027
  const bytes = await downloadBlob(apiUrl, asset.file.src);
223237
- writeFileSync23(abs, bytes);
224028
+ writeFileSync24(abs, bytes);
223238
224029
  sha = sha256(bytes);
223239
224030
  } else {
223240
224031
  const text = asset.text ?? "";
223241
- writeFileSync23(abs, text);
224032
+ writeFileSync24(abs, text);
223242
224033
  sha = sha256(text);
223243
224034
  }
223244
224035
  files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
@@ -223251,8 +224042,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
223251
224042
 
223252
224043
  // src/commands/scene.ts
223253
224044
  init_manifest();
223254
- import { existsSync as existsSync30, readFileSync as readFileSync27, readdirSync as readdirSync13, statSync as statSync9, writeFileSync as writeFileSync24 } from "node:fs";
223255
- import { basename as basename15, dirname as dirname17, join as join31, resolve as resolve14 } from "node:path";
224045
+ import { existsSync as existsSync32, readFileSync as readFileSync29, readdirSync as readdirSync13, statSync as statSync11, writeFileSync as writeFileSync26 } from "node:fs";
224046
+ import { basename as basename15, dirname as dirname17, join as join34, resolve as resolve16 } from "node:path";
223256
224047
 
223257
224048
  // src/compile/sceneCompile.ts
223258
224049
  init_localFiles();
@@ -223260,18 +224051,19 @@ init_ignore();
223260
224051
  init_distRoot();
223261
224052
  init_util();
223262
224053
  init_projectCompile();
223263
- import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
223264
- import { join as join30 } from "node:path";
224054
+ import { createHash as createHash8 } from "node:crypto";
224055
+ import { existsSync as existsSync31, mkdirSync as mkdirSync23, readFileSync as readFileSync28, writeFileSync as writeFileSync25 } from "node:fs";
224056
+ import { join as join33 } from "node:path";
223265
224057
  var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
223266
224058
  var rel = (key) => key.replace(/^\//, "");
223267
224059
  var resolveHarness = (loadSceneHarness2) => {
223268
- const inRepo = join30(distRoot, "..", "projects", "src", "scene-editor");
224060
+ const inRepo = join33(distRoot, "..", "projects", "src", "scene-editor");
223269
224061
  try {
223270
- return loadSceneHarness2(existsSync29(inRepo) ? inRepo : undefined);
224062
+ return loadSceneHarness2(existsSync31(inRepo) ? inRepo : undefined);
223271
224063
  } catch {
223272
- const vendored = join30(distRoot, "runtime", "scene-harness.json");
223273
- if (existsSync29(vendored))
223274
- return JSON.parse(readFileSync26(vendored, "utf8"));
224064
+ const vendored = join33(distRoot, "runtime", "scene-harness.json");
224065
+ if (existsSync31(vendored))
224066
+ return JSON.parse(readFileSync28(vendored, "utf8"));
223275
224067
  throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
223276
224068
  }
223277
224069
  };
@@ -223283,11 +224075,32 @@ var compileSceneEditor = async (opts) => {
223283
224075
  for (const file of scan2.files) {
223284
224076
  const path2 = "./" + rel(file.path);
223285
224077
  if (TEXT_EXT.test(path2)) {
223286
- entries.push({ path: path2, type: "text", text: readFileSync26(file.absPath, "utf8") });
224078
+ entries.push({ path: path2, type: "text", text: readFileSync28(file.absPath, "utf8") });
224079
+ } else if (/\.mat$/i.test(path2)) {
224080
+ entries.push({ path: path2, type: "shader", text: readFileSync28(file.absPath, "utf8"), absPath: file.absPath });
223287
224081
  } else {
223288
224082
  entries.push({ path: path2, type: "resource", fileSrc: "/project/" + rel(file.path) });
223289
224083
  }
223290
224084
  }
224085
+ const shaderEntries = entries.filter((e2) => e2.type === "shader");
224086
+ if (shaderEntries.length > 0) {
224087
+ const { warnings } = await applyLocalShaders(shaderEntries, { root, srcPrefix: "/project/.lecodes/shaders", platforms: ["webgl", "webgl-lite"] });
224088
+ for (const w of warnings)
224089
+ warnErr(w);
224090
+ const { buildLiteMaterialArtifact: buildLiteMaterialArtifact2 } = await Promise.resolve().then(() => (init_compile(), exports_compile));
224091
+ for (const e2 of shaderEntries) {
224092
+ if (e2.shaders?.length)
224093
+ continue;
224094
+ const hash = createHash8("sha256").update(e2.text ?? "").digest("hex").slice(0, 16);
224095
+ const dir = shadersCacheDir(root);
224096
+ mkdirSync23(dir, { recursive: true });
224097
+ const liteName = `${hash}_webgl-lite.filamat`;
224098
+ if (!existsSync31(join33(dir, liteName)))
224099
+ writeFileSync25(join33(dir, liteName), JSON.stringify(buildLiteMaterialArtifact2(e2.text ?? "")));
224100
+ const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`;
224101
+ e2.shaders = ["opengl", "webgl", "webgl-lite"].map((platform) => ({ platform, src }));
224102
+ }
224103
+ }
223291
224104
  const scene = scenePath.startsWith("./") ? scenePath : "./" + rel(scenePath);
223292
224105
  if (!entries.some((e2) => e2.path === scene && e2.type === "text")) {
223293
224106
  return { js: null, error: `Scene file not found: ${scene}` };
@@ -223317,7 +224130,7 @@ var DEFAULT_PORT3 = 4488;
223317
224130
  var findAppRoot = (start) => {
223318
224131
  let dir = start;
223319
224132
  while (true) {
223320
- if (existsSync30(join31(dir, "app.json")))
224133
+ if (existsSync32(join34(dir, "app.json")))
223321
224134
  return dir;
223322
224135
  const parent = dirname17(dir);
223323
224136
  if (parent === dir)
@@ -223336,10 +224149,10 @@ var getContext2 = (args) => {
223336
224149
  manifest = null;
223337
224150
  const pathArg = args._[0];
223338
224151
  if (pathArg) {
223339
- const abs = resolve14(process.cwd(), pathArg);
223340
- if (existsSync30(abs) && statSync9(abs).isDirectory())
224152
+ const abs = resolve16(process.cwd(), pathArg);
224153
+ if (existsSync32(abs) && statSync11(abs).isDirectory())
223341
224154
  root = abs;
223342
- else if (abs.endsWith(".scene.ts") && existsSync30(abs))
224155
+ else if (abs.endsWith(".scene.ts") && existsSync32(abs))
223343
224156
  root = findAppRoot(dirname17(abs)) ?? dirname17(abs);
223344
224157
  else
223345
224158
  throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
@@ -223350,12 +224163,12 @@ var getContext2 = (args) => {
223350
224163
  return { root, manifest, name: manifest?.name ?? basename15(root) };
223351
224164
  };
223352
224165
  var writeMcpConfig2 = (root, port) => {
223353
- const path2 = join31(root, ".mcp.json");
224166
+ const path2 = join34(root, ".mcp.json");
223354
224167
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
223355
224168
  let config = {};
223356
- if (existsSync30(path2)) {
224169
+ if (existsSync32(path2)) {
223357
224170
  try {
223358
- config = JSON.parse(readFileSync27(path2, "utf8"));
224171
+ config = JSON.parse(readFileSync29(path2, "utf8"));
223359
224172
  } catch {
223360
224173
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
223361
224174
  return;
@@ -223368,7 +224181,7 @@ var writeMcpConfig2 = (root, port) => {
223368
224181
  return;
223369
224182
  }
223370
224183
  config.mcpServers["lecodes-scene"] = entry;
223371
- writeFileSync24(path2, JSON.stringify(config, null, 2) + `
224184
+ writeFileSync26(path2, JSON.stringify(config, null, 2) + `
223372
224185
  `);
223373
224186
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
223374
224187
  };
@@ -223377,8 +224190,8 @@ var countScenes = (dir) => {
223377
224190
  for (const name of readdirSync13(dir)) {
223378
224191
  if (name === "node_modules" || name.startsWith("."))
223379
224192
  continue;
223380
- const full = join31(dir, name);
223381
- if (statSync9(full).isDirectory())
224193
+ const full = join34(dir, name);
224194
+ if (statSync11(full).isDirectory())
223382
224195
  n2 += countScenes(full);
223383
224196
  else if (name.endsWith(".scene.ts"))
223384
224197
  n2++;
@@ -223406,7 +224219,8 @@ var scene = async (args) => {
223406
224219
  manifest: ctx.manifest,
223407
224220
  name: ctx.name,
223408
224221
  scenePath
223409
- })
224222
+ }),
224223
+ shaderArtifact: (shaderText, platform) => localShaderArtifact(ctx.root, shaderText, platform)
223410
224224
  });
223411
224225
  if (!flagBool(args, "no-mcp"))
223412
224226
  writeMcpConfig2(ctx.root, port);
@@ -223420,6 +224234,79 @@ var scene = async (args) => {
223420
224234
 
223421
224235
  // src/commands/assets.ts
223422
224236
  init_util();
224237
+
224238
+ // src/cmgenTool.ts
224239
+ import { existsSync as existsSync33, readdirSync as readdirSync14 } from "node:fs";
224240
+ import { homedir as homedir8 } from "node:os";
224241
+ import { join as join35, resolve as resolve17 } from "node:path";
224242
+ init_util();
224243
+ var exeName2 = () => process.platform === "win32" ? "cmgen.exe" : "cmgen";
224244
+ var platformKey3 = () => {
224245
+ if (process.platform === "win32")
224246
+ return "windows-x64";
224247
+ if (process.platform === "linux")
224248
+ return "linux-x64";
224249
+ throw new CliError(process.platform === "darwin" ? "cmgen isn't published for macOS yet — build filament's host tools locally and set LECODES_CMGEN." : `cmgen isn't published for ${process.platform}.`);
224250
+ };
224251
+ var cacheRoot3 = () => join35(homedir8(), ".lecodes", "cmgen");
224252
+ var findCmgenIn = (dir) => {
224253
+ for (const entry of readdirSync14(dir, { withFileTypes: true })) {
224254
+ const p2 = join35(dir, entry.name);
224255
+ if (entry.isFile() && entry.name === exeName2())
224256
+ return p2;
224257
+ if (entry.isDirectory()) {
224258
+ const found = findCmgenIn(p2);
224259
+ if (found)
224260
+ return found;
224261
+ }
224262
+ }
224263
+ return null;
224264
+ };
224265
+ var installedCmgenVersions = () => {
224266
+ try {
224267
+ return readdirSync14(cacheRoot3(), { withFileTypes: true }).filter((e2) => e2.isDirectory() && e2.name.startsWith("cmgen-")).map((e2) => e2.name.slice("cmgen-".length)).filter((v) => findCmgenIn(join35(cacheRoot3(), `cmgen-${v}`)) !== null).sort(semverNewer);
224268
+ } catch {
224269
+ return [];
224270
+ }
224271
+ };
224272
+ var fetchCmgenManifest = () => fetchReleaseProduct("cmgen", "cmgen (environment tool)");
224273
+ var downloadCmgen = async (version, versions, dest) => {
224274
+ const key = platformKey3();
224275
+ const file = versions.find((v) => v.version === version)?.files[key];
224276
+ if (!file) {
224277
+ throw new CliError(`cmgen ${version} [${key}] isn't published — build filament's host tools locally and point ` + `LECODES_CMGEN at the binary.`);
224278
+ }
224279
+ note(`Downloading the environment tool (cmgen ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`);
224280
+ await downloadReleaseArchive(file, {
224281
+ cacheRoot: cacheRoot3(),
224282
+ dest,
224283
+ validate: (dir) => findCmgenIn(dir) ? null : "The downloaded archive doesn't contain the cmgen binary."
224284
+ });
224285
+ };
224286
+ var resolveCmgenExeCached = () => {
224287
+ const override = process.env.LECODES_CMGEN;
224288
+ if (override) {
224289
+ if (!existsSync33(override))
224290
+ throw new CliError(`LECODES_CMGEN points at "${override}" which doesn't exist.`);
224291
+ return resolve17(override);
224292
+ }
224293
+ const cached = installedCmgenVersions()[0];
224294
+ return cached ? findCmgenIn(join35(cacheRoot3(), `cmgen-${cached}`)) : null;
224295
+ };
224296
+ var resolveCmgenExe = async () => {
224297
+ const cached = resolveCmgenExeCached();
224298
+ if (cached)
224299
+ return cached;
224300
+ const { latest, versions } = await fetchCmgenManifest();
224301
+ const dir = join35(cacheRoot3(), `cmgen-${latest}`);
224302
+ await downloadCmgen(latest, versions, dir);
224303
+ const exe = findCmgenIn(dir);
224304
+ if (!exe)
224305
+ throw new CliError(`cmgen cache at ${dir} is broken — delete the folder and retry.`);
224306
+ return exe;
224307
+ };
224308
+
224309
+ // src/commands/assets.ts
223423
224310
  var assets = async (args) => {
223424
224311
  let mod;
223425
224312
  try {
@@ -223428,6 +224315,9 @@ var assets = async (args) => {
223428
224315
  throw new CliError("The asset pipeline isn't installed. Run: npm install -g lecodes-assets");
223429
224316
  }
223430
224317
  const raw = args._raw ?? args._;
224318
+ if (raw[0] === "sky" && !process.env.LECODES_CMGEN) {
224319
+ process.env.LECODES_CMGEN = await resolveCmgenExe();
224320
+ }
223431
224321
  const code = await mod.runAssetsCli(raw);
223432
224322
  if (code !== 0)
223433
224323
  process.exitCode = code;
@@ -223466,8 +224356,8 @@ var status3 = async (_args) => {
223466
224356
  };
223467
224357
 
223468
224358
  // src/commands/test.ts
223469
- import { existsSync as existsSync31, mkdirSync as mkdirSync22, readFileSync as readFileSync28, readdirSync as readdirSync14 } from "node:fs";
223470
- import { dirname as dirname18, resolve as resolve15 } from "node:path";
224359
+ import { existsSync as existsSync34, mkdirSync as mkdirSync24, readFileSync as readFileSync30, readdirSync as readdirSync15 } from "node:fs";
224360
+ import { dirname as dirname18, resolve as resolve18 } from "node:path";
223471
224361
  init_manifest();
223472
224362
  init_util();
223473
224363
  var deriveDesignFlows = (meta, entryScreen) => {
@@ -223530,7 +224420,7 @@ var slug3 = (s2) => s2.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").
223530
224420
  var readFlowFile = (abs, displayName) => {
223531
224421
  let raw;
223532
224422
  try {
223533
- raw = JSON.parse(readFileSync28(abs, "utf8"));
224423
+ raw = JSON.parse(readFileSync30(abs, "utf8"));
223534
224424
  } catch (e2) {
223535
224425
  throw new CliError(`Can't read "${displayName}": ${e2 instanceof Error ? e2.message : String(e2)}`);
223536
224426
  }
@@ -223556,7 +224446,14 @@ var test = async (args) => {
223556
224446
  const desktop2 = flagBool(args, "desktop");
223557
224447
  if (desktop2 && flagBool(args, "flows"))
223558
224448
  throw new CliError("--flows needs the headless renderer (design flows tap by name) — drop --desktop.");
223559
- const native = desktop2 ? await buildBundle(args, undefined, { localAssets: true }) : null;
224449
+ let desktopRoot;
224450
+ try {
224451
+ desktopRoot = findProjectRoot(process.cwd());
224452
+ } catch {
224453
+ desktopRoot = process.cwd();
224454
+ }
224455
+ const desktopRenderer = desktop2 ? resolveDesktopRenderer(desktopRoot, args) : undefined;
224456
+ const native = desktop2 ? await buildBundle(args, undefined, { localAssets: true, desktopRenderer }) : null;
223560
224457
  const headless = desktop2 ? null : await compileHeadlessBundle(args);
223561
224458
  const warnings = headless?.warnings ?? [];
223562
224459
  const useLocalAssets = headless?.useLocalAssets ?? false;
@@ -223578,40 +224475,40 @@ var test = async (args) => {
223578
224475
  if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
223579
224476
  throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
223580
224477
  const projectViewport = desktopDefaultViewport(root);
223581
- const testsDir = resolve15(root, flagStr(args, "dir") ?? "tests");
223582
- const designMetaPath = resolve15(root, flagStr(args, "design-dir") ?? "design", "meta.json");
224478
+ const testsDir = resolve18(root, flagStr(args, "dir") ?? "tests");
224479
+ const designMetaPath = resolve18(root, flagStr(args, "design-dir") ?? "design", "meta.json");
223583
224480
  const asJson = flagBool(args, "json");
223584
224481
  const out = asJson ? logErr : log;
223585
224482
  const cases = [];
223586
224483
  if (args._.length > 0) {
223587
224484
  for (const given of args._) {
223588
- const candidates = [resolve15(given), resolve15(testsDir, given), resolve15(testsDir, `${given}.flow.json`)];
223589
- const abs = candidates.find((p2) => existsSync31(p2));
224485
+ const candidates = [resolve18(given), resolve18(testsDir, given), resolve18(testsDir, `${given}.flow.json`)];
224486
+ const abs = candidates.find((p2) => existsSync34(p2));
223590
224487
  if (!abs)
223591
224488
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
223592
224489
  cases.push(readFlowFile(abs, given));
223593
224490
  }
223594
- } else if (existsSync31(testsDir)) {
223595
- for (const f3 of readdirSync14(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
223596
- cases.push(readFlowFile(resolve15(testsDir, f3), f3));
224491
+ } else if (existsSync34(testsDir)) {
224492
+ for (const f3 of readdirSync15(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
224493
+ cases.push(readFlowFile(resolve18(testsDir, f3), f3));
223597
224494
  }
223598
224495
  }
223599
224496
  const fixturesFlag = flagStr(args, "fixtures");
223600
- const defaultPack = resolve15(testsDir, "fixtures.json");
223601
- const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync31(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
224497
+ const defaultPack = resolve18(testsDir, "fixtures.json");
224498
+ const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync34(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
223602
224499
  const flowsOnly = flagBool(args, "flows");
223603
224500
  const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
223604
224501
  if (flowsOnly)
223605
224502
  cases.length = 0;
223606
224503
  let skipped = [];
223607
224504
  let entryScreen = null;
223608
- if (desktop2 && wantFlows && existsSync31(designMetaPath)) {
224505
+ if (desktop2 && wantFlows && existsSync34(designMetaPath)) {
223609
224506
  note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
223610
224507
  }
223611
- if (!desktop2 && (wantFlows || flowsOnly) && existsSync31(designMetaPath)) {
224508
+ if (!desktop2 && (wantFlows || flowsOnly) && existsSync34(designMetaPath)) {
223612
224509
  let meta;
223613
224510
  try {
223614
- meta = JSON.parse(readFileSync28(designMetaPath, "utf8"));
224511
+ meta = JSON.parse(readFileSync30(designMetaPath, "utf8"));
223615
224512
  } catch (e2) {
223616
224513
  throw new CliError(`Can't read ${designMetaPath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
223617
224514
  }
@@ -223643,17 +224540,18 @@ var test = async (args) => {
223643
224540
  }, timeoutMs);
223644
224541
  killer.unref?.();
223645
224542
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
223646
- const artifactsRoot = resolve15(root, flagStr(args, "artifacts") ?? resolve15(testsDir, ".artifacts"));
224543
+ const artifactsRoot = resolve18(root, flagStr(args, "artifacts") ?? resolve18(testsDir, ".artifacts"));
223647
224544
  const results = [];
223648
224545
  const runNativeCase = async (kase) => {
223649
- const caseDir = resolve15(artifactsRoot, slug3(kase.name));
224546
+ const caseDir = resolve18(artifactsRoot, slug3(kase.name));
223650
224547
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
223651
224548
  for (const st of plan.steps)
223652
224549
  if (st.screenshot)
223653
- mkdirSync22(dirname18(st.screenshot), { recursive: true });
223654
- mkdirSync22(caseDir, { recursive: true });
223655
- const finalPng = resolve15(caseDir, "final.png");
224550
+ mkdirSync24(dirname18(st.screenshot), { recursive: true });
224551
+ mkdirSync24(caseDir, { recursive: true });
224552
+ const finalPng = resolve18(caseDir, "final.png");
223656
224553
  const { output } = await runDesktopRender(native.js, native.resources, {
224554
+ renderer: desktopRenderer,
223657
224555
  width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
223658
224556
  height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
223659
224557
  frames: plan.frameCap,
@@ -223703,14 +224601,14 @@ var test = async (args) => {
223703
224601
  onConsole
223704
224602
  });
223705
224603
  try {
223706
- const io = fileScenarioIO(resolve15(artifactsRoot, slug3(kase.name)));
224604
+ const io = fileScenarioIO(resolve18(artifactsRoot, slug3(kase.name)));
223707
224605
  const outcome = await renderer.runScenario(session, steps, io);
223708
224606
  result = {
223709
224607
  name: kase.name,
223710
224608
  kind: kase.kind,
223711
224609
  ok: outcome.ok,
223712
224610
  steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
223713
- artifacts: outcome.failureArtifacts.map((p2) => resolve15(artifactsRoot, slug3(kase.name), p2))
224611
+ artifacts: outcome.failureArtifacts.map((p2) => resolve18(artifactsRoot, slug3(kase.name), p2))
223714
224612
  };
223715
224613
  } finally {
223716
224614
  session.dispose();
@@ -223778,11 +224676,11 @@ init_config();
223778
224676
  init_manifest();
223779
224677
  init_types4();
223780
224678
  init_util();
223781
- import { resolve as resolve16 } from "node:path";
224679
+ import { resolve as resolve19 } from "node:path";
223782
224680
  var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
223783
224681
  var types4 = async (args) => {
223784
224682
  const dirArg = args._[0];
223785
- const root = dirArg ? resolve16(dirArg) : (() => {
224683
+ const root = dirArg ? resolve19(dirArg) : (() => {
223786
224684
  try {
223787
224685
  return findProjectRoot(process.cwd());
223788
224686
  } catch {
@@ -223841,23 +224739,23 @@ var types4 = async (args) => {
223841
224739
  };
223842
224740
 
223843
224741
  // src/index.ts
223844
- import { existsSync as existsSync33, readFileSync as readFileSync30, realpathSync as realpathSync2 } from "node:fs";
224742
+ import { existsSync as existsSync36, readFileSync as readFileSync32, realpathSync as realpathSync2 } from "node:fs";
223845
224743
  import { createRequire as createRequire4 } from "node:module";
223846
- import { delimiter as delimiter2, dirname as dirname20, join as join33 } from "node:path";
224744
+ import { delimiter as delimiter2, dirname as dirname20, join as join37 } from "node:path";
223847
224745
 
223848
224746
  // src/commands/install.ts
223849
224747
  init_util();
223850
224748
  import { execFileSync as execFileSync5 } from "node:child_process";
223851
- import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync32, mkdirSync as mkdirSync23, readFileSync as readFileSync29, realpathSync, rmSync as rmSync9, symlinkSync } from "node:fs";
223852
- import { homedir as homedir8 } from "node:os";
223853
- import { basename as basename16, delimiter, dirname as dirname19, join as join32, resolve as resolve17, sep as sep2 } from "node:path";
224749
+ import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync35, mkdirSync as mkdirSync25, readFileSync as readFileSync31, realpathSync, rmSync as rmSync10, symlinkSync } from "node:fs";
224750
+ import { homedir as homedir9 } from "node:os";
224751
+ import { basename as basename16, delimiter, dirname as dirname19, join as join36, resolve as resolve20, sep as sep2 } from "node:path";
223854
224752
  var isWin = process.platform === "win32";
223855
- var HOME = homedir8();
224753
+ var HOME = homedir9();
223856
224754
  var safeRealpath = (p2) => {
223857
224755
  try {
223858
224756
  return realpathSync(p2);
223859
224757
  } catch {
223860
- return resolve17(p2);
224758
+ return resolve20(p2);
223861
224759
  }
223862
224760
  };
223863
224761
  var onPath = (dir) => {
@@ -223874,24 +224772,24 @@ var onPath = (dir) => {
223874
224772
  };
223875
224773
  var install = async (args) => {
223876
224774
  const bundle = dirname19(process.execPath);
223877
- const exeName2 = basename16(process.execPath);
224775
+ const exeName3 = basename16(process.execPath);
223878
224776
  const chiselName = isWin ? "chisel.exe" : "chisel";
223879
- if (!existsSync32(join32(bundle, "runtime")) || !existsSync32(join32(bundle, chiselName))) {
224777
+ if (!existsSync35(join36(bundle, "runtime")) || !existsSync35(join36(bundle, chiselName))) {
223880
224778
  throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
223881
224779
  }
223882
- const companions = [exeName2, chiselName, "runtime", "README.txt"].filter((f3) => existsSync32(join32(bundle, f3)));
224780
+ const companions = [exeName3, chiselName, "runtime", "README.txt"].filter((f3) => existsSync35(join36(bundle, f3)));
223883
224781
  if (isWin)
223884
224782
  return installWindows(args, bundle, companions);
223885
224783
  return installUnix(args, bundle, companions);
223886
224784
  };
223887
224785
  var copyBundle = (bundle, target, companions) => {
223888
- mkdirSync23(target, { recursive: true });
224786
+ mkdirSync25(target, { recursive: true });
223889
224787
  if (safeRealpath(target) === safeRealpath(bundle))
223890
224788
  return;
223891
224789
  for (const f3 of companions) {
223892
- const dest = join32(target, f3);
223893
- rmSync9(dest, { recursive: true, force: true });
223894
- cpSync4(join32(bundle, f3), dest, { recursive: true });
224790
+ const dest = join36(target, f3);
224791
+ rmSync10(dest, { recursive: true, force: true });
224792
+ cpSync4(join36(bundle, f3), dest, { recursive: true });
223895
224793
  }
223896
224794
  };
223897
224795
  var permError = (e2, dir, system) => {
@@ -223903,41 +224801,41 @@ var permError = (e2, dir, system) => {
223903
224801
  };
223904
224802
  var installUnix = (args, bundle, companions) => {
223905
224803
  const system = flagBool(args, "system");
223906
- const libDir = resolve17(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join32(HOME, ".local", "lib", "lecodes-cli")));
223907
- const binDir = resolve17(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join32(HOME, ".local", "bin")));
223908
- const link2 = join32(binDir, "lecodes");
224804
+ const libDir = resolve20(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join36(HOME, ".local", "lib", "lecodes-cli")));
224805
+ const binDir = resolve20(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join36(HOME, ".local", "bin")));
224806
+ const link2 = join36(binDir, "lecodes");
223909
224807
  try {
223910
224808
  copyBundle(bundle, libDir, companions);
223911
- chmodSync2(join32(libDir, "lecodes"), 493);
223912
- chmodSync2(join32(libDir, "chisel"), 493);
224809
+ chmodSync2(join36(libDir, "lecodes"), 493);
224810
+ chmodSync2(join36(libDir, "chisel"), 493);
223913
224811
  } catch (e2) {
223914
224812
  throw permError(e2, libDir, system);
223915
224813
  }
223916
224814
  try {
223917
- mkdirSync23(binDir, { recursive: true });
223918
- rmSync9(link2, { force: true });
223919
- symlinkSync(join32(libDir, "lecodes"), link2);
224815
+ mkdirSync25(binDir, { recursive: true });
224816
+ rmSync10(link2, { force: true });
224817
+ symlinkSync(join36(libDir, "lecodes"), link2);
223920
224818
  } catch (e2) {
223921
224819
  throw permError(e2, binDir, system);
223922
224820
  }
223923
224821
  success(`Installed to ${c.bold(libDir)}`);
223924
- log(` ${c.dim(link2 + " → " + join32(libDir, "lecodes"))}`);
224822
+ log(` ${c.dim(link2 + " → " + join36(libDir, "lecodes"))}`);
223925
224823
  finishPathUnix(args, binDir);
223926
224824
  };
223927
224825
  var shellRc = () => {
223928
224826
  const shell = basename16(process.env.SHELL ?? "");
223929
224827
  if (shell === "zsh")
223930
- return { file: join32(HOME, ".zshrc"), name: "~/.zshrc" };
224828
+ return { file: join36(HOME, ".zshrc"), name: "~/.zshrc" };
223931
224829
  if (shell === "bash")
223932
- return { file: join32(HOME, ".bashrc"), name: "~/.bashrc" };
223933
- return { file: join32(HOME, ".profile"), name: "~/.profile" };
224830
+ return { file: join36(HOME, ".bashrc"), name: "~/.bashrc" };
224831
+ return { file: join36(HOME, ".profile"), name: "~/.profile" };
223934
224832
  };
223935
224833
  var PATH_MARKER = "# Added by `lecodes install`";
223936
224834
  var appendPathToShellRc = (binDir) => {
223937
224835
  const { file, name } = shellRc();
223938
224836
  const line = `export PATH="${binDir}:$PATH"`;
223939
224837
  try {
223940
- const existing = existsSync32(file) ? readFileSync29(file, "utf8") : "";
224838
+ const existing = existsSync35(file) ? readFileSync31(file, "utf8") : "";
223941
224839
  if (existing.includes(line))
223942
224840
  return name;
223943
224841
  appendFileSync3(file, `${existing && !existing.endsWith(`
@@ -223975,7 +224873,7 @@ var finishPathUnix = (args, binDir) => {
223975
224873
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
223976
224874
  };
223977
224875
  var installWindows = (args, bundle, companions) => {
223978
- const target = resolve17(flagStr(args, "dir") ?? join32(process.env.LOCALAPPDATA ?? join32(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
224876
+ const target = resolve20(flagStr(args, "dir") ?? join36(process.env.LOCALAPPDATA ?? join36(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
223979
224877
  try {
223980
224878
  copyBundle(bundle, target, companions);
223981
224879
  } catch (e2) {
@@ -224015,7 +224913,7 @@ init_util();
224015
224913
  var STANDALONE = (() => {
224016
224914
  const dir = dirname20(process.execPath);
224017
224915
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
224018
- return existsSync33(join33(dir, "runtime")) && existsSync33(join33(dir, chisel));
224916
+ return existsSync36(join37(dir, "runtime")) && existsSync36(join37(dir, chisel));
224019
224917
  })();
224020
224918
  var INSTALLED = STANDALONE && (() => {
224021
224919
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -224030,8 +224928,8 @@ var INSTALLED = STANDALONE && (() => {
224030
224928
  if (!d)
224031
224929
  return false;
224032
224930
  try {
224033
- const cand = join33(d, exe);
224034
- return existsSync33(cand) && realpathSync2(cand) === self2;
224931
+ const cand = join37(d, exe);
224932
+ return existsSync36(cand) && realpathSync2(cand) === self2;
224035
224933
  } catch {
224036
224934
  return false;
224037
224935
  }
@@ -224102,6 +225000,7 @@ ${c.bold("Commands:")}
224102
225000
  --desktop also open the app in the native desktop host
224103
225001
  (same hot reload; window from app.json's
224104
225002
  desktop block; closing it keeps the server)
225003
+ --fps (--desktop) draw the host's frame counter
224105
225004
  compile Compile the local project to JS (prints to stdout)
224106
225005
  (was 'lecodes preview' — the old name still works)
224107
225006
  -o, --out <file> write to a file instead of stdout
@@ -224110,8 +225009,16 @@ ${c.bold("Commands:")}
224110
225009
  --no-minify keep the bundle readable (no compress/mangle)
224111
225010
  --no-sourcemap don't generate/inline the source map
224112
225011
  --public-url <url> asset URL prefix (default: the API origin)
224113
- render Render the local UI project to a semantic JSON description
225012
+ render [file] Render the local UI project to a semantic JSON description
224114
225013
  (needs the optional 'lecodes-renderer' package; UI projects only)
225014
+ [file] render ONE screen module instead of the app:
225015
+ a file that default-exports a UIScreen (or a
225016
+ (state) => UIScreen) — every 'lecodes design'
225017
+ screen is written that way. A bare id
225018
+ ("login") resolves in design/screens/, and a
225019
+ design screen renders at its meta.json frame
225020
+ --state <name> ([file]) the state to pass a
225021
+ (state) => UIScreen screen
224115
225022
  -o, --out <file> write JSON to a file instead of stdout
224116
225023
  --png [file] rasterize a PNG (default screenshot.png;
224117
225024
  needs @napi-rs/canvas)
@@ -224204,7 +225111,7 @@ ${c.bold("Commands:")}
224204
225111
  add "<text>" [--screen <id>] [--state <s>] [--element <name>]
224205
225112
  reply <number> "<text>" · resolve <number> [--reply "<text>"] [--reopen]
224206
225113
  (--ai signs a write as Claude; sends with the next "lecodes push")
224207
- desktop [run|update] The native desktop host ('lecodes render --desktop' uses it too):
225114
+ desktop [run|build|update] The desktop target — the host 'lecodes render --desktop' uses too:
224208
225115
  no args shows the version status (selected / downloaded /
224209
225116
  newest published)
224210
225117
  run compile the current project and open it in a real
@@ -224214,11 +225121,35 @@ ${c.bold("Commands:")}
224214
225121
  app.json fullscreen; --scale <0.5..4|fit> overrides
224215
225122
  the display scale — "fit" sizes a 1080p-designed app
224216
225123
  to the monitor with the density raised to match;
224217
- --entry overrides the entrypoint; asset() resources
225124
+ --entry overrides the entrypoint; --fps draws the
225125
+ host's frame counter in the corner — composited fps,
225126
+ plus the 3D fps when they diverge; asset() resources
224218
225127
  load from the working tree — --remote-assets or
224219
225128
  --public-url <url> uses the server's copies instead)
225129
+ build stage a portable app folder (default dist/<name>/):
225130
+ the cached host renamed to <Name>.exe + app.js +
225131
+ assets/ (the project's files) + runtime/ (the engine's)
225132
+ + app.json (the host reads title/size/fullscreen from
225133
+ it) + app.ico from app.json "icon". Duplicate asset
225134
+ basenames are a build error (they silently alias in
225135
+ the bundle). Also spelled 'lecodes app build desktop'.
225136
+ -o <dir> output folder (default dist/<name>)
225137
+ --slug <name> folder/zip name override (Latin-only)
225138
+ --renderer <gl|vulkan> host + shader variant
225139
+ --probe headless-render the staged folder to PNG
225140
+ --zip <out>.zip, UTF-8/forward-slash entries
224220
225141
  update move to the newest published release
224221
225142
  (cached in ~/.lecodes/renderer)
225143
+ lightmap bake Bake static shadows + ambient occlusion into a lightmap atlas
225144
+ (assets/lightmap/lightmap.{png,bake}) by running the app in
225145
+ the desktop host: the statics are what the app registers
225146
+ with Lightmap.add, the bake fires at its Lightmap.load call.
225147
+ Props need UVs first: lecodes assets doctor … --lightmap-uv
225148
+ --out <dir> where the atlas lands (assets/lightmap)
225149
+ --size <px> atlas edge (4096) --texel <m> density (0.02)
225150
+ --sun-rays/--ao-rays <n> quality (32 / 64)
225151
+ --ao-distance <m> AO reach (2) --sun-angle <deg> (0.5)
225152
+ --frames <n> load headroom (900) --logs host output
224222
225153
  shaders [compile|update] The project's custom shaders (.mat) and their local compile:
224223
225154
  no args lists the shaders + their compile state; compile
224224
225155
  compiles them all into .lecodes/shaders (the same pass
@@ -224252,6 +225183,11 @@ ${c.bold("Commands:")}
224252
225183
  Skeleton, clips (+ root motion / first-frame-vs-rest per clip), meshes,
224253
225184
  materials, textures, bind sanity, duplicate names; --filmstrip renders
224254
225185
  a CPU-skinned PNG strip per clip (no GPU)
225186
+ assets sky <env.exr|.hdr> [-o dir] [--size N] [--ibl-size N] [--ibl-only]
225187
+ Equirectangular HDR → the two cubemaps a scene needs: sky.ktx (sharp,
225188
+ for 'skybox: { texture }') and ibl.ktx (prefiltered + SH, lights the
225189
+ scene) — they are NOT interchangeable. Reports where the sun is in
225190
+ the environment so the scene's light can be aimed to match
224255
225191
  assets doctor <in.glb|dir> [--fix] [--ktx2] [--dump]
224256
225192
  Analyze / fix a GLB against engine limits (bone cap 256, texture
224257
225193
  formats/size, KTX2 via toktx)
@@ -224300,15 +225236,8 @@ ${c.bold("Commands:")}
224300
225236
  --configuration <c> Release (default) or Debug
224301
225237
  --no-aab skip the .aab (APK only)
224302
225238
  --no-sync skip the lecodes app sync step
224303
- app build desktop Stage a portable desktop app folder (default dist/<name>/):
224304
- the cached host renamed to <Name>.exe + app.js + every asset +
224305
- app.json (the host ≥1.3.0 reads title/size/fullscreen from it)
224306
- + app.ico from app.json "icon". Duplicate asset basenames are
224307
- a build error (they silently alias in the bundle).
224308
- -o <dir> output folder (default dist/<name>)
224309
- --slug <name> folder/zip name override (e.g. Latin-only)
224310
- --probe headless-render the staged folder to a PNG
224311
- --zip <out>.zip, UTF-8/forward-slash entries
225239
+ app build desktop Same as 'lecodes desktop build' (kept for symmetry with the
225240
+ ios/android shells) — see the desktop group above.
224312
225241
  app upload ios Upload the archive to App Store Connect (then bumps buildNumber)
224313
225242
  --key-path <p8> --key-id <id> --issuer-id <id>
224314
225243
  ASC API key; stored in ~/.lecodes/asc.json
@@ -224323,7 +225252,7 @@ ${c.bold("Commands:")}
224323
225252
  — 'new --env' banks it in .env), revoke <id>
224324
225253
 
224325
225254
  Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
224326
- var commands = { init: init2, login, create, clone: clone3, link, status: status3, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn, desktop, shaders, assets };
225255
+ var commands = { init: init2, login, create, clone: clone3, link, status: status3, diff: diff2, pull, push, compile: compile2, preview: previewLegacy, render, test, types: types4, design, scene, dev, app, pn, desktop, shaders, assets, lightmap };
224327
225256
  if (STANDALONE)
224328
225257
  commands.install = install;
224329
225258
  var readVersion = () => package_default.version ?? "unknown";
@@ -224338,9 +225267,9 @@ var peerVersion = (name, probe) => {
224338
225267
  const require2 = createRequire4(import.meta.url);
224339
225268
  let dir = dirname20(require2.resolve(probe));
224340
225269
  for (let depth = 0;depth < 16; depth++) {
224341
- const manifest = join33(dir, "package.json");
224342
- if (existsSync33(manifest)) {
224343
- const parsed = JSON.parse(readFileSync30(manifest, "utf8"));
225270
+ const manifest = join37(dir, "package.json");
225271
+ if (existsSync36(manifest)) {
225272
+ const parsed = JSON.parse(readFileSync32(manifest, "utf8"));
224344
225273
  if (parsed.name === name)
224345
225274
  return parsed.version ?? "unknown";
224346
225275
  }