reze-engine 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/engine.d.ts +34 -0
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +258 -15
  4. package/dist/gpu-profile.d.ts +19 -0
  5. package/dist/gpu-profile.d.ts.map +1 -0
  6. package/dist/gpu-profile.js +120 -0
  7. package/dist/graph/compile.d.ts +34 -0
  8. package/dist/graph/compile.d.ts.map +1 -0
  9. package/dist/graph/compile.js +299 -0
  10. package/dist/graph/presets/body.d.ts +3 -0
  11. package/dist/graph/presets/body.d.ts.map +1 -0
  12. package/dist/graph/presets/body.js +100 -0
  13. package/dist/graph/presets/cloth_rough.d.ts +3 -0
  14. package/dist/graph/presets/cloth_rough.d.ts.map +1 -0
  15. package/dist/graph/presets/cloth_rough.js +61 -0
  16. package/dist/graph/presets/cloth_smooth.d.ts +3 -0
  17. package/dist/graph/presets/cloth_smooth.d.ts.map +1 -0
  18. package/dist/graph/presets/cloth_smooth.js +53 -0
  19. package/dist/graph/presets/default.d.ts +3 -0
  20. package/dist/graph/presets/default.d.ts.map +1 -0
  21. package/dist/graph/presets/default.js +20 -0
  22. package/dist/graph/presets/hair.d.ts +3 -0
  23. package/dist/graph/presets/hair.d.ts.map +1 -0
  24. package/dist/graph/presets/hair.js +79 -0
  25. package/dist/graph/presets/metal.d.ts +3 -0
  26. package/dist/graph/presets/metal.d.ts.map +1 -0
  27. package/dist/graph/presets/metal.js +58 -0
  28. package/dist/graph/presets/stockings.d.ts +3 -0
  29. package/dist/graph/presets/stockings.d.ts.map +1 -0
  30. package/dist/graph/presets/stockings.js +54 -0
  31. package/dist/graph/registry.d.ts +28 -0
  32. package/dist/graph/registry.d.ts.map +1 -0
  33. package/dist/graph/registry.js +322 -0
  34. package/dist/graph/schema.d.ts +66 -0
  35. package/dist/graph/schema.d.ts.map +1 -0
  36. package/dist/graph/schema.js +6 -0
  37. package/dist/graph/slots.d.ts +14 -0
  38. package/dist/graph/slots.d.ts.map +1 -0
  39. package/dist/graph/slots.js +123 -0
  40. package/dist/index.d.ts +11 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +9 -0
  43. package/dist/physics/profile.d.ts +18 -0
  44. package/dist/physics/profile.d.ts.map +1 -0
  45. package/dist/physics/profile.js +44 -0
  46. package/package.json +2 -1
  47. package/src/engine.ts +290 -15
  48. package/src/graph/compile.ts +342 -0
  49. package/src/graph/presets/body.ts +103 -0
  50. package/src/graph/presets/cloth_rough.ts +64 -0
  51. package/src/graph/presets/cloth_smooth.ts +56 -0
  52. package/src/graph/presets/default.ts +23 -0
  53. package/src/graph/presets/hair.ts +82 -0
  54. package/src/graph/presets/metal.ts +61 -0
  55. package/src/graph/presets/stockings.ts +57 -0
  56. package/src/graph/registry.ts +351 -0
  57. package/src/graph/schema.ts +60 -0
  58. package/src/graph/slots.ts +145 -0
  59. package/src/index.ts +25 -0
  60. package/dist/physics-debug.d.ts +0 -30
  61. package/dist/physics-debug.d.ts.map +0 -1
  62. package/dist/physics-debug.js +0 -526
  63. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  64. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  65. package/dist/shaders/passes/physics-debug.js +0 -69
