Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/lib/cad/manifold-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,14 @@ export function empty(): any {
* `runOriginalID`, so every triangle of this part carries it through
* subsequent `.add`/`.subtract`/`.intersect`. After the final boolean,
* `getMesh().runOriginalID` lets the renderer map each triangle back to
* its part and color it (see `part-id.ts`, `analyzeParts`, `builder.ts`).
* its part and color it (see `part-id.ts`, `analyzeAssembly` in
* `server/part-colors.ts`, `builder.ts`).
*
* Injected into the /primitives sandbox as `__tag` and wrapped around each
* recognized named instance by `buildPrimitiveGeom`. A no-op (returns the
* value untouched) on anything that isn't a Manifold — e.g. a
* `resolveProfile(...)` instance that returns a point array — so wrapping
* is always safe.
* Call node by `composition-tree.emitNode` at compose time (K.63 assembly
* path). A no-op (returns the value untouched) on anything that isn't a
* Manifold — e.g. a `resolveProfile(...)` instance that returns a point
* array — so wrapping is always safe.
*/
export function tagManifold(m: any, hashId: number): any {
if (!m || typeof m.getMesh !== 'function') return m;
Expand Down
85 changes: 21 additions & 64 deletions src/lib/server/part-colors.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
/**
* part-colors — build the per-part color lookup for a composite's render.
*
* Pairs with `tagInstanceSources` (primitive-loader): the loader stamps
* each named part with `partHashId(name)`; this derives the matching
* `hashId → color` table the renderer applies after CSG. The hashId is
* the transport (survives boolean ops via Manifold's mesh relation); the
* color is a CLIENT-OWNED, editable prop (`meta.instanceColors[name]`),
* falling back to the deterministic per-name palette.
* The K.63 emit path (composition-tree.ts) stamps each Call node with
* `__tag(<call>, partHashId(alias))` at compose time, so the rendered
* mesh carries the alias's hashId through Manifold's relation. This
* module walks the source's `meta.composition` TreeNode and derives the
* matching `hashId → color` table the renderer applies after CSG.
*
* The OLD primitive-composite branch (recognizeComposite-driven, for
* pre-K.63 `.prim.ts` files with `const X = …` + `return X.add(Y)` chains)
* was deleted with the old composite editor — primitives without
* `meta.composition` fall through to the INACTIVE LUT and the renderer
* uses its material/heuristic path for them.
*
* Role rule (user spec): a part used as a SUBTRACT / INTERSECT tool does
* not own a color — the surface it creates (the cut/bore wall) takes the
* BODY's color, so a cutaway reads as one material with a hole, not a
* differently-colored plug. We implement that by mapping a subtractive
* part's hashId straight to the body color.
*/
import { recognizeComposite } from './recognize-composite';
import { evalMetaLiteral } from './primitives-meta';
import { colorsForInstance, DEFAULT_INNER_COLOR } from '$lib/shared/instance-colors';
import { partHashId } from '$lib/cad/part-id';
Expand Down Expand Up @@ -46,72 +50,25 @@ const INACTIVE: PartColorLUT = {

export function analyzeParts(source: string): PartColorLUT {
// K.63 ASSEMBLY PATH — `.asm.ts` files carry meta.composition (a TreeNode)
// instead of the older `const X = …;` instance declarations
// recognizeComposite scans for. Detect the assembly shape and walk the
// composition tree separately so the per-instance colour swatches in
// CompositionEditor actually drive the bake. Without this branch, the
// K.63 source path 0-instances → INACTIVE → colours are written to
// meta.instanceColors but never applied at render time.
let metaForAsm: any = null;
try { metaForAsm = evalMetaLiteral(source); } catch { /* fall through */ }
if (metaForAsm?.composition && typeof metaForAsm.composition === 'object') {
return analyzeAssembly(metaForAsm);
}

let rec: any;
try { rec = recognizeComposite(source); } catch { return INACTIVE; }
const uses = new Set<string>(rec.uses ?? []);
const instances = (rec.instances ?? []).filter((i: any) => uses.has(i.call));
if (!instances.length) return INACTIVE;

let instanceColors: Record<string, any> = {};
try {
const meta = evalMetaLiteral(source);
if (meta?.instanceColors && typeof meta.instanceColors === 'object') instanceColors = meta.instanceColors;
} catch { /* meta-less / unparseable → palette only */ }

// Roles from the composition chain(s). An operand referenced by name in a
// `.subtract(X)` / `.intersect(X)` is a tool; base + `.add(X)` are additive.
const subtractiveNames = new Set<string>();
const additiveOrder: string[] = [];
const collect = (ops: any[] | undefined) => {
for (const o of ops ?? []) {
if (!o?.name) continue;
if (o.op === 'subtract' || o.op === 'intersect') subtractiveNames.add(o.name);
else additiveOrder.push(o.name); // base (op null) or add
}
};
collect(rec.operands);
for (const k of Object.keys(rec.chains ?? {})) collect(rec.chains[k]);

const instNames = new Set<string>(instances.map((i: any) => i.name));
const bodyName = additiveOrder.find((n) => instNames.has(n)) ?? instances[0].name;
const bodyId = partHashId(bodyName);
const bodyPair = colorsForInstance(bodyName, instanceColors[bodyName]);

const outer: Record<number, string> = {};
const inner: Record<number, string> = {};
const subtractive: number[] = [];
for (const inst of instances) {
const id = partHashId(inst.name);
const c = colorsForInstance(inst.name, instanceColors[inst.name]);
outer[id] = c.outer;
inner[id] = c.inner;
if (subtractiveNames.has(inst.name)) subtractive.push(id);
}
return { outer, inner, subtractive, bodyId, bodyInner: bodyPair.inner, bodyColor: bodyPair.outer, active: true };
// and the loader's emit wraps each Call with `__tag(..., partHashId(alias))`.
// Walk the composition tree to enumerate aliases + their CSG role and
// build the LUT the renderer keys off. Non-assembly sources have no
// meta.composition → INACTIVE (renderer falls back to material colours).
let meta: any = null;
try { meta = evalMetaLiteral(source); } catch { /* unparseable meta — fall through */ }
if (!meta?.composition || typeof meta.composition !== 'object') return INACTIVE;
return analyzeAssembly(meta);
}

/** Per-instance colour LUT for a K.63 .asm.ts source. Each Call node in
* meta.composition is an instance — keyed by the alphabetic alias the
* CompositionEditor assigned (`A`, `B`, `C`, …) — and the alias is
* hashed via partHashId to match the loader's tagInstanceSources stamp.
* hashed via partHashId to match the loader's tag stamp.
*
* Subtract/intersect roles are derived by walking the tree: any Call
* that sits on the `.arg` side of a method node (or as a child of an
* intersect/subtract op anywhere on the tree) is a CSG tool. Tools
* take the body colour on their cut surfaces — same rule the primitive
* composite path uses. */
* take the body colour on their cut surfaces. */
function analyzeAssembly(meta: any): PartColorLUT {
const instanceColors: Record<string, any> =
(meta?.instanceColors && typeof meta.instanceColors === 'object') ? meta.instanceColors : {};
Expand Down
37 changes: 6 additions & 31 deletions src/lib/server/primitive-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,13 @@ import { transformSync } from 'esbuild';
import * as helpers from '$lib/cad/manifold-helpers';
import { SANDBOX_ARG_NAMES, sandboxArgValues } from '$lib/cad/primitive-sandbox';
import { compileProfileBuild } from './profile-fn';
import { recognizeComposite } from './recognize-composite';
import { partHashId } from '$lib/cad/part-id';
import { paramKeysOf } from '$lib/cad/assembly-deps';

/**
* Wrap each recognized named PART instance's init with `__tag(<init>,
* <hashId>)` so its geometry carries a stable source id through CSG
* (color-by-source — see part-id.ts). Operates on the ORIGINAL source
* (recognizer offsets map to it) and splices in reverse offset order so
* earlier edits don't shift later spans. Only "parts" (instances whose
* call is a declared `uses` dependency) are tagged — a `resolveProfile`
* local is skipped, and `__tag` is a no-op on non-Manifolds anyway.
* Returns the source unchanged when recognition isn't position-mapped
* (type-stripped) → falls back to a single uniform color.
*/
function tagInstanceSources(source: string): string {
let rec: any;
try { rec = recognizeComposite(source); } catch { return source; }
if (!rec.editable || !Array.isArray(rec.instances) || !rec.instances.length) return source;
const uses = new Set<string>(rec.uses ?? []);
const targets = rec.instances
.filter((i: any) => uses.has(i.call) && i.initStart >= 0 && i.initEnd > i.initStart)
.sort((a: any, b: any) => b.initStart - a.initStart);
let out = source;
for (const inst of targets) {
const id = partHashId(inst.name);
out = out.slice(0, inst.initStart)
+ `__tag(${out.slice(inst.initStart, inst.initEnd)}, ${id})`
+ out.slice(inst.initEnd);
}
return out;
}
// Color-by-source stamping is owned by the K.63 assembly emit path
// (composition-tree.ts wraps each Call with `__tag(...)` at compose time).
// Old primitive composites (.prim.ts that rely on recognize-composite) no
// longer get per-part tagging in the loader — they fall back to a single
// uniform color through CSG.

type GeomFn = (...args: any[]) => any;

Expand Down Expand Up @@ -154,7 +129,7 @@ export async function buildPrimitiveGeom(
// when there's no volume primitive with this id).
}

let body = transpile(tagInstanceSources(source));
let body = transpile(source);
// ALIAS POLICY — rewrite a dep's call sites in the body to a collision-proof
// alias when any of:
// (a) the body declares a `const/let/var <dep>` shadowing the dep arg
Expand Down
Loading
Loading