@felbrasil/dreamy 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,160 @@
1
1
  "use client";
2
2
 
3
+ // ../viewport/src/math/colors.ts
4
+ import { Color, LinearSRGBColorSpace, Vector3 } from "three";
5
+ var FIXED_COLORS = { tongue: 16021135, teeth: 16514042, black: 1118481 };
6
+ var PALETTE_ORDER = ["primary", "secondary", "detail", "highlight", "eye", "pupil", "custom"];
7
+ function hexOf(ref, palette) {
8
+ if (ref === "tongue" || ref === "teeth" || ref === "black") return FIXED_COLORS[ref];
9
+ return palette[ref] ?? 16711935;
10
+ }
11
+ function cssOf(ref, palette) {
12
+ return cssHex(hexOf(ref, palette));
13
+ }
14
+ var cssHex = (hex) => "#" + (hex & 16777215).toString(16).padStart(6, "0");
15
+ function rawColor(hex, out = new Color()) {
16
+ return out.setRGB((hex >> 16 & 255) / 255, (hex >> 8 & 255) / 255, (hex & 255) / 255, LinearSRGBColorSpace);
17
+ }
18
+ var srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
19
+ function linearVec(hex, out = new Vector3()) {
20
+ return out.set(srgbToLinear((hex >> 16 & 255) / 255), srgbToLinear((hex >> 8 & 255) / 255), srgbToLinear((hex & 255) / 255));
21
+ }
22
+ function linearColor(hex, out = new Color()) {
23
+ const v = linearVec(hex);
24
+ return out.setRGB(v.x, v.y, v.z, LinearSRGBColorSpace);
25
+ }
26
+ function rawVec(hex, out = new Vector3()) {
27
+ return out.set((hex >> 16 & 255) / 255, (hex >> 8 & 255) / 255, (hex & 255) / 255);
28
+ }
29
+ var paletteKey = (p) => PALETTE_ORDER.map((s) => p[s]).join(",");
30
+
31
+ // ../viewport/src/math/pixel-palette.ts
32
+ import { Color as Color2 } from "three";
33
+ function paletteVariants(hex, black) {
34
+ const base = rawColor(hex);
35
+ const hsl = { h: 0, s: 0, l: 0 };
36
+ rgbToHsl(base.r, base.g, base.b, hsl);
37
+ const { h, s, l } = hsl;
38
+ if (l < 0.08 || hex === black) return { base, light: base.clone(), shade: base.clone() };
39
+ const toward = (from, to, k) => {
40
+ let d = to - from;
41
+ d -= Math.round(d);
42
+ return (from + d * k + 1) % 1;
43
+ };
44
+ const light = hslToRaw(toward(h, 0.12, 0.1), s * 0.92, Math.min(0.97, l + 0.16 + (1 - l) * 0.18));
45
+ const shade = hslToRaw(toward(h, 0.7, 0.12), Math.min(1, s * 1.05 + 0.04), l * 0.58);
46
+ const lum = (c) => 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
47
+ if (lum(light) < lum(base)) light.copy(base);
48
+ return { base, light, shade };
49
+ }
50
+ function rgbToHsl(r, g, b, out) {
51
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
52
+ const l = (min + max) / 2;
53
+ let h = 0, s = 0;
54
+ if (min !== max) {
55
+ const d = max - min;
56
+ s = l <= 0.5 ? d / (max + min) : d / (2 - max - min);
57
+ if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
58
+ else if (max === g) h = (b - r) / d + 2;
59
+ else h = (r - g) / d + 4;
60
+ h /= 6;
61
+ }
62
+ out.h = h;
63
+ out.s = s;
64
+ out.l = l;
65
+ }
66
+ function hslToRaw(h, s, l) {
67
+ const hue = (p2, q2, t) => {
68
+ if (t < 0) t += 1;
69
+ if (t > 1) t -= 1;
70
+ if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
71
+ if (t < 1 / 2) return q2;
72
+ if (t < 2 / 3) return p2 + (q2 - p2) * 6 * (2 / 3 - t);
73
+ return p2;
74
+ };
75
+ const c = new Color2();
76
+ if (s === 0) {
77
+ c.r = c.g = c.b = l;
78
+ return c;
79
+ }
80
+ const q = l <= 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
81
+ c.r = hue(p, q, h + 1 / 3);
82
+ c.g = hue(p, q, h);
83
+ c.b = hue(p, q, h - 1 / 3);
84
+ return c;
85
+ }
86
+
87
+ // ../viewport/src/math/sprite-roles.ts
88
+ var SPRITE_SLOTS = [...PALETTE_ORDER, "tongue", "teeth"];
89
+ var ROLE_TRANSPARENT = 0;
90
+ var ROLE_INK = 1;
91
+ var SPRITE_ROLE_COUNT = 2 + SPRITE_SLOTS.length * 3;
92
+ var spriteRole = (slot, tone) => 2 + SPRITE_SLOTS.indexOf(slot) * 3 + tone;
93
+ function roleSlot(role) {
94
+ if (role < 2 || role >= SPRITE_ROLE_COUNT) return void 0;
95
+ const k = role - 2;
96
+ return { slot: SPRITE_SLOTS[Math.floor(k / 3)], tone: k % 3 };
97
+ }
98
+ var toHex = (c) => Math.round(c.r * 255) << 16 | Math.round(c.g * 255) << 8 | Math.round(c.b * 255);
99
+ function spriteColors(palette) {
100
+ const out = [0, FIXED_COLORS.black];
101
+ for (const slot of SPRITE_SLOTS) {
102
+ const hex = slot === "tongue" || slot === "teeth" ? FIXED_COLORS[slot] : palette[slot];
103
+ const v = paletteVariants(hex, FIXED_COLORS.black);
104
+ out.push(toHex(v.base), toHex(v.light), toHex(v.shade));
105
+ }
106
+ return out;
107
+ }
108
+ function decodeRoles(rgba, width, height) {
109
+ const roles = new Uint8Array(width * height);
110
+ let approximated = 0;
111
+ for (let y = 0; y < height; y++) {
112
+ const src = (height - 1 - y) * width * 4, dst = y * width;
113
+ for (let x = 0; x < width; x++) {
114
+ const role = rgba[src + x * 4];
115
+ roles[dst + x] = role < SPRITE_ROLE_COUNT ? role : ROLE_INK;
116
+ if (role && rgba[src + x * 4 + 1] > 127) approximated++;
117
+ }
118
+ }
119
+ return { roles, approximated };
120
+ }
121
+ function spriteAnchor(roles, width, height) {
122
+ let minX = width, maxX = -1, minY = height, maxY = -1;
123
+ for (let y = 0; y < height; y++) {
124
+ for (let x = 0; x < width; x++) {
125
+ if (!roles[y * width + x]) continue;
126
+ if (x < minX) minX = x;
127
+ if (x > maxX) maxX = x;
128
+ if (y < minY) minY = y;
129
+ if (y > maxY) maxY = y;
130
+ }
131
+ }
132
+ if (maxX < 0) return { footY: 0, centerX: 0, centerY: 0 };
133
+ return { footY: maxY, centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2 };
134
+ }
135
+
3
136
  // ../viewport/src/scene/frame-camera.ts