@@ -0,0 +1,34 @@
1
+ import type { Diagnostic, ExposedParam, StyleGraph } from "./schema";
2
+ export type CompileOptions = {
3
+ /** Fold exposed params to their defaults as consts (no StyleUniforms binding).
4
+ * Used by golden tests and by hosts that don't bind the style buffer. */
5
+ inlineParams?: boolean;
6
+ /** Override the graph output with any node's socket — Blender's node-preview
7
+ * (Ctrl+Shift+Click viewer) workflow for the editor. */
8
+ previewNode?: {
9
+ node: string;
10
+ socket: string;
11
+ };
12
+ };
13
+ /** UBO slot for one exposed param: write `value` at style.p[vec4Index] (+ component). */
14
+ export type StyleSlot = {
15
+ id: string;
16
+ kind: "float" | "color";
17
+ vec4Index: number;
18
+ component?: "x" | "y" | "z" | "w";
19
+ expr: string;
20
+ };
21
+ export type CompileResult = {
22
+ ok: boolean;
23
+ /** Full WGSL module (empty string when !ok). */
24
+ wgsl: string;
25
+ /** Just the generated node section — snapshot tests and editor debugging. */
26
+ fsBody: string;
27
+ slotMap: StyleSlot[];
28
+ diagnostics: Diagnostic[];
29
+ prunedNodes: string[];
30
+ };
31
+ export declare function assignStyleSlots(params: ExposedParam[]): StyleSlot[];
32
+ export declare function validateGraph(graph: StyleGraph, opts?: CompileOptions): Diagnostic[];
33
+ export declare function compileGraph(graph: StyleGraph, opts?: CompileOptions): CompileResult;
34
+ //# sourceMappingURL=compile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/graph/compile.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAA0B,UAAU,EAAE,MAAM,UAAU,CAAA;AAK5F,MAAM,MAAM,cAAc,GAAG;IAC3B;8EAC0E;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAC/C,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,OAAO,CAAA;IACX,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAcD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,EAAE,CAuBpE;AAID,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,GAAE,cAAmB,GAAG,UAAU,EAAE,CAqGxF;AAID,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,GAAE,cAAmB,GAAG,aAAa,CA6JxF"}
@@ -0,0 +1,299 @@
1
+ // Graph → WGSL compiler: validate → prune → toposort → peephole → emit.
2
+ // Deterministic by construction (topo ties broken by node id, literals formatted by
3
+ // shortest round-trip) so the same graph JSON always yields byte-identical WGSL.
4
+ // See docs/graph-compiler-spec.md.
5
+ import { MAX_NODES, MAX_PARAMS } from "./schema";
6
+ import { NODE_REGISTRY, canConvert, convert, fmtValue, literalFits } from "./registry";
7
+ import { assembleModule } from "./slots";
8
+ const ID_RE = /^[a-z0-9_]+$/;
9
+ const err = (message, nodeId) => ({ severity: "error", nodeId, message });
10
+ const warn = (message, nodeId) => ({ severity: "warning", nodeId, message });
11
+ // ─── Param slot assignment ──────────────────────────────────────────
12
+ // Floats pack 4-per-vec4 in declaration order; colors take .rgb of a fresh vec4.
13
+ // Depends only on the params array, never on graph topology — a topology edit
14
+ // doesn't move slider slots.
15
+ const COMPONENTS = ["x", "y", "z", "w"];
16
+ export function assignStyleSlots(params) {
17
+ const slots = [];
18
+ let vec4Index = 0;
19
+ let comp = 0;
20
+ for (const p of params) {
21
+ if (p.kind === "color") {
22
+ if (comp > 0) {
23
+ vec4Index++;
24
+ comp = 0;
25
+ }
26
+ slots.push({ id: p.id, kind: "color", vec4Index, expr: `style.p[${vec4Index}].rgb` });
27
+ vec4Index++;
28
+ }
29
+ else {
30
+ const c = COMPONENTS[comp];
31
+ slots.push({ id: p.id, kind: "float", vec4Index, component: c, expr: `style.p[${vec4Index}].${c}` });
32
+ comp++;
33
+ if (comp === 4) {
34
+ vec4Index++;
35
+ comp = 0;
36
+ }
37
+ }
38
+ }
39
+ return slots;
40
+ }
41
+ // ─── Validation ─────────────────────────────────────────────────────
42
+ export function validateGraph(graph, opts = {}) {
43
+ const d = [];
44
+ const nodes = new Map();
45
+ if (graph.version !== 1)
46
+ d.push(err(`unsupported graph version: ${graph.version}`));
47
+ if (graph.nodes.length > MAX_NODES)
48
+ d.push(err(`graph has ${graph.nodes.length} nodes (max ${MAX_NODES})`));
49
+ for (const node of graph.nodes) {
50
+ if (!ID_RE.test(node.id))
51
+ d.push(err(`invalid node id "${node.id}" (want /^[a-z0-9_]+$/)`, node.id));
52
+ if (nodes.has(node.id))
53
+ d.push(err(`duplicate node id "${node.id}"`, node.id));
54
+ nodes.set(node.id, node);
55
+ const spec = NODE_REGISTRY[node.type];
56
+ if (!spec) {
57
+ d.push(err(`unknown node type "${node.type}"`, node.id));
58
+ continue;
59
+ }
60
+ for (const [socket, value] of Object.entries(node.inputs ?? {})) {
61
+ const input = spec.inputs[socket];
62
+ if (!input)
63
+ d.push(err(`node "${node.id}" (${node.type}) has no input socket "${socket}"`, node.id));
64
+ else if (!literalFits(value, input.type))
65
+ d.push(err(`literal for "${node.id}.${socket}" doesn't fit ${input.type} socket`, node.id));
66
+ }
67
+ }
68
+ const linkedInputs = new Set();
69
+ for (const link of graph.links) {
70
+ const fromNode = nodes.get(link.from.node);
71
+ const toNode = nodes.get(link.to.node);
72
+ if (!fromNode) {
73
+ d.push(err(`link source node "${link.from.node}" doesn't exist`));
74
+ continue;
75
+ }
76
+ if (!toNode) {
77
+ d.push(err(`link target node "${link.to.node}" doesn't exist`));
78
+ continue;
79
+ }
80
+ const fromSpec = NODE_REGISTRY[fromNode.type];
81
+ const toSpec = NODE_REGISTRY[toNode.type];
82
+ if (!fromSpec || !toSpec)
83
+ continue; // unknown type already reported
84
+ const fromT = fromSpec.outputs[link.from.socket];
85
+ if (!fromT) {
86
+ d.push(err(`node "${link.from.node}" (${fromNode.type}) has no output socket "${link.from.socket}"`, link.from.node));
87
+ continue;
88
+ }
89
+ const toInput = toSpec.inputs[link.to.socket];
90
+ if (!toInput) {
91
+ d.push(err(`node "${link.to.node}" (${toNode.type}) has no input socket "${link.to.socket}"`, link.to.node));
92
+ continue;
93
+ }
94
+ if (toInput.type === "vec4") {
95
+ d.push(err(`ramp stop colors must be literals — cannot link into "${link.to.node}.${link.to.socket}"`, link.to.node));
96
+ continue;
97
+ }
98
+ if (!canConvert(fromT, toInput.type))
99
+ d.push(err(`type mismatch: "${link.from.node}.${link.from.socket}" (${fromT}) → "${link.to.node}.${link.to.socket}" (${toInput.type})`, link.to.node));
100
+ const key = `${link.to.node}.${link.to.socket}`;
101
+ if (linkedInputs.has(key))
102
+ d.push(err(`input "${key}" has more than one incoming link`, link.to.node));
103
+ linkedInputs.add(key);
104
+ }
105
+ const params = graph.params ?? [];
106
+ if (params.length > MAX_PARAMS)
107
+ d.push(err(`graph exposes ${params.length} params (max ${MAX_PARAMS})`));
108
+ const paramIds = new Set();
109
+ const paramTargets = new Set();
110
+ for (const p of params) {
111
+ if (paramIds.has(p.id))
112
+ d.push(err(`duplicate param id "${p.id}"`));
113
+ paramIds.add(p.id);
114
+ const node = nodes.get(p.target.node);
115
+ if (!node) {
116
+ d.push(err(`param "${p.id}" targets missing node "${p.target.node}"`));
117
+ continue;
118
+ }
119
+ const spec = NODE_REGISTRY[node.type];
120
+ const input = spec?.inputs[p.target.socket];
121
+ if (!input) {
122
+ d.push(err(`param "${p.id}" targets missing socket "${p.target.node}.${p.target.socket}"`, p.target.node));
123
+ continue;
124
+ }
125
+ const key = `${p.target.node}.${p.target.socket}`;
126
+ if (linkedInputs.has(key))
127
+ d.push(err(`param "${p.id}" targets linked socket "${key}" — sliders only override literals`, p.target.node));
128
+ if (paramTargets.has(key))
129
+ d.push(err(`params target socket "${key}" more than once`, p.target.node));
130
+ paramTargets.add(key);
131
+ const wantKind = p.kind === "color" ? "color" : "float";
132
+ if (input.type !== wantKind)
133
+ d.push(err(`param "${p.id}" is ${p.kind} but socket "${key}" is ${input.type}`, p.target.node));
134
+ if (!literalFits(p.default, input.type))
135
+ d.push(err(`param "${p.id}" default doesn't fit ${input.type}`, p.target.node));
136
+ }
137
+ const out = opts.previewNode ?? graph.output;
138
+ const outNode = nodes.get(out.node);
139
+ const outSpec = outNode ? NODE_REGISTRY[outNode.type] : undefined;
140
+ const outT = outSpec?.outputs[out.socket];
141
+ if (!outT)
142
+ d.push(err(`output "${out.node}.${out.socket}" doesn't resolve`, out.node));
143
+ else if (outT === "vec4")
144
+ d.push(err(`output "${out.node}.${out.socket}" must be color or float`, out.node));
145
+ return d;
146
+ }
147
+ // ─── Compile ────────────────────────────────────────────────────────
148
+ export function compileGraph(graph, opts = {}) {
149
+ const fail = (diagnostics) => ({
150
+ ok: false,
151
+ wgsl: "",
152
+ fsBody: "",
153
+ slotMap: assignStyleSlots(graph.params ?? []),
154
+ diagnostics,
155
+ prunedNodes: [],
156
+ });
157
+ const diagnostics = validateGraph(graph, opts);
158
+ if (diagnostics.some((x) => x.severity === "error"))
159
+ return fail(diagnostics);
160
+ const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
161
+ const spec = (node) => NODE_REGISTRY[node.type];
162
+ // incoming[nodeId.socket] = producer; outgoing edges for toposort
163
+ const incoming = new Map();
164
+ const dependsOn = new Map();
165
+ for (const id of nodes.keys())
166
+ dependsOn.set(id, new Set());
167
+ for (const link of graph.links) {
168
+ incoming.set(`${link.to.node}.${link.to.socket}`, link.from);
169
+ dependsOn.get(link.to.node).add(link.from.node);
170
+ }
171
+ // Prune: reverse-DFS from the (possibly preview-overridden) output.
172
+ const out = opts.previewNode ?? graph.output;
173
+ const reachable = new Set();
174
+ const stack = [out.node];
175
+ while (stack.length) {
176
+ const id = stack.pop();
177
+ if (reachable.has(id))
178
+ continue;
179
+ reachable.add(id);
180
+ for (const dep of dependsOn.get(id))
181
+ stack.push(dep);
182
+ }
183
+ const prunedNodes = [...nodes.keys()].filter((id) => !reachable.has(id)).sort();
184
+ // requiresLink applies to live nodes only.
185
+ for (const id of reachable) {
186
+ const node = nodes.get(id);
187
+ for (const [socket, input] of Object.entries(spec(node).inputs)) {
188
+ if (input.requiresLink && !incoming.has(`${id}.${socket}`) && node.inputs?.[socket] === undefined)
189
+ diagnostics.push(err(`input "${id}.${socket}" must be linked`, id));
190
+ }
191
+ }
192
+ // Exposed params → style slots. Slot assignment is topology-independent; a param
193
+ // whose target got pruned keeps its slot but has no effect (warning).
194
+ const params = graph.params ?? [];
195
+ const slotMap = assignStyleSlots(params);
196
+ const paramBySocket = new Map();
197
+ params.forEach((param, i) => {
198
+ if (!reachable.has(param.target.node))
199
+ diagnostics.push(warn(`param "${param.id}" targets pruned node "${param.target.node}" — slider has no effect`));
200
+ paramBySocket.set(`${param.target.node}.${param.target.socket}`, { param, slot: slotMap[i] });
201
+ });
202
+ if (diagnostics.some((x) => x.severity === "error"))
203
+ return fail(diagnostics);
204
+ // Toposort (Kahn) over reachable nodes; ready set kept sorted for determinism.
205
+ const remaining = new Map();
206
+ for (const id of reachable) {
207
+ const deps = new Set([...dependsOn.get(id)].filter((dep) => reachable.has(dep)));
208
+ remaining.set(id, deps);
209
+ }
210
+ const order = [];
211
+ const ready = [...remaining.entries()]
212
+ .filter(([, deps]) => deps.size === 0)
213
+ .map(([id]) => id)
214
+ .sort();
215
+ while (ready.length) {
216
+ const id = ready.shift();
217
+ order.push(id);
218
+ for (const [other, deps] of remaining) {
219
+ if (deps.delete(id) && deps.size === 0) {
220
+ ready.push(other);
221
+ ready.sort();
222
+ }
223
+ }
224
+ remaining.delete(id);
225
+ }
226
+ if (remaining.size) {
227
+ diagnostics.push(err(`cycle through nodes: ${[...remaining.keys()].sort().join(" → ")}`));
228
+ return fail(diagnostics);
229
+ }
230
+ // ── Emit ──
231
+ const varName = (id) => `n_${id}`;
232
+ const usesStyle = { current: false };
233
+ // Expression for a producer's output socket, converted to the consumer's type.
234
+ const outputExpr = (from, wantT) => {
235
+ const fromNode = nodes.get(from.node);
236
+ const fromSpec = spec(fromNode);
237
+ const fromT = fromSpec.outputs[from.socket];
238
+ const base = fromSpec.contextOutputs
239
+ ? fromSpec.contextOutputs[from.socket]
240
+ : varName(from.node) + (fromSpec.outputSelect?.[from.socket] ?? "");
241
+ return convert(fromT, wantT, base);
242
+ };
243
+ const resolveInput = (node, socket, wantT) => {
244
+ const key = `${node.id}.${socket}`;
245
+ const link = incoming.get(key);
246
+ if (link)
247
+ return { expr: outputExpr(link, wantT) };
248
+ const exposed = paramBySocket.get(key);
249
+ if (exposed && !opts.inlineParams) {
250
+ usesStyle.current = true;
251
+ const raw = exposed.slot.expr;
252
+ return { expr: exposed.param.kind === "float" ? convert("float", wantT, raw) : convert("color", wantT, raw) };
253
+ }
254
+ const inputSpec = spec(node).inputs[socket];
255
+ const value = exposed?.param.default ?? node.inputs?.[socket] ?? inputSpec.default;
256
+ if (value === undefined) {
257
+ if (inputSpec.contextDefault)
258
+ return { expr: inputSpec.contextDefault };
259
+ throw new Error(`unresolved input ${key}`); // guarded by requiresLink validation
260
+ }
261
+ return { expr: fmtValue(value, wantT), literal: value };
262
+ };
263
+ // Peephole canonicalizations — fire only on plain literals (unexposed, unlinked),
264
+ // reproducing the hand-written Safari specializations.
265
+ const emitExpr = (node, args) => {
266
+ const raw = {};
267
+ for (const [k, v] of Object.entries(args))
268
+ raw[k] = v.expr;
269
+ if (node.type === "hue_sat" && args.hue.literal === 0.5)
270
+ return `hue_sat_id(${raw.saturation}, ${raw.value}, ${raw.fac}, ${raw.color})`;
271
+ if (node.type.startsWith("mix/") && node.type !== "mix/add_emit") {
272
+ if (args.fac.literal === 0)
273
+ return raw.a;
274
+ if (node.type === "mix/blend" && args.fac.literal === 1)
275
+ return raw.b;
276
+ }
277
+ if (node.type === "tex_noise" &&
278
+ args.detail.literal === 2 &&
279
+ args.roughness.literal === 0.5 &&
280
+ args.distortion.literal === 0)
281
+ return `tex_noise_d2(${raw.vector}, ${raw.scale})`;
282
+ return spec(node).emit(raw);
283
+ };
284
+ const lines = [];
285
+ for (const id of order) {
286
+ const node = nodes.get(id);
287
+ const s = spec(node);
288
+ if (s.contextOutputs)
289
+ continue;
290
+ const args = {};
291
+ for (const [socket, input] of Object.entries(s.inputs))
292
+ args[socket] = resolveInput(node, socket, input.type);
293
+ lines.push(` let ${varName(id)} = ${emitExpr(node, args)}; // @node:${id}`);
294
+ }
295
+ lines.push(` let final_color = ${outputExpr(out, "color")}; // @node:${out.node}`);
296
+ const fsBody = lines.join("\n");
297
+ const wgsl = assembleModule(graph.slot, fsBody, usesStyle.current);
298
+ return { ok: true, wgsl, fsBody, slotMap, diagnostics, prunedNodes };
299
+ }
@@ -0,0 +1,3 @@
1
+ import type { StyleGraph } from "../schema";
2
+ export declare const BODY_GRAPH: StyleGraph;
3
+ //# sourceMappingURL=body.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/body.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,UAAU,EAAE,UA+FxB,CAAA"}
@@ -0,0 +1,100 @@
1
+ // M_Body as a StyleGraph — port of shaders/materials/body.ts (仿深空之眼 "M_Body").
2
+ // Toon + warm rim + rim1/rim2 stack mixed 50/50 against a Principled BSDF with
3
+ // noise-bumped normal. The hand port's local ramp_ease is ramp_cardinal (identical
4
+ // smoothstep form); the Mapping node's zero loc/rot folds to a scale multiply.
5
+ export const BODY_GRAPH = {
6
+ version: 1,
7
+ name: "Body",
8
+ slot: "body",
9
+ nodes: [
10
+ { id: "tex", type: "texture" },
11
+ { id: "geo", type: "geometry" },
12
+ { id: "str", type: "shader_to_rgb_diffuse" },
13
+ { id: "toon", type: "ramp_constant", inputs: { pos0: 0.0, pos1: 0.2966 } },
14
+ { id: "shadow_tint", type: "hue_sat", inputs: { hue: 0.5, saturation: 2.0, value: 0.3499999940395355, fac: 1.0 } },
15
+ { id: "lit_tint", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.5, value: 1.0, fac: 1.0 } },
16
+ { id: "toon_color", type: "mix/blend" },
17
+ { id: "bc", type: "bright_contrast", inputs: { bright: 0.1, contrast: 0.2 } },
18
+ { id: "emission3", type: "emission", inputs: { strength: 4.0 } },
19
+ { id: "warm_add", type: "math/add", inputs: { b: 0.5 } },
20
+ { id: "warm_clamp", type: "math/clamp01" },
21
+ {
22
+ id: "warm_ramp",
23
+ type: "ramp_cardinal",
24
+ inputs: {
25
+ pos0: 0.2409,
26
+ color0: [0.2426, 0.068, 0.0588, 1.0],
27
+ pos1: 0.4663,
28
+ color1: [0.6677, 0.5024, 0.5126, 1.0],
29
+ },
30
+ },
31
+ { id: "warm_emit", type: "emission", inputs: { strength: 0.30000001192092896 } },
32
+ { id: "rim1_fres", type: "fresnel", inputs: { ior: 2.0 } },
33
+ { id: "rim1_lw", type: "layer_weight/facing", inputs: { blend: 0.24000005424022675 } },
34
+ { id: "rim1_str", type: "math/multiply" },
35
+ { id: "rim1", type: "emission", inputs: { color: [0.984157919883728, 0.6110184788703918, 0.5736401677131653] } },
36
+ { id: "rim2_lw", type: "layer_weight/facing", inputs: { blend: 0.20000000298023224 } },
37
+ { id: "rim2_pow", type: "math/power", inputs: { b: 1.4300000667572021 } },
38
+ { id: "rim2_ramp", type: "ramp_cardinal", inputs: { pos0: 0.0, pos1: 0.5052 } },
39
+ { id: "rim2_mix", type: "mix_shader", inputs: { b: [1.0, 0.4303792119026184, 0.3315804898738861] } },
40
+ { id: "npr_add1", type: "add_shader" },
41
+ { id: "npr_stack", type: "add_shader" },
42
+ { id: "map", type: "mapping", inputs: { scl: [1.0, 1.0, 1.5] } },
43
+ { id: "noise", type: "tex_noise", inputs: { scale: 1.0 } },
44
+ { id: "noise_ramp", type: "ramp_linear", inputs: { pos0: 0.0, pos1: 1.0 } },
45
+ { id: "bump", type: "bump", inputs: { strength: 0.324644535779953 } },
46
+ {
47
+ id: "principled_base",
48
+ type: "mix/blend",
49
+ inputs: { b: [0.6831911206245422, 0.19474034011363983, 0.13732507824897766] },
50
+ },
51
+ { id: "p_emit", type: "emission", inputs: { strength: 0.2 } },
52
+ {
53
+ id: "principled",
54
+ type: "principled",
55
+ inputs: { metallic: 0.0, specular: 0.5, roughness: 0.3, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
56
+ },
57
+ { id: "p_sum", type: "add_shader" },
58
+ { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.5 } },
59
+ ],
60
+ links: [
61
+ { from: { node: "str", socket: "value" }, to: { node: "toon", socket: "fac" } },
62
+ { from: { node: "tex", socket: "color" }, to: { node: "shadow_tint", socket: "color" } },
63
+ { from: { node: "tex", socket: "color" }, to: { node: "lit_tint", socket: "color" } },
64
+ { from: { node: "toon", socket: "fac_out" }, to: { node: "toon_color", socket: "fac" } },
65
+ { from: { node: "shadow_tint", socket: "color" }, to: { node: "toon_color", socket: "a" } },
66
+ { from: { node: "lit_tint", socket: "color" }, to: { node: "toon_color", socket: "b" } },
67
+ { from: { node: "toon_color", socket: "color" }, to: { node: "bc", socket: "color" } },
68
+ { from: { node: "bc", socket: "color" }, to: { node: "emission3", socket: "color" } },
69
+ { from: { node: "toon", socket: "fac_out" }, to: { node: "warm_add", socket: "a" } },
70
+ { from: { node: "warm_add", socket: "value" }, to: { node: "warm_clamp", socket: "a" } },
71
+ { from: { node: "warm_clamp", socket: "value" }, to: { node: "warm_ramp", socket: "fac" } },
72
+ { from: { node: "warm_ramp", socket: "color" }, to: { node: "warm_emit", socket: "color" } },
73
+ { from: { node: "rim1_fres", socket: "value" }, to: { node: "rim1_str", socket: "a" } },
74
+ { from: { node: "rim1_lw", socket: "value" }, to: { node: "rim1_str", socket: "b" } },
75
+ { from: { node: "rim1_str", socket: "value" }, to: { node: "rim1", socket: "strength" } },
76
+ { from: { node: "rim2_lw", socket: "value" }, to: { node: "rim2_pow", socket: "a" } },
77
+ { from: { node: "rim2_pow", socket: "value" }, to: { node: "rim2_ramp", socket: "fac" } },
78
+ { from: { node: "emission3", socket: "color" }, to: { node: "rim2_mix", socket: "a" } },
79
+ { from: { node: "rim2_ramp", socket: "fac_out" }, to: { node: "rim2_mix", socket: "fac" } },
80
+ { from: { node: "rim1", socket: "color" }, to: { node: "npr_add1", socket: "a" } },
81
+ { from: { node: "rim2_mix", socket: "color" }, to: { node: "npr_add1", socket: "b" } },
82
+ { from: { node: "npr_add1", socket: "color" }, to: { node: "npr_stack", socket: "a" } },
83
+ { from: { node: "warm_emit", socket: "color" }, to: { node: "npr_stack", socket: "b" } },
84
+ { from: { node: "geo", socket: "rest_pos" }, to: { node: "map", socket: "vector" } },
85
+ { from: { node: "map", socket: "vector" }, to: { node: "noise", socket: "vector" } },
86
+ { from: { node: "noise", socket: "value" }, to: { node: "noise_ramp", socket: "fac" } },
87
+ { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "bump", socket: "height" } },
88
+ { from: { node: "geo", socket: "normal" }, to: { node: "bump", socket: "normal" } },
89
+ { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "principled_base", socket: "fac" } },
90
+ { from: { node: "bc", socket: "color" }, to: { node: "principled_base", socket: "a" } },
91
+ { from: { node: "bc", socket: "color" }, to: { node: "p_emit", socket: "color" } },
92
+ { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base" } },
93
+ { from: { node: "bump", socket: "vector" }, to: { node: "principled", socket: "normal" } },
94
+ { from: { node: "principled", socket: "color" }, to: { node: "p_sum", socket: "a" } },
95
+ { from: { node: "p_emit", socket: "color" }, to: { node: "p_sum", socket: "b" } },
96
+ { from: { node: "npr_stack", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
97
+ { from: { node: "p_sum", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
98
+ ],
99
+ output: { node: "mix_shader_001", socket: "color" },
100
+ };
@@ -0,0 +1,3 @@
1
+ import type { StyleGraph } from "../schema";
2
+ export declare const CLOTH_ROUGH_GRAPH: StyleGraph;
3
+ //# sourceMappingURL=cloth_rough.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloth_rough.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/cloth_rough.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,iBAAiB,EAAE,UAwD/B,CAAA"}
@@ -0,0 +1,61 @@
1
+ // M_Rough_Cloth as a StyleGraph — port of shaders/materials/cloth_rough.ts.
2
+ // NPR graph identical to M_Smooth_Cloth, but the noise bump subtree IS live on
3
+ // Principled.Normal (weave bump in rest space) and Roughness is raised to 0.8187.
4
+ // The tex_noise node hits the detail=2 peephole → tex_noise_d2.
5
+ export const CLOTH_ROUGH_GRAPH = {
6
+ version: 1,
7
+ name: "Rough Cloth",
8
+ slot: "cloth_rough",
9
+ nodes: [
10
+ { id: "tex", type: "texture" },
11
+ { id: "geo", type: "geometry" },
12
+ { id: "str", type: "shader_to_rgb_diffuse" },
13
+ { id: "ramp_008", type: "ramp_constant_aa", inputs: { edge: 0.2966 } },
14
+ { id: "mix04_fac", type: "math/multiply", inputs: { b: 0.5 } },
15
+ { id: "dark_tex", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.19999998807907104, fac: 1.0 } },
16
+ { id: "mix_004", type: "mix/blend" },
17
+ { id: "sep_n", type: "separate_xyz" },
18
+ { id: "bevel_clamp", type: "math/clamp01" },
19
+ { id: "mix_003", type: "mix/blend" },
20
+ { id: "hue_004", type: "hue_sat", inputs: { hue: 0.5, saturation: 0.800000011920929, value: 2.0, fac: 1.0 } },
21
+ { id: "npr_overlay", type: "mix/overlay", inputs: { fac: 1.0 } },
22
+ { id: "npr_emit", type: "emission", inputs: { strength: 18.200000762939453 } },
23
+ { id: "noise", type: "tex_noise", inputs: { scale: 17.7 } },
24
+ { id: "noise_ramp", type: "ramp_linear", inputs: { pos0: 0.0, pos1: 1.0 } },
25
+ { id: "bump", type: "bump", inputs: { strength: 1.0 } },
26
+ { id: "principled_base", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.800000011920929, fac: 1.0 } },
27
+ {
28
+ id: "principled",
29
+ type: "principled",
30
+ inputs: { metallic: 0.0, specular: 0.8, roughness: 0.8187, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
31
+ },
32
+ { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.8999999761581421 } },
33
+ ],
34
+ links: [
35
+ { from: { node: "str", socket: "value" }, to: { node: "ramp_008", socket: "fac" } },
36
+ { from: { node: "ramp_008", socket: "fac_out" }, to: { node: "mix04_fac", socket: "a" } },
37
+ { from: { node: "tex", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
38
+ { from: { node: "mix04_fac", socket: "value" }, to: { node: "mix_004", socket: "fac" } },
39
+ { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_004", socket: "a" } },
40
+ { from: { node: "tex", socket: "color" }, to: { node: "mix_004", socket: "b" } },
41
+ { from: { node: "geo", socket: "normal" }, to: { node: "sep_n", socket: "vector" } },
42
+ { from: { node: "sep_n", socket: "y" }, to: { node: "bevel_clamp", socket: "a" } },
43
+ { from: { node: "bevel_clamp", socket: "value" }, to: { node: "mix_003", socket: "fac" } },
44
+ { from: { node: "mix_004", socket: "color" }, to: { node: "mix_003", socket: "a" } },
45
+ { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_003", socket: "b" } },
46
+ { from: { node: "mix_003", socket: "color" }, to: { node: "hue_004", socket: "color" } },
47
+ { from: { node: "mix_003", socket: "color" }, to: { node: "npr_overlay", socket: "a" } },
48
+ { from: { node: "hue_004", socket: "color" }, to: { node: "npr_overlay", socket: "b" } },
49
+ { from: { node: "npr_overlay", socket: "color" }, to: { node: "npr_emit", socket: "color" } },
50
+ { from: { node: "geo", socket: "rest_pos" }, to: { node: "noise", socket: "vector" } },
51
+ { from: { node: "noise", socket: "value" }, to: { node: "noise_ramp", socket: "fac" } },
52
+ { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "bump", socket: "height" } },
53
+ { from: { node: "geo", socket: "normal" }, to: { node: "bump", socket: "normal" } },
54
+ { from: { node: "tex", socket: "color" }, to: { node: "principled_base", socket: "color" } },
55
+ { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base" } },
56
+ { from: { node: "bump", socket: "vector" }, to: { node: "principled", socket: "normal" } },
57
+ { from: { node: "npr_emit", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
58
+ { from: { node: "principled", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
59
+ ],
60
+ output: { node: "mix_shader_001", socket: "color" },
61
+ };
@@ -0,0 +1,3 @@
1
+ import type { StyleGraph } from "../schema";
2
+ export declare const CLOTH_SMOOTH_GRAPH: StyleGraph;
3
+ //# sourceMappingURL=cloth_smooth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloth_smooth.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/cloth_smooth.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,kBAAkB,EAAE,UAgDhC,CAAA"}
@@ -0,0 +1,53 @@
1
+ // M_Smooth_Cloth as a StyleGraph — port of shaders/materials/cloth_smooth.ts.
2
+ // NPR toon + bevel + overlay-boosted emission (18.2×) mixed 10/90 against a plain
3
+ // Principled BSDF. The Blender graph's dead bump subtree is omitted (as in the hand
4
+ // port). hue_sat nodes with hue=0.5 compile to the hue_sat_id specialization.
5
+ export const CLOTH_SMOOTH_GRAPH = {
6
+ version: 1,
7
+ name: "Smooth Cloth",
8
+ slot: "cloth_smooth",
9
+ nodes: [
10
+ { id: "tex", type: "texture" },
11
+ { id: "geo", type: "geometry" },
12
+ { id: "str", type: "shader_to_rgb_diffuse" },
13
+ { id: "ramp_008", type: "ramp_constant_aa", inputs: { edge: 0.2966 } },
14
+ { id: "mix04_fac", type: "math/multiply", inputs: { b: 0.5 } },
15
+ { id: "dark_tex", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.19999998807907104, fac: 1.0 } },
16
+ { id: "mix_004", type: "mix/blend" },
17
+ { id: "sep_n", type: "separate_xyz" },
18
+ { id: "bevel_clamp", type: "math/clamp01" },
19
+ { id: "mix_003", type: "mix/blend" },
20
+ { id: "hue_004", type: "hue_sat", inputs: { hue: 0.5, saturation: 0.800000011920929, value: 2.0, fac: 1.0 } },
21
+ { id: "npr_overlay", type: "mix/overlay", inputs: { fac: 1.0 } },
22
+ { id: "npr_emit", type: "emission", inputs: { strength: 18.200000762939453 } },
23
+ { id: "principled_base", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.800000011920929, fac: 1.0 } },
24
+ {
25
+ id: "principled",
26
+ type: "principled",
27
+ inputs: { metallic: 0.0, specular: 0.8, roughness: 0.5, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
28
+ },
29
+ { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.8999999761581421 } },
30
+ ],
31
+ links: [
32
+ { from: { node: "str", socket: "value" }, to: { node: "ramp_008", socket: "fac" } },
33
+ { from: { node: "ramp_008", socket: "fac_out" }, to: { node: "mix04_fac", socket: "a" } },
34
+ { from: { node: "tex", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
35
+ { from: { node: "mix04_fac", socket: "value" }, to: { node: "mix_004", socket: "fac" } },
36
+ { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_004", socket: "a" } },
37
+ { from: { node: "tex", socket: "color" }, to: { node: "mix_004", socket: "b" } },
38
+ { from: { node: "geo", socket: "normal" }, to: { node: "sep_n", socket: "vector" } },
39
+ { from: { node: "sep_n", socket: "y" }, to: { node: "bevel_clamp", socket: "a" } },
40
+ { from: { node: "bevel_clamp", socket: "value" }, to: { node: "mix_003", socket: "fac" } },
41
+ { from: { node: "mix_004", socket: "color" }, to: { node: "mix_003", socket: "a" } },
42
+ { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_003", socket: "b" } },
43
+ { from: { node: "mix_003", socket: "color" }, to: { node: "hue_004", socket: "color" } },
44
+ { from: { node: "mix_003", socket: "color" }, to: { node: "npr_overlay", socket: "a" } },
45
+ { from: { node: "hue_004", socket: "color" }, to: { node: "npr_overlay", socket: "b" } },
46
+ { from: { node: "npr_overlay", socket: "color" }, to: { node: "npr_emit", socket: "color" } },
47
+ { from: { node: "tex", socket: "color" }, to: { node: "principled_base", socket: "color" } },
48
+ { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base" } },
49
+ { from: { node: "npr_emit", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
50
+ { from: { node: "principled", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
51
+ ],
52
+ output: { node: "mix_shader_001", socket: "color" },
53
+ };
@@ -0,0 +1,3 @@
1
+ import type { StyleGraph } from "../schema";
2
+ export declare const DEFAULT_GRAPH: StyleGraph;
3
+ //# sourceMappingURL=default.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/default.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,aAAa,EAAE,UAc3B,CAAA"}
@@ -0,0 +1,20 @@
1
+ // Default material as a StyleGraph — Blender's new-material template verbatim:
2
+ // Image Texture → Principled BSDF (Metallic 0, Specular 0.5, Roughness 0.5) → Output.
3
+ // This is both the port of shaders/materials/default.ts (must match it pixel-for-pixel;
4
+ // see tests/graph.test.mjs) and the blank-canvas starter graph an editor offers when
5
+ // the user creates a new style.
6
+ export const DEFAULT_GRAPH = {
7
+ version: 1,
8
+ name: "Principled BSDF",
9
+ slot: "default",
10
+ nodes: [
11
+ { id: "tex", type: "texture" },
12
+ {
13
+ id: "principled",
14
+ type: "principled",
15
+ inputs: { metallic: 0.0, specular: 0.5, roughness: 0.5, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
16
+ },
17
+ ],
18
+ links: [{ from: { node: "tex", socket: "color" }, to: { node: "principled", socket: "base" } }],
19
+ output: { node: "principled", socket: "color" },
20
+ };
@@ -0,0 +1,3 @@
1
+ import type { StyleGraph } from "../schema";
2
+ export declare const HAIR_GRAPH: StyleGraph;
3
+ //# sourceMappingURL=hair.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hair.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/hair.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,UAAU,EAAE,UAmExB,CAAA"}