@multiplekex/shallot 0.1.6 → 0.1.9

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.
@@ -0,0 +1,94 @@
1
+ import { resource, type State } from "../../core";
2
+ import type { ComputeNode, ExecutionContext } from "../compute";
3
+ import { MASK_FORMAT } from "./scene";
4
+
5
+ export interface DrawContext {
6
+ readonly device: GPUDevice;
7
+ readonly format: GPUTextureFormat;
8
+ readonly maskFormat: GPUTextureFormat;
9
+ }
10
+
11
+ export interface DrawContributor {
12
+ readonly id: string;
13
+ readonly order: number;
14
+ draw(pass: GPURenderPassEncoder, ctx: DrawContext): void;
15
+ }
16
+
17
+ export interface TransparentPassState {
18
+ contributors: Map<string, DrawContributor>;
19
+ }
20
+
21
+ export const TransparentPass = resource<TransparentPassState>("transparent-pass");
22
+
23
+ export function registerDrawContributor(state: State, contributor: DrawContributor): void {
24
+ const pass = TransparentPass.from(state);
25
+ if (pass) {
26
+ pass.contributors.set(contributor.id, contributor);
27
+ }
28
+ }
29
+
30
+ export function unregisterDrawContributor(state: State, id: string): void {
31
+ const pass = TransparentPass.from(state);
32
+ if (pass) {
33
+ pass.contributors.delete(id);
34
+ }
35
+ }
36
+
37
+ export interface TransparentNodeConfig {
38
+ getContributors: () => DrawContributor[];
39
+ }
40
+
41
+ export function createTransparentNode(config: TransparentNodeConfig): ComputeNode {
42
+ return {
43
+ id: "transparent",
44
+ phase: "transparent",
45
+ inputs: [{ id: "depth", access: "read" }],
46
+ outputs: [
47
+ { id: "scene", access: "write" },
48
+ { id: "mask", access: "write" },
49
+ ],
50
+
51
+ execute(ctx: ExecutionContext) {
52
+ const contributors = config.getContributors();
53
+ if (contributors.length === 0) return;
54
+
55
+ const targetView = ctx.getTextureView("scene") ?? ctx.canvasView;
56
+ const depthView = ctx.getTextureView("depth")!;
57
+ const maskView = ctx.getTextureView("mask")!;
58
+
59
+ const pass = ctx.encoder.beginRenderPass({
60
+ colorAttachments: [
61
+ {
62
+ view: targetView,
63
+ loadOp: "load" as const,
64
+ storeOp: "store" as const,
65
+ },
66
+ {
67
+ view: maskView,
68
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
69
+ loadOp: "clear" as const,
70
+ storeOp: "store" as const,
71
+ },
72
+ ],
73
+ depthStencilAttachment: {
74
+ view: depthView,
75
+ depthLoadOp: "load" as const,
76
+ depthStoreOp: "store" as const,
77
+ },
78
+ });
79
+
80
+ const drawCtx: DrawContext = {
81
+ device: ctx.device,
82
+ format: ctx.format,
83
+ maskFormat: MASK_FORMAT,
84
+ };
85
+
86
+ const sorted = [...contributors].sort((a, b) => a.order - b.order);
87
+ for (const contributor of sorted) {
88
+ contributor.draw(pass, drawCtx);
89
+ }
90
+
91
+ pass.end();
92
+ },
93
+ };
94
+ }
@@ -1,6 +1,6 @@
1
1
  import { Pair } from "bitecs";
2
2
  import { setTraits } from "../../core/component";
3
- import { ChildOf, type State, type ParseContext } from "../../core";
3
+ import { ChildOf, type State, type PostLoadContext } from "../../core";
4
4
  import { Tween, TweenState, ensureResolved, captureFromValue } from "./tween";
5
5
 
6
6
  const compareNumbers = (a: number, b: number) => a - b;
@@ -35,7 +35,7 @@ setTraits(Sequence, {
35
35
  }),
36
36
  });
37
37
 
38
- export function finalizeSequences(_state: State, _context: ParseContext): void {}
38
+ export function finalizeSequences(_state: State, _context: PostLoadContext): void {}
39
39
 