4
- import { Vector3 } from "three";
137
+ import { Quaternion as Q, Vector3 as Vector32 } from "three";
5
138
  var DEFAULT_PADDING = 0.03;
139
+ function viewDirection(view, rotation) {
140
+ const front = new Vector32(0, 0, 1).applyQuaternion(new Q(rotation.x, rotation.y, rotation.z, rotation.w));
141
+ front.y = 0;
142
+ if (front.lengthSq() < 1e-8) front.set(0, 0, 1);
143
+ front.normalize();
144
+ const left = new Vector32(front.z, 0, -front.x);
145
+ const yaw = view.yaw * Math.PI / 180, pitch = Math.max(-89.9, Math.min(89.9, view.pitch)) * Math.PI / 180;
146
+ return front.multiplyScalar(Math.cos(yaw)).addScaledVector(left, Math.sin(yaw)).multiplyScalar(Math.cos(pitch)).add(new Vector32(0, Math.sin(pitch), 0)).normalize();
147
+ }
6
148
  function basis(dir) {
7
149
  const forward = dir.clone().negate();
8
- const right = new Vector3().crossVectors(forward, new Vector3(0, 1, 0));
150
+ const right = new Vector32().crossVectors(forward, new Vector32(0, 1, 0));
9
151
  if (right.lengthSq() < 1e-8) right.set(1, 0, 0);
10
152
  right.normalize();
11
- return { right, up: new Vector3().crossVectors(right, forward).normalize() };
153
+ return { right, up: new Vector32().crossVectors(right, forward).normalize() };
12
154
  }
13
155
  function frameContain(stage, box, points, options = {}) {
14
156
  const { camera, orbit } = stage;
15
- const dir = options.direction ? new Vector3(options.direction.x, options.direction.y, options.direction.z) : camera.position.clone().sub(orbit.target);
157
+ const dir = options.direction ? new Vector32(options.direction.x, options.direction.y, options.direction.z) : camera.position.clone().sub(orbit.target);
16
158
  if (dir.lengthSq() < 1e-10) dir.set(0, 0.35, 1);
17
159
  dir.normalize();
18
160
  const { right, up } = basis(dir);
@@ -20,10 +162,10 @@ function frameContain(stage, box, points, options = {}) {
20
162
  const aspect = options.aspect ?? 1;
21
163
  const keep = 1 - Math.min(0.5, Math.max(0, options.padding ?? DEFAULT_PADDING));
22
164
  const [areaH, areaV] = (tanH / tanV >= aspect ? [tanV * aspect, tanV] : [tanH, tanH / aspect]).map((t) => t * keep);
23
- const center = box.getCenter(new Vector3());
165
+ const center = box.getCenter(new Vector32());
24
166
  let distance = 0;
25
167
  for (let i = 0; i < 8; i++) {
26
- const c = new Vector3(i & 1 ? box.max.x : box.min.x, i & 2 ? box.max.y : box.min.y, i & 4 ? box.max.z : box.min.z).sub(center);
168
+ const c = new Vector32(i & 1 ? box.max.x : box.min.x, i & 2 ? box.max.y : box.min.y, i & 4 ? box.max.z : box.min.z).sub(center);
27
169
  const toward = c.dot(dir);
28
170
  distance = Math.max(distance, Math.abs(c.dot(right)) / areaH + toward, Math.abs(c.dot(up)) / areaV + toward);
29
171
  }
@@ -59,34 +201,6 @@ import { MOUSE, Raycaster as Raycaster2, TOUCH, Vector3 as Vector327 } from "thr
59
201
  // ../viewport/src/decals/decal-atlas.ts
60
202
  import { CanvasTexture, LinearFilter, SRGBColorSpace } from "three";
61
203
 
62
- // ../viewport/src/math/colors.ts
63
- import { Color, LinearSRGBColorSpace, Vector3 as Vector32 } from "three";
64
- var FIXED_COLORS = { tongue: 16021135, teeth: 16514042, black: 1118481 };
65
- var PALETTE_ORDER = ["primary", "secondary", "detail", "highlight", "eye", "pupil", "custom"];
66
- function hexOf(ref, palette) {
67
- if (ref === "tongue" || ref === "teeth" || ref === "black") return FIXED_COLORS[ref];
68
- return palette[ref] ?? 16711935;
69
- }
70
- function cssOf(ref, palette) {
71
- return cssHex(hexOf(ref, palette));
72
- }
73
- var cssHex = (hex) => "#" + (hex & 16777215).toString(16).padStart(6, "0");
74
- function rawColor(hex, out = new Color()) {
75
- return out.setRGB((hex >> 16 & 255) / 255, (hex >> 8 & 255) / 255, (hex & 255) / 255, LinearSRGBColorSpace);
76
- }
77
- var srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
78
- function linearVec(hex, out = new Vector32()) {
79
- return out.set(srgbToLinear((hex >> 16 & 255) / 255), srgbToLinear((hex >> 8 & 255) / 255), srgbToLinear((hex & 255) / 255));
80
- }
81
- function linearColor(hex, out = new Color()) {
82
- const v = linearVec(hex);
83
- return out.setRGB(v.x, v.y, v.z, LinearSRGBColorSpace);
84
- }
85
- function rawVec(hex, out = new Vector32()) {
86
- return out.set((hex >> 16 & 255) / 255, (hex >> 8 & 255) / 255, (hex & 255) / 255);
87
- }
88
- var paletteKey = (p) => PALETTE_ORDER.map((s) => p[s]).join(",");
89
-
90
204
  // ../viewport/src/decals/builtin-docs.ts
91
205
  var BUILTIN_DOCS = {
92
206
  u6: {
@@ -801,7 +915,7 @@ var DecalAtlas = class {
801
915
  };
802
916
 
803
917
  // ../viewport/src/decals/interior-thumbs.ts
804
- import { Color as Color2, DoubleSide, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, WebGLRenderer } from "three";
918
+ import { Color as Color3, DoubleSide, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, WebGLRenderer } from "three";
805
919
 
806
920
  // ../viewport/src/decals/decal-glsl.ts
807
921
  var INTERIOR_GLSL = (
@@ -1111,7 +1225,7 @@ function renderInteriorThumbs(rooms) {
1111
1225
  r.setSize(S, S, false);
1112
1226
  const mat = new ShaderMaterial({
1113
1227
  side: DoubleSide,
1114
- uniforms: { uRoom: { value: 0 }, uDepth: { value: 1 }, uWall: { value: new Color2() }, uSeed: { value: 3 } },
1228
+ uniforms: { uRoom: { value: 0 }, uDepth: { value: 1 }, uWall: { value: new Color3() }, uSeed: { value: 3 } },
1115
1229
  vertexShader: "varying vec2 vP; void main() { vP = position.xy; gl_Position = vec4(position.xy * 2.0, 0.0, 1.0); }",
1116
1230
  fragmentShader: `${INTERIOR_GLSL}
1117
1231
  varying vec2 vP; uniform float uRoom; uniform float uDepth; uniform vec3 uWall; uniform float uSeed;
@@ -1433,14 +1547,14 @@ function writeSegmentMatrices(bones, m, out) {
1433
1547
  const size = bones[i]?.size;
1434
1548
  const th = Math.max(0.02, fs.thick / Math.max(bindFs.thick, 1e-6) * (size?.thickness ?? 1));
1435
1549
  let sL;
1436
- let Q = rel;
1550
+ let Q2 = rel;
1437
1551
  if (segChild >= 0) {
1438
1552
  const seg = new Vector35().setFromMatrixPosition(m.shownGlobal[segChild]).sub(sp);
1439
1553
  const L = seg.length();
1440
1554
  sL = bindLen > 1e-6 ? L / bindLen : 1;
1441
1555
  if (L > 1e-6) {
1442
1556
  const from = axis.clone().applyQuaternion(rel);
1443
- Q = new Quaternion2().setFromUnitVectors(from, seg.divideScalar(L)).multiply(rel);
1557
+ Q2 = new Quaternion2().setFromUnitVectors(from, seg.divideScalar(L)).multiply(rel);
1444
1558
  }
1445
1559
  } else {
1446
1560
  sL = fs.along / Math.max(bindFs.along, 1e-6);
@@ -1465,7 +1579,7 @@ function writeSegmentMatrices(bones, m, out) {
1465
1579
  0,
1466
1580
  1
1467
1581
  );
1468
- mR.makeRotationFromQuaternion(Q).multiply(mS);
1582
+ mR.makeRotationFromQuaternion(Q2).multiply(mS);
1469
1583
  mR.multiply(mT.makeTranslation(-H0.x, -H0.y, -H0.z));
1470
1584
  mR.premultiply(mT.makeTranslation(sp.x, sp.y, sp.z));
1471
1585
  mR.toArray(out, i * 16);
@@ -3859,7 +3973,7 @@ var SelectTool = class {
3859
3973
  };
3860
3974
 
3861
3975
  // ../viewport/src/interaction/selection-boxes.ts
3862
- import { Box3Helper, Box3 as Box34, Color as Color3 } from "three";
3976
+ import { Box3Helper, Box3 as Box34, Color as Color4 } from "three";
3863
3977
  var SelectionBoxes = class {
3864
3978
  constructor(ctx, ui) {
3865
3979
  this.ctx = ctx;
@@ -3873,7 +3987,7 @@ var SelectionBoxes = class {
3873
3987
  const ids = s.mode === "assembly" && !this.ctx.locked() ? s.selection.objectIds : [];
3874
3988
  const groups = ids.map((id) => this.ctx.views.group(id)).filter((g) => !!g);
3875
3989
  while (this.helpers.length < groups.length) {
3876
- const h = new Box3Helper(new Box34(), new Color3(3718648));
3990
+ const h = new Box3Helper(new Box34(), new Color4(3718648));
3877
3991
  this.ui.add(h);
3878
3992
  this.helpers.push(h);
3879
3993
  }
@@ -4531,10 +4645,13 @@ var MeshDriver = class {
4531
4645
  const key = req.key;
4532
4646
  const seq = ++this.seq;
4533
4647
  this.last.set(key, { seq, content: "", resolution: 0 });
4648
+ this.inFlight++;
4534
4649
  this.evaluator.skinWeights(req).then((r) => {
4535
4650
  if (!this.disposed && this.last.get(key)?.seq === seq) done(r);
4536
4651
  }, (e) => {
4537
4652
  if (!this.disposed && !isAbort(e)) console.warn("skin weights failed", key, e);
4653
+ }).finally(() => {
4654
+ this.inFlight--;
4538
4655
  });
4539
4656
  }
4540
4657
  /** Forget a key (the object is gone): pending work is dropped. */
@@ -4750,10 +4867,10 @@ var SceneViews = class {
4750
4867
  };
4751
4868
 
4752
4869
  // ../viewport/src/rig/bone-viz.ts
4753
- import { Box3 as Box37, BufferAttribute as BufferAttribute4, BufferGeometry as BufferGeometry7, Color as Color4, Group as Group6, LineBasicMaterial as LineBasicMaterial3, LineSegments as LineSegments2, Points, PointsMaterial, Sphere, Vector3 as Vector321 } from "three";
4754
- var COL_RIG = new Color4("#34d399");
4755
- var COL_POSE = new Color4("#60a5fa");
4756
- var COL_SEL = new Color4("#fde047");
4870
+ import { Box3 as Box37, BufferAttribute as BufferAttribute4, BufferGeometry as BufferGeometry7, Color as Color5, Group as Group6, LineBasicMaterial as LineBasicMaterial3, LineSegments as LineSegments2, Points, PointsMaterial, Sphere, Vector3 as Vector321 } from "three";
4871
+ var COL_RIG = new Color5("#34d399");
4872
+ var COL_POSE = new Color5("#60a5fa");
4873
+ var COL_SEL = new Color5("#fde047");
4757
4874
  var BoneViz = class {
4758
4875
  root = new Group6();
4759
4876
  line;
@@ -4856,7 +4973,7 @@ import { Vector3 as Vector324 } from "three";
4856
4973
  import {
4857
4974
  ACESFilmicToneMapping,
4858
4975
  AmbientLight,
4859
- Color as Color5,
4976
+ Color as Color6,
4860
4977
  DirectionalLight,
4861
4978
  FogExp2,
4862
4979
  Group as Group7,
@@ -4914,7 +5031,7 @@ function createStage(host, globals2) {
4914
5031
  renderer.domElement.style.touchAction = "none";
4915
5032
  host.appendChild(renderer.domElement);
4916
5033
  const scene = new Scene3();
4917
- scene.background = new Color5(SKY);
5034
+ scene.background = new Color6(SKY);
4918
5035
  scene.fog = new FogExp2(SKY, 0.012);
4919
5036
  const camera = new PerspectiveCamera(50, (host.clientWidth || 1) / (host.clientHeight || 1), 0.1, 1e3);
4920
5037
  camera.position.copy(DEFAULT_CAMERA);
@@ -5062,63 +5179,9 @@ import {
5062
5179
  WebGLRenderTarget
5063
5180
  } from "three";
5064
5181
 
5065
- // ../viewport/src/math/pixel-palette.ts
5066
- import { Color as Color6 } from "three";
5067
- function paletteVariants(hex, black) {
5068
- const base = rawColor(hex);
5069
- const hsl = { h: 0, s: 0, l: 0 };
5070
- rgbToHsl(base.r, base.g, base.b, hsl);
5071
- const { h, s, l } = hsl;
5072
- if (l < 0.08 || hex === black) return { base, light: base.clone(), shade: base.clone() };
5073
- const toward = (from, to, k) => {
5074
- let d = to - from;
5075
- d -= Math.round(d);
5076
- return (from + d * k + 1) % 1;
5077
- };
5078
- const light = hslToRaw(toward(h, 0.12, 0.1), s * 0.92, Math.min(0.97, l + 0.16 + (1 - l) * 0.18));
5079
- const shade = hslToRaw(toward(h, 0.7, 0.12), Math.min(1, s * 1.05 + 0.04), l * 0.58);
5080
- const lum = (c) => 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
5081
- if (lum(light) < lum(base)) light.copy(base);
5082
- return { base, light, shade };
5083
- }
5084
- function rgbToHsl(r, g, b, out) {
5085
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
5086
- const l = (min + max) / 2;
5087
- let h = 0, s = 0;
5088
- if (min !== max) {
5089
- const d = max - min;
5090
- s = l <= 0.5 ? d / (max + min) : d / (2 - max - min);
5091
- if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
5092
- else if (max === g) h = (b - r) / d + 2;
5093
- else h = (r - g) / d + 4;
5094
- h /= 6;
5095
- }
5096
- out.h = h;
5097
- out.s = s;
5098
- out.l = l;
5099
- }
5100
- function hslToRaw(h, s, l) {
5101
- const hue = (p2, q2, t) => {
5102
- if (t < 0) t += 1;
5103
- if (t > 1) t -= 1;
5104
- if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
5105
- if (t < 1 / 2) return q2;
5106
- if (t < 2 / 3) return p2 + (q2 - p2) * 6 * (2 / 3 - t);
5107
- return p2;
5108
- };
5109
- const c = new Color6();
5110
- if (s === 0) {
5111
- c.r = c.g = c.b = l;
5112
- return c;
5113
- }
5114
- const q = l <= 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
5115
- c.r = hue(p, q, h + 1 / 3);
5116
- c.g = hue(p, q, h);
5117
- c.b = hue(p, q, h - 1 / 3);
5118
- return c;
5119
- }
5120
-
5121
5182
  // ../viewport/src/scene/resolve-frag.ts
5183
+ var TEETH_INDEX = 8;
5184
+ var APPROX_DISTANCE = "0.08";
5122
5185
  var RESOLVE_FRAG = (
5123
5186
  /* glsl */
5124
5187
  `
@@ -5144,6 +5207,7 @@ uniform float uTone1;
5144
5207
  uniform float uTone2;
5145
5208
  uniform float uDither;
5146
5209
  uniform float uGlossWhite;
5210
+ uniform float uIndexOut;
5147
5211
  varying vec2 vUv;
5148
5212
 
5149
5213
  // same curves three.js applies to a material's output on screen
@@ -5195,13 +5259,16 @@ bool outlineAt(vec2 p) {
5195
5259
  return false;
5196
5260
  }
5197
5261
 
5198
- vec3 celShade(vec3 c, float gloss, vec2 p) {
5262
+ // The pixel-art choice for a lit pixel, as a sprite role: 1 ink, or 2 + palette index \xD7 3 + tone (0 base, 1 light,
5263
+ // 2 shade); a shiny surface's light tone is teeth-white (the teeth base role).
5264
+ float celRole(vec3 c, float gloss, vec2 p) {
5199
5265
  // Pixels from black decals or unlit black surfaces render directly as solid ink
5200
- if (max(c.r, max(c.g, c.b)) < 0.05) return uInk;
5266
+ if (max(c.r, max(c.g, c.b)) < 0.05) return 1.0;
5201
5267
 
5202
- vec3 base = vec3(0.0), light = vec3(0.0), shade = vec3(0.0);
5268
+ vec3 base = vec3(0.0);
5203
5269
  float bestScore = 1e9;
5204
5270
  float bestS = 1.0;
5271
+ float bestI = 0.0;
5205
5272
  float cl = length(c) + 0.02;
5206
5273
  for (int i = 0; i < ${PIXEL_MAX_PALETTE}; i++) {
5207
5274
  if (i >= uPalCount) break;
@@ -5209,24 +5276,63 @@ vec3 celShade(vec3 c, float gloss, vec2 p) {
5209
5276
  float s = clamp(dot(c, P) / max(dot(P, P), 0.0001), 0.2, 4.0);
5210
5277
  float score = length(c - s * P) / cl + 0.12 * abs(log2(s / 1.25));
5211
5278
  if (score < bestScore) {
5212
- bestScore = score; bestS = s;
5213
- base = uPalBase[i]; light = uPalLight[i]; shade = uPalShade[i];
5279
+ bestScore = score; bestS = s; bestI = float(i);
5280
+ base = uPalBase[i];
5214
5281
  }
5215
5282
  }
5216
5283
 
5217
5284
  // Black from the palette stays flat: no teeth-white specular, no dithering, no stepping
5218
- if (length(base - uInk) < 0.05 || length(base) < 0.10) return uInk;
5285
+ if (length(base - uInk) < 0.05 || length(base) < 0.10) return 1.0;
5219
5286
 
5220
- if (gloss > uGlossWhite) light = uTeeth; // only shiny materials catch teeth-white light
5221
5287
  float tau = log2(bestS) + (bayer4(p) - 0.47) * uDither;
5222
- if (tau > uTone2) return light;
5223
- if (tau > uTone1) return base;
5224
- return shade;
5288
+ if (tau > uTone2) return (gloss > uGlossWhite) ? ${2 + TEETH_INDEX * 3}.0 : 2.0 + bestI * 3.0 + 1.0; // only shiny materials catch teeth-white light
5289
+ if (tau > uTone1) return 2.0 + bestI * 3.0;
5290
+ return 2.0 + bestI * 3.0 + 2.0;
5291
+ }
5292
+
5293
+ vec3 roleColor(float role) {
5294
+ if (role < 1.5) return uInk;
5295
+ float k = role - 2.0;
5296
+ float slot = floor(k / 3.0 + 0.001);
5297
+ float tone = k - slot * 3.0;
5298
+ vec3 col = uInk;
5299
+ for (int i = 0; i < ${PIXEL_MAX_PALETTE}; i++) {
5300
+ if (float(i) == slot) col = (tone < 0.5) ? uPalBase[i] : ((tone < 1.5) ? uPalLight[i] : uPalShade[i]);
5301
+ }
5302
+ return col;
5303
+ }
5304
+
5305
+ vec3 celShade(vec3 c, float gloss, vec2 p) { return roleColor(celRole(c, gloss, p)); }
5306
+
5307
+ // A flat (decal) color as the nearest role: x the role, y 1 when the match is not close (art outside the palette)
5308
+ vec2 nearestRole(vec3 c) {
5309
+ if (max(c.r, max(c.g, c.b)) < 0.05) return vec2(1.0, 0.0);
5310
+ float best = length(c - uInk), role = 1.0;
5311
+ for (int i = 0; i < ${PIXEL_MAX_PALETTE}; i++) {
5312
+ if (i >= uPalCount) break;
5313
+ if (length(uPalBase[i] - uInk) < 0.05) continue;
5314
+ float d0 = length(c - uPalBase[i]), d1 = length(c - uPalLight[i]), d2 = length(c - uPalShade[i]);
5315
+ if (d0 < best) { best = d0; role = 2.0 + float(i) * 3.0; }
5316
+ if (d1 < best) { best = d1; role = 2.0 + float(i) * 3.0 + 1.0; }
5317
+ if (d2 < best) { best = d2; role = 2.0 + float(i) * 3.0 + 2.0; }
5318
+ }
5319
+ return vec2(role, best > ${APPROX_DISTANCE} ? 1.0 : 0.0);
5225
5320
  }
5226
5321
 
5227
5322
  void main() {
5228
5323
  vec2 p = floor(vUv * uSize);
5229
5324
  vec4 s = texture2D(tColor, (uPixel > 0.5) ? (p + 0.5) / uSize : vUv);
5325
+ // Sprite bake: the role of each pixel in red (/ 255), "approximated" in green
5326
+ if (uIndexOut > 0.5) {
5327
+ vec2 r = vec2(0.0);
5328
+ if (s.a > 0.05) {
5329
+ if (s.a >= 0.45 && s.a <= 0.50) r = nearestRole(s.rgb);
5330
+ else r = vec2(celRole(s.rgb, (s.a < 0.4) ? 0.0 : clamp((1.0 - s.a) * 2.0, 0.0, 1.0), p), 0.0);
5331
+ }
5332
+ if (outlineAt(p)) r = vec2(1.0, 0.0);
5333
+ gl_FragColor = vec4(r.x / 255.0, r.y, 0.0, 1.0);
5334
+ return;
5335
+ }
5230
5336
  vec3 bg = uBg;
5231
5337
  if (uChecker > 0.5) {
5232
5338
  vec2 g = floor(gl_FragCoord.xy / (9.0 * uDpr));
@@ -5284,7 +5390,8 @@ var RenderPipeline = class {
5284
5390
  uTone1: { value: Math.log2(PIXEL_SHADE_BELOW) },
5285
5391
  uTone2: { value: Math.log2(PIXEL_LIGHT_ABOVE) },
5286
5392
  uDither: { value: PIXEL_DITHER },
5287
- uGlossWhite: { value: PIXEL_GLOSS_WHITE }
5393
+ uGlossWhite: { value: PIXEL_GLOSS_WHITE },
5394
+ uIndexOut: { value: 0 }
5288
5395
  },
5289
5396
  vertexShader: "varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }",
5290
5397
  fragmentShader: RESOLVE_FRAG,
@@ -5304,6 +5411,8 @@ var RenderPipeline = class {
5304
5411
  quadCam = new OrthographicCamera2(-1, 1, 1, -1, 0, 1);
5305
5412
  paletteKey = "";
5306
5413
  size = new Vector24();
5414
+ /** Sprite bakes: the world pass and the role target, kept apart from the view's target. */
5415
+ bake = null;
5307
5416
  render(prefs, palette, keep, isolated) {
5308
5417
  const { renderer, scene, camera } = this.stage;
5309
5418
  const selOnly = prefs.renderSelectedOnly && !!keep && keep.length > 0;
@@ -5414,6 +5523,12 @@ var RenderPipeline = class {
5414
5523
  this.target.depthTexture?.dispose();
5415
5524
  this.target.dispose();
5416
5525
  }
5526
+ this.target = this.worldTarget(w, h, pixel, smooth, half);
5527
+ this.targetKey = key;
5528
+ return this.target;
5529
+ }
5530
+ /** The offscreen world pass: linear, with "kind" in alpha, and a depth texture for pixel-art outlines. */
5531
+ worldTarget(w, h, pixel, smooth, half) {
5417
5532
  const filter = smooth ? LinearFilter2 : NearestFilter2;
5418
5533
  const rt = new WebGLRenderTarget(w, h, {
5419
5534
  minFilter: filter,
@@ -5429,10 +5544,93 @@ var RenderPipeline = class {
5429
5544
  } else if (smooth) {
5430
5545
  rt.samples = 4;
5431
5546
  }
5432
- this.target = rt;
5433
- this.targetKey = key;
5434
5547
  return rt;
5435
5548
  }
5549
+ /**
5550
+ * A sprite bake: `keep` alone (lights stay, ground and helpers go) in pixel art into a `size`-square target, resolved
5551
+ * to sprite roles instead of colors, read back as RGBA bytes (rows from the bottom). The camera must already be
5552
+ * square. Nothing reaches the canvas.
5553
+ */
5554
+ renderSprite(size, palette, keep) {
5555
+ const { renderer, scene, camera } = this.stage;
5556
+ if (this.bake?.size !== size) {
5557
+ this.disposeBake();
5558
+ const r = this.stage.renderer;
5559
+ const half = r.extensions.has("EXT_color_buffer_float") || r.extensions.has("EXT_color_buffer_half_float");
5560
+ const roles2 = new WebGLRenderTarget(size, size, {
5561
+ minFilter: NearestFilter2,
5562
+ magFilter: NearestFilter2,
5563
+ format: RGBAFormat2,
5564
+ type: UnsignedByteType,
5565
+ depthBuffer: false,
5566
+ stencilBuffer: false
5567
+ });
5568
+ this.bake = { size, world: this.worldTarget(size, size, true, false, half), roles: roles2 };
5569
+ }
5570
+ const { world, roles } = this.bake;
5571
+ const prev = {
5572
+ tm: renderer.toneMapping,
5573
+ bg: scene.background,
5574
+ fog: scene.fog,
5575
+ autoClear: renderer.autoClear,
5576
+ clearColor: renderer.getClearColor(new Color7()),
5577
+ clearAlpha: renderer.getClearAlpha()
5578
+ };
5579
+ const hidden = [];
5580
+ const hide = (n) => {
5581
+ if (n.visible) {
5582
+ n.visible = false;
5583
+ hidden.push(n);
5584
+ }
5585
+ };
5586
+ const u = this.quadMat.uniforms;
5587
+ try {
5588
+ hide(this.stage.ui);
5589
+ this.hideUnselected(keep, hide);
5590
+ renderer.toneMapping = NoToneMapping;
5591
+ scene.background = null;
5592
+ scene.fog = null;
5593
+ this.globals.uPixelOut.value = 1;
5594
+ renderer.setRenderTarget(world);
5595
+ renderer.setClearColor(0, 0);
5596
+ renderer.autoClear = true;
5597
+ renderer.render(scene, camera);
5598
+ this.globals.uPixelOut.value = 0;
5599
+ u.tColor.value = world.texture;
5600
+ u.tDepth.value = world.depthTexture;
5601
+ u.uSize.value.set(size, size);
5602
+ u.uNear.value = camera.near;
5603
+ u.uFar.value = camera.far;
5604
+ u.uPixel.value = 1;
5605
+ u.uChecker.value = 0;
5606
+ u.uIndexOut.value = 1;
5607
+ this.setPalette(palette, u);
5608
+ renderer.setRenderTarget(roles);
5609
+ renderer.render(this.quadScene, this.quadCam);
5610
+ const out = new Uint8Array(size * size * 4);
5611
+ renderer.readRenderTargetPixels(roles, 0, 0, size, size, out);
5612
+ return out;
5613
+ } finally {
5614
+ u.uIndexOut.value = 0;
5615
+ this.globals.uPixelOut.value = 0;
5616
+ hidden.forEach((n) => {
5617
+ n.visible = true;
5618
+ });
5619
+ renderer.setRenderTarget(null);
5620
+ renderer.toneMapping = prev.tm;
5621
+ renderer.autoClear = prev.autoClear;
5622
+ renderer.setClearColor(prev.clearColor, prev.clearAlpha);
5623
+ scene.background = prev.bg;
5624
+ scene.fog = prev.fog;
5625
+ }
5626
+ }
5627
+ disposeBake() {
5628
+ if (!this.bake) return;
5629
+ this.bake.world.depthTexture?.dispose();
5630
+ this.bake.world.dispose();
5631
+ this.bake.roles.dispose();
5632
+ this.bake = null;
5633
+ }
5436
5634
  setPalette(palette, u) {
5437
5635
  const hexes = [...PALETTE_ORDER.map((s) => palette[s]), FIXED_COLORS.tongue, FIXED_COLORS.teeth, FIXED_COLORS.black].slice(0, PIXEL_MAX_PALETTE);
5438
5636
  const key = paletteKey(palette);
@@ -5451,6 +5649,7 @@ var RenderPipeline = class {
5451
5649
  rawVec(FIXED_COLORS.black, u.uInk.value);
5452
5650
  }
5453
5651
  dispose() {
5652
+ this.disposeBake();
5454
5653
  this.target?.depthTexture?.dispose();
5455
5654
  this.target?.dispose();
5456
5655
  this.quadMat.dispose();
@@ -5513,6 +5712,11 @@ function stressTestObjects(rnd = Math.random) {
5513
5712
  // ../viewport/src/create-viewport.ts
5514
5713
  var LOD_EVERY = 4;
5515
5714
  var LOD_CHANGES_PER_TICK = 8;
5715
+ var BAKE_SETTLED_CHECKS = 3;
5716
+ var BAKE_POLL_MS = 16;
5717
+ var BAKE_TIMEOUT_MS = 3e4;
5718
+ var BAKE_FOV = 25;
5719
+ var IDENTITY_Q = { x: 0, y: 0, z: 0, w: 1 };
5516
5720
  function createViewport({ evaluator }) {
5517
5721
  let creation = null;
5518
5722
  let session = null;
@@ -5525,6 +5729,7 @@ function createViewport({ evaluator }) {
5525
5729
  let lastDesigns = null;
5526
5730
  let levelsBase = 0;
5527
5731
  let stress = [];
5732
+ const baking = /* @__PURE__ */ new Set();
5528
5733
  const driver = new MeshDriver(evaluator);
5529
5734
  const getAtlas = (palette) => atlas ??= new DecalAtlas(palette);
5530
5735
  const toast = (msg) => callbacks?.toast(msg);
@@ -5658,7 +5863,7 @@ function createViewport({ evaluator }) {
5658
5863
  const c = v.bounds.getCenter(new Vector327()).applyMatrix4(v.group.matrixWorld);
5659
5864
  const size = v.bounds.getSize(new Vector327());
5660
5865
  const px = projectedPx(Math.max(size.x, size.y, size.z) * v.group.matrixWorld.getMaxScaleOnAxis(), cam.position.distanceTo(c), cam.fov, h);
5661
- const want = lodWanted(mm.vctx.levels, px, v.resolution, v === scoped || sel.has(id));
5866
+ const want = lodWanted(mm.vctx.levels, px, v.resolution, v === scoped || sel.has(id) || baking.has(id));
5662
5867
  if (want !== v.resolution) {
5663
5868
  v.setResolution(want);
5664
5869
  changes++;
@@ -5683,7 +5888,7 @@ function createViewport({ evaluator }) {
5683
5888
  const s = session, c = creation;
5684
5889
  const { stage } = mm;
5685
5890
  if (pendingFrame && pendingFrame.ids.every((id) => !mm.views.worldBounds([id]).isEmpty())) {
5686
- frameContain(stage, mm.views.worldBounds(pendingFrame.ids), mm.views.worldPoints(pendingFrame.ids), pendingFrame.options);
5891
+ frameContain(stage, mm.views.worldBounds(pendingFrame.ids), mm.views.worldPoints(pendingFrame.ids), withView(pendingFrame.ids, pendingFrame.options));
5687
5892
  pendingFrame = null;
5688
5893
  }
5689
5894
  mm.vctx.globals.uTime.value = time * 1e-3;
@@ -5883,6 +6088,86 @@ function createViewport({ evaluator }) {
5883
6088
  c.getContext("2d").drawImage(src, 0, 0, c.width, c.height);
5884
6089
  return c.toDataURL("image/png");
5885
6090
  }
6091
+ function withView(ids, options) {
6092
+ if (!options.view) return options;
6093
+ const facing = creation?.objects.find((o) => o.id === ids[0])?.transform.rotation ?? IDENTITY_Q;
6094
+ const d = viewDirection(options.view, facing);
6095
+ return { ...options, direction: { x: d.x, y: d.y, z: d.z } };
6096
+ }
6097
+ function sculptViewsOf(mm, ids) {
6098
+ return ids.flatMap((id) => {
6099
+ const v = mm.views.views.get(id)?.view;
6100
+ return v instanceof RigView ? v.sculptViews() : v instanceof SculptView ? [v] : [];
6101
+ });
6102
+ }
6103
+ function settled(mm, ids) {
6104
+ const finest = mm.vctx.levels[0] ?? 36;
6105
+ const started = performance.now();
6106
+ return new Promise((resolve, reject) => {
6107
+ let calm = 0;
6108
+ const check = () => {
6109
+ if (m !== mm) {
6110
+ reject(new Error("The viewport was disposed during a bake."));
6111
+ return;
6112
+ }
6113
+ const views = sculptViewsOf(mm, ids);
6114
+ const ready = ids.every((id) => !mm.views.worldBounds([id]).isEmpty()) && driver.busy === 0 && views.every((v) => v.resolution === finest);
6115
+ calm = ready ? calm + 1 : 0;
6116
+ if (calm >= BAKE_SETTLED_CHECKS) {
6117
+ resolve();
6118
+ return;
6119
+ }
6120
+ if (performance.now() - started > BAKE_TIMEOUT_MS) {
6121
+ const fine = views.filter((v) => v.resolution === finest).length;
6122
+ reject(new Error(`The objects did not finish meshing in time for the bake (${driver.busy} requests in flight, ${fine} of ${views.length} parts at the finest level).`));
6123
+ return;
6124
+ }
6125
+ setTimeout(check, BAKE_POLL_MS);
6126
+ };
6127
+ setTimeout(check, BAKE_POLL_MS);
6128
+ });
6129
+ }
6130
+ async function bakeSprite(options) {
6131
+ const mm = m;
6132
+ if (!mm || !creation || !session) throw new Error("The viewport is not mounted.");
6133
+ const ids = options.objectIds.filter((id) => mm.views.views.has(id));
6134
+ if (!ids.length) throw new Error("There is nothing to bake.");
6135
+ const size = Math.round(options.size);
6136
+ if (!(size >= 16 && size <= 512)) throw new Error("A sprite is 16 to 512 pixels wide.");
6137
+ ids.forEach((id) => baking.add(id));
6138
+ try {
6139
+ const finest = mm.vctx.levels[0] ?? 36;
6140
+ for (const v of sculptViewsOf(mm, ids)) if (v.resolution !== finest) v.setResolution(finest);
6141
+ await settled(mm, ids);
6142
+ const c = creation;
6143
+ if (!c) throw new Error("The viewport is not mounted.");
6144
+ const { camera, orbit } = mm.stage;
6145
+ const saved = { position: camera.position.clone(), target: orbit.target.clone(), aspect: camera.aspect, fov: camera.fov };
6146
+ try {
6147
+ camera.aspect = 1;
6148
+ camera.fov = Math.min(60, Math.max(5, options.fov ?? BAKE_FOV));
6149
+ camera.updateProjectionMatrix();
6150
+ frameContain(mm.stage, mm.views.worldBounds(ids), mm.views.worldPoints(ids), withView(ids, { aspect: 1, padding: options.padding ?? 0.03, view: options.view }));
6151
+ camera.updateMatrixWorld();
6152
+ updateCameraSun(mm.stage, true);
6153
+ for (const v of mm.views.allSculptViews()) v.updateLocalCamera(camera.position);
6154
+ mm.vctx.atlas.flush();
6155
+ const keep = ids.map((id) => mm.views.group(id)).filter((g) => !!g);
6156
+ const { roles, approximated } = decodeRoles(mm.pipeline.renderSprite(size, c.palette, keep), size, size);
6157
+ return { width: size, height: size, roles, colors: spriteColors(c.palette), anchor: spriteAnchor(roles, size, size), approximated };
6158
+ } finally {
6159
+ camera.aspect = saved.aspect;
6160
+ camera.fov = saved.fov;
6161
+ camera.updateProjectionMatrix();
6162
+ camera.position.copy(saved.position);
6163
+ orbit.target.copy(saved.target);
6164
+ camera.lookAt(saved.target);
6165
+ orbit.update();
6166
+ }
6167
+ } finally {
6168
+ ids.forEach((id) => baking.delete(id));
6169
+ }
6170
+ }
5886
6171
  function dispose() {
5887
6172
  const mm = m;
5888
6173
  m = null;
@@ -5918,6 +6203,7 @@ function createViewport({ evaluator }) {
5918
6203
  return m?.views.contentBounds(objectId);
5919
6204
  },
5920
6205
  thumbnail,
6206
+ bakeSprite,
5921
6207
  // Releases the canvas, GPU resources and pending meshes, but stays reusable: React StrictMode (and a
5922
6208
  // remounted page) mounts again after dispose. The evaluator's lifetime belongs to the app.
5923
6209
  dispose() {
@@ -5931,6 +6217,13 @@ function createViewport({ evaluator }) {
5931
6217
  }
5932
6218
 
5933
6219
  export {
6220
+ SPRITE_SLOTS,
6221
+ ROLE_TRANSPARENT,
6222
+ ROLE_INK,
6223
+ SPRITE_ROLE_COUNT,
6224
+ spriteRole,
6225
+ roleSlot,
6226
+ spriteColors,
5934
6227
  createViewport
5935
6228
  };
5936
- //# sourceMappingURL=chunk-VZMBE5MV.js.map
6229
+ //# sourceMappingURL=chunk-5KZP6X65.js.map