40
40
  function getChildrenSorted(state: State, parentEid: number): number[] {
41
41
  childrenBuffer.length = 0;
@@ -3,8 +3,8 @@ import {
3
3
  registerPostLoadHook,
4
4
  type State,
5
5
  type System,
6
- type ParseContext,
7
6
  type Plugin,
7
+ type PostLoadContext,
8
8
  } from "../../core";
9
9
  import { setTraits } from "../../core/component";
10
10
  import { getEasingIndex, getEasing } from "./easing";
@@ -57,7 +57,7 @@ setTraits(Tween, {
57
57
  delay: 0,
58
58
  easingIndex: 0,
59
59
  }),
60
- adapter: (attrs: Record<string, string>, state: State, context: ParseContext) => {
60
+ adapter: (attrs: Record<string, string>, eid: number) => {
61
61
  const parsed = parseTweenAttrs(attrs);
62
62
  const result: Record<string, number> = {};
63
63
 
@@ -66,7 +66,7 @@ setTraits(Tween, {
66
66
  if (parsed.easing) result.easingIndex = getEasingIndex(parsed.easing);
67
67
 
68
68
  if (parsed.target) {
69
- setupTweenFromXml(state, context, parsed, context.currentEid);
69
+ setupTweenFromXml(parsed, eid);
70
70
  }
71
71
 
72
72
  return result;
@@ -110,12 +110,7 @@ interface PendingTween {
110
110
 
111
111
  let pendingXmlTweens: PendingTween[] = [];
112
112
 
113
- function setupTweenFromXml(
114
- _state: State,
115
- _context: ParseContext,
116
- attrs: Record<string, string>,
117
- tweenEid: number
118
- ): void {
113
+ function setupTweenFromXml(attrs: Record<string, string>, tweenEid: number): void {
119
114
  pendingXmlTweens.push({
120
115
  tweenEid,
121
116
  target: attrs.target,
@@ -123,7 +118,7 @@ function setupTweenFromXml(
123
118
  });
124
119
  }
125
120
 
126
- export function finalizePendingTweens(state: State, context: ParseContext): void {
121
+ export function finalizePendingTweens(state: State, context: PostLoadContext): void {
127
122
  for (const pending of pendingXmlTweens) {
128
123
  const parsed = parseTargetPath(pending.target);
129
124
  if (!parsed) continue;
@@ -135,7 +130,14 @@ export function finalizePendingTweens(state: State, context: ParseContext): void
135
130
  if (!binding) continue;
136
131
 
137
132
  state.addRelation(pending.tweenEid, TweenTarget, targetEid);
138
- Tween.to[pending.tweenEid] = parseFloat(pending.to);
133
+ const toValue =
134
+ pending.to.startsWith("0x") || pending.to.startsWith("0X")
135
+ ? parseInt(pending.to, 16)
136
+ : parseFloat(pending.to);
137
+ if (!Number.isFinite(toValue)) {
138
+ throw new Error(`Tween has invalid 'to' value: "${pending.to}" (parsed as ${toValue})`);
139
+ }
140
+ Tween.to[pending.tweenEid] = toValue;
139
141
  }
140
142
  pendingXmlTweens = [];
141
143
  }
@@ -159,8 +161,12 @@ export function ensureResolved(state: State, tweenEid: number): void {
159
161
  const binding = state.getFieldAccessor(tweenEid);
160
162
 
161
163
  if (binding && targetEid >= 0) {
164
+ const toValue = Tween.to[tweenEid];
165
+ if (!Number.isFinite(toValue)) {
166
+ throw new Error(`Tween ${tweenEid} has invalid to value: ${toValue}`);
167
+ }
162
168
  Tween.from[tweenEid] = binding.get(targetEid) ?? 0;
163
- binding.set(targetEid, Tween.to[tweenEid]);
169
+ binding.set(targetEid, toValue);
164
170
  }
165
171
 
166
172
  Tween.elapsed[tweenEid] = duration;
@@ -188,7 +194,13 @@ function updateTweens(state: State, dt: number): void {
188
194
 
189
195
  const elapsed = Tween.elapsed[tweenEid];
190
196
  const duration = Tween.duration[tweenEid];
191
- const rawProgress = Math.min(elapsed / duration, 1);
197
+ const rawProgress = duration <= 0 ? 1 : Math.min(elapsed / duration, 1);
198
+
199
+ if (!Number.isFinite(rawProgress)) {
200
+ throw new Error(
201
+ `Tween ${tweenEid} invalid progress: elapsed=${elapsed}, duration=${duration}, dt=${dt}`
202
+ );
203
+ }
192
204
 
193
205
  const easingFn = getEasing(Tween.easingIndex[tweenEid]);
194
206
  const easedProgress = easingFn(rawProgress);
@@ -197,6 +209,12 @@ function updateTweens(state: State, dt: number): void {
197
209
  const to = Tween.to[tweenEid];
198
210
  const value = from + (to - from) * easedProgress;
199
211
 
212
+ if (!Number.isFinite(value)) {
213
+ throw new Error(
214
+ `Tween ${tweenEid} computed NaN: from=${from}, to=${to}, eased=${easedProgress}, raw=${rawProgress}`
215
+ );
216
+ }
217
+
200
218
  if (binding && targetEid >= 0) {
201
219
  binding.set(targetEid, value);
202
220
  }