copper3d 3.6.4 → 3.6.6
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.
- package/dist/Utils/segmentation/NrrdTools.d.ts +59 -6
- package/dist/Utils/segmentation/NrrdTools.js +69 -9
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/tools/AiAssistTool.d.ts +108 -5
- package/dist/Utils/segmentation/tools/AiAssistTool.js +408 -25
- package/dist/Utils/segmentation/tools/AiAssistTool.js.map +1 -1
- package/dist/bundle.esm.js +495 -36
- package/dist/bundle.umd.js +495 -36
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +59 -6
- package/dist/types/Utils/segmentation/tools/AiAssistTool.d.ts +108 -5
- package/dist/types/index.d.ts +1 -1
- package/package.json +2 -1
package/dist/bundle.esm.js
CHANGED
|
@@ -79116,14 +79116,29 @@ class AiAssistTool extends BaseTool {
|
|
|
79116
79116
|
this.committedVol = null;
|
|
79117
79117
|
this.promptTool = "point";
|
|
79118
79118
|
this.polarity = 1; // 1 = foreground (positive), 0 = background
|
|
79119
|
-
/**
|
|
79120
|
-
|
|
79119
|
+
/** The label value (1-255) the AI currently paints into = the active Segmentation.
|
|
79120
|
+
* Each Segmentation is a label value in the single-channel scratch volume; its
|
|
79121
|
+
* colour lives in the volume's colorMap (set via setSegmentColor). */
|
|
79122
|
+
this.activeLabel = 1;
|
|
79121
79123
|
/** Scribble brush radius (px), user-adjustable via slider. */
|
|
79122
79124
|
this.scribbleSize = 5;
|
|
79123
79125
|
/** Accumulated prompts for the CURRENT slice; reset on slice/axis change. */
|
|
79124
79126
|
this.points = [];
|
|
79125
79127
|
this.scribble = [];
|
|
79128
|
+
/** Lasso contour sent to the backend (densified along the curve at finish). */
|
|
79129
|
+
this.lasso = [];
|
|
79126
79130
|
this.activeSlice = -1;
|
|
79131
|
+
// ── Lasso v2 editing state (discrete clicked vertices → smooth closed curve) ──
|
|
79132
|
+
/** Placed lasso vertices (voxel + screen + polarity). Empty = not editing. */
|
|
79133
|
+
this.lassoVerts = [];
|
|
79134
|
+
/** Undo / redo snapshot stacks of the vertex list (for Ctrl+Z / Ctrl+Y). */
|
|
79135
|
+
this.lassoUndo = [];
|
|
79136
|
+
this.lassoRedo = [];
|
|
79137
|
+
/** Index of the vertex currently hovered (shows a red ✕; click removes it). −1 = none. */
|
|
79138
|
+
this.lassoHoverIdx = -1;
|
|
79139
|
+
/** Double-click detection (a dbl-click finishes the lasso). */
|
|
79140
|
+
this.lastLassoDownTime = 0;
|
|
79141
|
+
this.lastLassoDownPos = { x: 0, y: 0 };
|
|
79127
79142
|
// Drag state (box / scribble) — voxel coords for the prompt
|
|
79128
79143
|
this.dragging = false;
|
|
79129
79144
|
this.dragStart = null;
|
|
@@ -79134,8 +79149,14 @@ class AiAssistTool extends BaseTool {
|
|
|
79134
79149
|
// Live cursor position (screen px) for the scribble brush-size preview ring —
|
|
79135
79150
|
// updated on every hover move so the ring follows the mouse like the paint brush.
|
|
79136
79151
|
this.hoverScreen = null;
|
|
79152
|
+
/** Hide the point seed markers once a prediction has painted (only the mask
|
|
79153
|
+
* should remain). A new point click re-shows them until its mask returns. */
|
|
79154
|
+
this.hidePointMarkers = false;
|
|
79137
79155
|
/** Fired when a prompt gesture completes — app calls backend, then applyMask(). */
|
|
79138
79156
|
this.onPrompt = null;
|
|
79157
|
+
/** Fired whenever the lasso vertex set changes (add/delete/undo/redo/finish/cancel),
|
|
79158
|
+
* so the panel can reactively show the Finish button + vertex count. */
|
|
79159
|
+
this.onLassoChange = null;
|
|
79139
79160
|
this.host = host;
|
|
79140
79161
|
}
|
|
79141
79162
|
// ── Configuration (driven from the panel via NrrdTools) ────────────────────
|
|
@@ -79153,16 +79174,51 @@ class AiAssistTool extends BaseTool {
|
|
|
79153
79174
|
setScribbleSize(size) {
|
|
79154
79175
|
this.scribbleSize = Math.max(1, Math.min(40, Math.round(size)));
|
|
79155
79176
|
}
|
|
79156
|
-
|
|
79157
|
-
|
|
79158
|
-
|
|
79159
|
-
|
|
79160
|
-
|
|
79161
|
-
if (c !== this.
|
|
79177
|
+
/** Select the active Segmentation by its label value (1-255). Switching starts a
|
|
79178
|
+
* fresh prompt set — otherwise accumulated prompts would be re-predicted and
|
|
79179
|
+
* repainted into the new label, recolouring the previous segmentation's region. */
|
|
79180
|
+
setActiveSegment(label) {
|
|
79181
|
+
const c = Math.max(1, Math.min(255, Math.round(label)));
|
|
79182
|
+
if (c !== this.activeLabel)
|
|
79162
79183
|
this.resetPrompts();
|
|
79163
|
-
this.
|
|
79184
|
+
this.activeLabel = c;
|
|
79185
|
+
}
|
|
79186
|
+
getActiveLabel() { return this.activeLabel; }
|
|
79187
|
+
/** Set a Segmentation's colour (its label's entry in the scratch volume colorMap).
|
|
79188
|
+
* The 2D overlay repaints next frame (copper3d's render loop re-reads the map). */
|
|
79189
|
+
setSegmentColor(label, color) {
|
|
79190
|
+
var _a;
|
|
79191
|
+
(_a = this.scratchVol) === null || _a === void 0 ? void 0 : _a.setChannelColor(Math.round(label), color);
|
|
79192
|
+
}
|
|
79193
|
+
/** "New segmentation": freeze the current regions (so they persist) and switch to
|
|
79194
|
+
* a fresh label, exactly like the old "New region" but targeting a new label. */
|
|
79195
|
+
newSegment(label) {
|
|
79196
|
+
this.commitRegion();
|
|
79197
|
+
this.setActiveSegment(label);
|
|
79198
|
+
}
|
|
79199
|
+
/** Delete a Segmentation: erase every voxel painted with its label from BOTH the
|
|
79200
|
+
* live scratch and the frozen (committed) volume, so the region disappears from
|
|
79201
|
+
* the view (otherwise orphaned label voxels would render with the wrong colour).
|
|
79202
|
+
* Goes via setRawData so the volume's version counter bumps (overlay repaints). */
|
|
79203
|
+
clearSegment(label) {
|
|
79204
|
+
const lbl = Math.round(label);
|
|
79205
|
+
const zeroOut = (v) => {
|
|
79206
|
+
if (!v)
|
|
79207
|
+
return;
|
|
79208
|
+
const raw = v.getRawData().slice();
|
|
79209
|
+
let changed = false;
|
|
79210
|
+
for (let i = 0; i < raw.length; i++) {
|
|
79211
|
+
if (raw[i] === lbl) {
|
|
79212
|
+
raw[i] = 0;
|
|
79213
|
+
changed = true;
|
|
79214
|
+
}
|
|
79215
|
+
}
|
|
79216
|
+
if (changed)
|
|
79217
|
+
v.setRawData(raw);
|
|
79218
|
+
};
|
|
79219
|
+
zeroOut(this.scratchVol);
|
|
79220
|
+
zeroOut(this.committedVol);
|
|
79164
79221
|
}
|
|
79165
|
-
getChannel() { return this.channel; }
|
|
79166
79222
|
getScratchVolume() { return this.scratchVol; }
|
|
79167
79223
|
/** Serialize the scratch volume as per-z-slice RLE (only NON-empty slices),
|
|
79168
79224
|
* for persisting to ai_generated_nii_LPS on the backend. Binary (any channel →
|
|
@@ -79204,6 +79260,50 @@ class AiAssistTool extends BaseTool {
|
|
|
79204
79260
|
}
|
|
79205
79261
|
return { axis: "z", width, height, slices: out };
|
|
79206
79262
|
}
|
|
79263
|
+
/** Per-SEGMENTATION serialization for the 3D build: one binary RLE per non-empty
|
|
79264
|
+
* z-slice PER label (no binarize-merge), so the backend writes a multi-label
|
|
79265
|
+
* NIfTI and colours the GLB per segmentation. Includes both live and frozen
|
|
79266
|
+
* regions (both live in scratchVol). */
|
|
79267
|
+
getScratchSegments() {
|
|
79268
|
+
if (!this.scratchVol)
|
|
79269
|
+
return null;
|
|
79270
|
+
const { depth } = this.scratchVol.getDimensions();
|
|
79271
|
+
const byLabel = new Map();
|
|
79272
|
+
let width = 0;
|
|
79273
|
+
let height = 0;
|
|
79274
|
+
for (let z = 0; z < depth; z++) {
|
|
79275
|
+
const { data, width: w, height: h } = this.scratchVol.getSliceUint8(z, "z");
|
|
79276
|
+
width = w;
|
|
79277
|
+
height = h;
|
|
79278
|
+
const labels = new Set();
|
|
79279
|
+
for (let i = 0; i < data.length; i++) {
|
|
79280
|
+
if (data[i])
|
|
79281
|
+
labels.add(data[i]);
|
|
79282
|
+
}
|
|
79283
|
+
for (const label of labels) {
|
|
79284
|
+
const runs = [];
|
|
79285
|
+
let value = 0; // always begin with a 0-run (matches backend rle_decode)
|
|
79286
|
+
let count = 0;
|
|
79287
|
+
for (let i = 0; i < data.length; i++) {
|
|
79288
|
+
const v = data[i] === label ? 1 : 0;
|
|
79289
|
+
if (v === value) {
|
|
79290
|
+
count++;
|
|
79291
|
+
}
|
|
79292
|
+
else {
|
|
79293
|
+
runs.push(count);
|
|
79294
|
+
value = v;
|
|
79295
|
+
count = 1;
|
|
79296
|
+
}
|
|
79297
|
+
}
|
|
79298
|
+
runs.push(count);
|
|
79299
|
+
if (!byLabel.has(label))
|
|
79300
|
+
byLabel.set(label, []);
|
|
79301
|
+
byLabel.get(label).push({ sliceIndex: z, rle: runs });
|
|
79302
|
+
}
|
|
79303
|
+
}
|
|
79304
|
+
const segments = [...byLabel.entries()].map(([label, slices]) => ({ label, slices }));
|
|
79305
|
+
return { axis: "z", width, height, segments };
|
|
79306
|
+
}
|
|
79207
79307
|
// ── Sandbox lifecycle ──────────────────────────────────────────────────────
|
|
79208
79308
|
/** Create (or reuse) the scratch volume sized to the layer grid and register
|
|
79209
79309
|
* it under maskData.volumes['aiScratch']. Snapshots the empty buffer. */
|
|
@@ -79224,14 +79324,10 @@ class AiAssistTool extends BaseTool {
|
|
|
79224
79324
|
this.scratchVol.clear();
|
|
79225
79325
|
}
|
|
79226
79326
|
volumes[AI_SCRATCH_LAYER] = this.scratchVol;
|
|
79227
|
-
//
|
|
79228
|
-
//
|
|
79229
|
-
//
|
|
79230
|
-
|
|
79231
|
-
const c = AI_MASK_CHANNEL_COLORS[ch];
|
|
79232
|
-
if (c)
|
|
79233
|
-
this.scratchVol.setChannelColor(ch, { r: c.r, g: c.g, b: c.b, a: c.a });
|
|
79234
|
-
}
|
|
79327
|
+
// Segmentation colours are now driven per-label from the app (store → useAiAssist
|
|
79328
|
+
// → setSegmentColor) right after enter, so the scratch can carry an arbitrary
|
|
79329
|
+
// number of independently-coloured segmentations. No fixed palette is applied
|
|
79330
|
+
// here. Scoped to THIS volume only — global/clinician palettes are untouched.
|
|
79235
79331
|
this.snapshotData = this.scratchVol.getRawData().slice();
|
|
79236
79332
|
this.committedVol = null; // nothing frozen at session start
|
|
79237
79333
|
this.resetPrompts();
|
|
@@ -79277,10 +79373,26 @@ class AiAssistTool extends BaseTool {
|
|
|
79277
79373
|
resetPrompts() {
|
|
79278
79374
|
this.points = [];
|
|
79279
79375
|
this.scribble = [];
|
|
79376
|
+
this.lasso = [];
|
|
79280
79377
|
this.box = undefined;
|
|
79281
79378
|
this.dragScreenStart = null;
|
|
79282
79379
|
this.dragScreen = null;
|
|
79283
79380
|
this.screenScribble = [];
|
|
79381
|
+
this.clearLassoEditing();
|
|
79382
|
+
}
|
|
79383
|
+
/** Drop the in-progress lasso vertices + undo/redo history (does not touch a
|
|
79384
|
+
* prediction already painted from a finished lasso). */
|
|
79385
|
+
clearLassoEditing() {
|
|
79386
|
+
this.lassoVerts = [];
|
|
79387
|
+
this.lassoUndo = [];
|
|
79388
|
+
this.lassoRedo = [];
|
|
79389
|
+
this.lassoHoverIdx = -1;
|
|
79390
|
+
this.notifyLasso();
|
|
79391
|
+
}
|
|
79392
|
+
/** Tell the app layer the lasso vertex set changed (drives the Finish button). */
|
|
79393
|
+
notifyLasso() {
|
|
79394
|
+
var _a;
|
|
79395
|
+
(_a = this.onLassoChange) === null || _a === void 0 ? void 0 : _a.call(this, this.lassoVerts.length, this.lassoVerts.length > 0);
|
|
79284
79396
|
}
|
|
79285
79397
|
// ── Coordinate mapping (all three views) ───────────────────────────────────
|
|
79286
79398
|
//
|
|
@@ -79341,14 +79453,53 @@ class AiAssistTool extends BaseTool {
|
|
|
79341
79453
|
return null;
|
|
79342
79454
|
return { vx, vy, w: sd.w, h: sd.h };
|
|
79343
79455
|
}
|
|
79456
|
+
/** Voxel-slice (vx,vy) → screen offset (px). Exact inverse of toVoxel, so
|
|
79457
|
+
* markers / lasso curves drawn from voxel coords track pan AND zoom each frame
|
|
79458
|
+
* (the freehand previews store screen px directly; persistent overlays don't). */
|
|
79459
|
+
voxelToScreen(vx, vy) {
|
|
79460
|
+
const sd = this.sliceDims();
|
|
79461
|
+
if (!sd)
|
|
79462
|
+
return null;
|
|
79463
|
+
const nrrd = this.ctx.nrrd_states;
|
|
79464
|
+
const img = nrrd.image;
|
|
79465
|
+
let hMM, vMM, flipV;
|
|
79466
|
+
switch (this.ctx.protectedData.axis) {
|
|
79467
|
+
case "z":
|
|
79468
|
+
hMM = img.nrrd_x_mm;
|
|
79469
|
+
vMM = img.nrrd_y_mm;
|
|
79470
|
+
flipV = false;
|
|
79471
|
+
break;
|
|
79472
|
+
case "y":
|
|
79473
|
+
hMM = img.nrrd_x_mm;
|
|
79474
|
+
vMM = img.nrrd_z_mm;
|
|
79475
|
+
flipV = true;
|
|
79476
|
+
break;
|
|
79477
|
+
case "x":
|
|
79478
|
+
hMM = img.nrrd_z_mm;
|
|
79479
|
+
vMM = img.nrrd_y_mm;
|
|
79480
|
+
flipV = false;
|
|
79481
|
+
break;
|
|
79482
|
+
default: return null;
|
|
79483
|
+
}
|
|
79484
|
+
const vyRaw = flipV ? sd.h - 1 - vy : vy;
|
|
79485
|
+
const x = (vx * hMM / sd.w) * nrrd.view.sizeFactor;
|
|
79486
|
+
const y = (vyRaw * vMM / sd.h) * nrrd.view.sizeFactor;
|
|
79487
|
+
return { x, y };
|
|
79488
|
+
}
|
|
79344
79489
|
// ── Pointer handlers (left button; right stays pan in EventRouter) ──────────
|
|
79345
79490
|
onPointerDown(e) {
|
|
79346
79491
|
const hit = this.toVoxel(e);
|
|
79347
79492
|
if (!hit)
|
|
79348
79493
|
return;
|
|
79349
79494
|
this.syncSlice();
|
|
79495
|
+
// Lasso is click-to-place vertices (NOT a drag) — own handler.
|
|
79496
|
+
if (this.promptTool === "lasso") {
|
|
79497
|
+
this.onLassoPointerDown(e, hit.vx, hit.vy);
|
|
79498
|
+
return;
|
|
79499
|
+
}
|
|
79350
79500
|
if (this.promptTool === "point") {
|
|
79351
79501
|
this.points.push({ x: hit.vx, y: hit.vy, label: this.polarity });
|
|
79502
|
+
this.hidePointMarkers = false; // show the new seed until its mask returns
|
|
79352
79503
|
this.emit();
|
|
79353
79504
|
}
|
|
79354
79505
|
else {
|
|
@@ -79364,9 +79515,43 @@ class AiAssistTool extends BaseTool {
|
|
|
79364
79515
|
}
|
|
79365
79516
|
}
|
|
79366
79517
|
}
|
|
79518
|
+
/** Lasso click: double-click finishes; click on a vertex deletes it; otherwise
|
|
79519
|
+
* add a vertex. Nothing is sent to the backend until finishLasso(). */
|
|
79520
|
+
onLassoPointerDown(e, vx, vy) {
|
|
79521
|
+
const now = Date.now();
|
|
79522
|
+
const dxl = e.offsetX - this.lastLassoDownPos.x;
|
|
79523
|
+
const dyl = e.offsetY - this.lastLassoDownPos.y;
|
|
79524
|
+
const isDbl = now - this.lastLassoDownTime < 300 &&
|
|
79525
|
+
dxl * dxl + dyl * dyl <= 36; // ~6px
|
|
79526
|
+
this.lastLassoDownTime = now;
|
|
79527
|
+
this.lastLassoDownPos = { x: e.offsetX, y: e.offsetY };
|
|
79528
|
+
// Double-click → finish (the 1st click of the pair already placed the vertex).
|
|
79529
|
+
if (isDbl && this.lassoVerts.length >= 3) {
|
|
79530
|
+
this.finishLasso();
|
|
79531
|
+
return;
|
|
79532
|
+
}
|
|
79533
|
+
// Click on an existing vertex → delete it (recompute the curve).
|
|
79534
|
+
const idx = this.lassoVertAt(e.offsetX, e.offsetY);
|
|
79535
|
+
if (idx >= 0) {
|
|
79536
|
+
this.pushLassoUndo();
|
|
79537
|
+
this.lassoVerts.splice(idx, 1);
|
|
79538
|
+
this.lassoHoverIdx = -1;
|
|
79539
|
+
this.notifyLasso();
|
|
79540
|
+
return;
|
|
79541
|
+
}
|
|
79542
|
+
// Otherwise add a new vertex.
|
|
79543
|
+
this.pushLassoUndo();
|
|
79544
|
+
this.lassoVerts.push({ vx, vy, sx: e.offsetX, sy: e.offsetY, label: this.polarity });
|
|
79545
|
+
this.notifyLasso();
|
|
79546
|
+
}
|
|
79367
79547
|
onPointerMove(e) {
|
|
79368
|
-
// Track hover for the scribble preview ring even when
|
|
79548
|
+
// Track hover for the scribble preview ring / point crosshair even when idle.
|
|
79369
79549
|
this.hoverScreen = { x: e.offsetX, y: e.offsetY };
|
|
79550
|
+
// Lasso: update which vertex (if any) is hovered, for the red ✕ delete affordance.
|
|
79551
|
+
if (this.promptTool === "lasso") {
|
|
79552
|
+
this.lassoHoverIdx = this.lassoVertAt(e.offsetX, e.offsetY);
|
|
79553
|
+
return; // lasso is click-based; no drag accumulation
|
|
79554
|
+
}
|
|
79370
79555
|
if (!this.dragging)
|
|
79371
79556
|
return;
|
|
79372
79557
|
// Track screen pos first so the live preview follows even slightly out of bounds.
|
|
@@ -79419,12 +79604,116 @@ class AiAssistTool extends BaseTool {
|
|
|
79419
79604
|
box: this.box,
|
|
79420
79605
|
scribble: this.scribble.length ? [...this.scribble] : undefined,
|
|
79421
79606
|
scribbleRadius: this.scribbleSize,
|
|
79607
|
+
lasso: this.lasso.length ? [...this.lasso] : undefined,
|
|
79422
79608
|
});
|
|
79423
79609
|
}
|
|
79610
|
+
// ── Lasso v2 (discrete vertices → smooth closed curve) ──────────────────────
|
|
79611
|
+
/** Index of the lasso vertex under (sx,sy) within the hit radius, else −1.
|
|
79612
|
+
* Uses live voxel→screen so it tracks pan/zoom. Topmost (latest) wins. */
|
|
79613
|
+
lassoVertAt(sx, sy) {
|
|
79614
|
+
var _a;
|
|
79615
|
+
const r2 = AiAssistTool.LASSO_HIT_R * AiAssistTool.LASSO_HIT_R;
|
|
79616
|
+
for (let i = this.lassoVerts.length - 1; i >= 0; i--) {
|
|
79617
|
+
const v = this.lassoVerts[i];
|
|
79618
|
+
const s = (_a = this.voxelToScreen(v.vx, v.vy)) !== null && _a !== void 0 ? _a : { x: v.sx, y: v.sy };
|
|
79619
|
+
const dx = sx - s.x, dy = sy - s.y;
|
|
79620
|
+
if (dx * dx + dy * dy <= r2)
|
|
79621
|
+
return i;
|
|
79622
|
+
}
|
|
79623
|
+
return -1;
|
|
79624
|
+
}
|
|
79625
|
+
/** Snapshot the vertex list for undo (clears redo). */
|
|
79626
|
+
pushLassoUndo() {
|
|
79627
|
+
this.lassoUndo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79628
|
+
this.lassoRedo = [];
|
|
79629
|
+
if (this.lassoUndo.length > 100)
|
|
79630
|
+
this.lassoUndo.shift();
|
|
79631
|
+
}
|
|
79632
|
+
/** Public: undo the last add/delete of a lasso vertex (Ctrl+Z while editing). */
|
|
79633
|
+
lassoUndoAction() {
|
|
79634
|
+
if (!this.lassoUndo.length)
|
|
79635
|
+
return;
|
|
79636
|
+
this.lassoRedo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79637
|
+
this.lassoVerts = this.lassoUndo.pop();
|
|
79638
|
+
this.lassoHoverIdx = -1;
|
|
79639
|
+
this.notifyLasso();
|
|
79640
|
+
}
|
|
79641
|
+
/** Public: redo (Ctrl+Y / Ctrl+Shift+Z while editing). */
|
|
79642
|
+
lassoRedoAction() {
|
|
79643
|
+
if (!this.lassoRedo.length)
|
|
79644
|
+
return;
|
|
79645
|
+
this.lassoUndo.push(this.lassoVerts.map((v) => (Object.assign({}, v))));
|
|
79646
|
+
this.lassoVerts = this.lassoRedo.pop();
|
|
79647
|
+
this.lassoHoverIdx = -1;
|
|
79648
|
+
this.notifyLasso();
|
|
79649
|
+
}
|
|
79650
|
+
/** Public: abandon the in-progress lasso (Esc). */
|
|
79651
|
+
cancelLasso() { this.clearLassoEditing(); }
|
|
79652
|
+
/** Public: true while the user is placing lasso vertices (≥1 vertex). */
|
|
79653
|
+
isLassoEditing() { return this.lassoVerts.length > 0; }
|
|
79654
|
+
/** Public: number of placed lasso vertices (drives the Finish button label/enable). */
|
|
79655
|
+
getLassoVertCount() { return this.lassoVerts.length; }
|
|
79656
|
+
/** Vertices ordered by polar angle around their centroid, so the closed curve is
|
|
79657
|
+
* a SIMPLE (non-self-intersecting) loop regardless of click order — otherwise
|
|
79658
|
+
* connecting points in click order + closing easily produces a bow-tie crossing.
|
|
79659
|
+
* Trade-off: deep concavities collapse to their star-shaped hull, which is fine
|
|
79660
|
+
* for a "rough loop around a blob". */
|
|
79661
|
+
orderedLassoVerts() {
|
|
79662
|
+
const n = this.lassoVerts.length;
|
|
79663
|
+
if (n < 3)
|
|
79664
|
+
return this.lassoVerts.slice();
|
|
79665
|
+
let cx = 0, cy = 0;
|
|
79666
|
+
for (const v of this.lassoVerts) {
|
|
79667
|
+
cx += v.vx;
|
|
79668
|
+
cy += v.vy;
|
|
79669
|
+
}
|
|
79670
|
+
cx /= n;
|
|
79671
|
+
cy /= n;
|
|
79672
|
+
return this.lassoVerts.slice().sort((a, b) => Math.atan2(a.vy - cy, a.vx - cx) - Math.atan2(b.vy - cy, b.vx - cx));
|
|
79673
|
+
}
|
|
79674
|
+
/** Public: close the curve, densify it, and send it to the backend as the lasso
|
|
79675
|
+
* contour. Needs ≥3 vertices (a real area). Then clears the editing state. */
|
|
79676
|
+
finishLasso() {
|
|
79677
|
+
if (this.lassoVerts.length < 3)
|
|
79678
|
+
return;
|
|
79679
|
+
// Order by angle (no self-crossing), then sample the SAME closed curve the user
|
|
79680
|
+
// saw → contour points (so the mask matches the curve, not a straight polygon).
|
|
79681
|
+
const contour = this.sampleClosedSpline(this.orderedLassoVerts().map((v) => ({ x: v.vx, y: v.vy })), 16);
|
|
79682
|
+
const label = this.lassoVerts[0].label; // a lasso carries one polarity (set at start)
|
|
79683
|
+
this.lasso = contour.map((p) => ({ x: Math.round(p.x), y: Math.round(p.y), label }));
|
|
79684
|
+
this.clearLassoEditing();
|
|
79685
|
+
this.emit();
|
|
79686
|
+
}
|
|
79687
|
+
/** Sample a smooth CLOSED Catmull-Rom spline through `pts` → denser polyline.
|
|
79688
|
+
* `segs` samples per control-point span. <3 pts → returns a copy unchanged. */
|
|
79689
|
+
sampleClosedSpline(pts, segs) {
|
|
79690
|
+
const n = pts.length;
|
|
79691
|
+
if (n < 3)
|
|
79692
|
+
return pts.slice();
|
|
79693
|
+
const at = (i) => pts[((i % n) + n) % n];
|
|
79694
|
+
const out = [];
|
|
79695
|
+
for (let i = 0; i < n; i++) {
|
|
79696
|
+
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
|
|
79697
|
+
for (let s = 0; s < segs; s++) {
|
|
79698
|
+
const t = s / segs, t2 = t * t, t3 = t2 * t;
|
|
79699
|
+
out.push({
|
|
79700
|
+
x: 0.5 * (2 * p1.x + (-p0.x + p2.x) * t +
|
|
79701
|
+
(2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
|
|
79702
|
+
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),
|
|
79703
|
+
y: 0.5 * (2 * p1.y + (-p0.y + p2.y) * t +
|
|
79704
|
+
(2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
|
|
79705
|
+
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),
|
|
79706
|
+
});
|
|
79707
|
+
}
|
|
79708
|
+
}
|
|
79709
|
+
return out;
|
|
79710
|
+
}
|
|
79424
79711
|
// ── Apply backend result → scratch volume ──────────────────────────────────
|
|
79425
79712
|
applyMask(result) {
|
|
79426
79713
|
if (!this.scratchVol)
|
|
79427
79714
|
return;
|
|
79715
|
+
// A prediction landed → hide the point seed markers; only the mask should show.
|
|
79716
|
+
this.hidePointMarkers = true;
|
|
79428
79717
|
// 3D result (engine B): write each slice across the covered span.
|
|
79429
79718
|
if (result.slices && result.sliceRange) {
|
|
79430
79719
|
const [lo] = result.sliceRange;
|
|
@@ -79454,7 +79743,7 @@ class AiAssistTool extends BaseTool {
|
|
|
79454
79743
|
pos += run;
|
|
79455
79744
|
value ^= 1;
|
|
79456
79745
|
}
|
|
79457
|
-
const ch = this.
|
|
79746
|
+
const ch = this.activeLabel;
|
|
79458
79747
|
try {
|
|
79459
79748
|
const out = this.scratchVol.getSliceUint8(sliceIndex, axis).data; // copy
|
|
79460
79749
|
// Frozen same-channel pixels for this slice (if any region was committed).
|
|
@@ -79527,10 +79816,7 @@ class AiAssistTool extends BaseTool {
|
|
|
79527
79816
|
}
|
|
79528
79817
|
targetCtx.restore();
|
|
79529
79818
|
}
|
|
79530
|
-
// Scribble brush-size preview ring (when NOT dragging)
|
|
79531
|
-
// the cursor whose radius = scribbleSize, so the user sees the brush size and
|
|
79532
|
-
// how the slider changes it (mirrors the paint brush's preview ring). Drawn
|
|
79533
|
-
// every frame from the live hover position by copper3d's render loop.
|
|
79819
|
+
// Scribble brush-size preview ring (when NOT dragging).
|
|
79534
79820
|
if (this.promptTool === "scribble" && !this.dragging && this.hoverScreen) {
|
|
79535
79821
|
targetCtx.save();
|
|
79536
79822
|
const fg = this.polarity === 1;
|
|
@@ -79543,8 +79829,105 @@ class AiAssistTool extends BaseTool {
|
|
|
79543
79829
|
targetCtx.stroke();
|
|
79544
79830
|
targetCtx.restore();
|
|
79545
79831
|
}
|
|
79832
|
+
// Point markers + hover crosshair so the clinician sees exactly where each seed
|
|
79833
|
+
// lands (and where the next click will go) before/while the prediction returns.
|
|
79834
|
+
if (this.promptTool === "point") {
|
|
79835
|
+
if (!this.hidePointMarkers) {
|
|
79836
|
+
for (const p of this.points) {
|
|
79837
|
+
const s = this.voxelToScreen(p.x, p.y);
|
|
79838
|
+
if (s)
|
|
79839
|
+
this.drawSeedMarker(targetCtx, s.x, s.y, p.label === 1);
|
|
79840
|
+
}
|
|
79841
|
+
}
|
|
79842
|
+
if (!this.dragging && this.hoverScreen) {
|
|
79843
|
+
targetCtx.save();
|
|
79844
|
+
targetCtx.strokeStyle = this.polarity === 1
|
|
79845
|
+
? "rgba(120,255,180,0.55)" : "rgba(255,120,120,0.55)";
|
|
79846
|
+
targetCtx.lineWidth = 1;
|
|
79847
|
+
const { x, y } = this.hoverScreen;
|
|
79848
|
+
targetCtx.beginPath();
|
|
79849
|
+
targetCtx.moveTo(x - 7, y);
|
|
79850
|
+
targetCtx.lineTo(x + 7, y);
|
|
79851
|
+
targetCtx.moveTo(x, y - 7);
|
|
79852
|
+
targetCtx.lineTo(x, y + 7);
|
|
79853
|
+
targetCtx.stroke();
|
|
79854
|
+
targetCtx.restore();
|
|
79855
|
+
}
|
|
79856
|
+
}
|
|
79857
|
+
// Lasso v2: smooth closed curve through the placed vertices (closed + filled
|
|
79858
|
+
// from ≥2 vertices), vertex dots, and a red ✕ on the hovered vertex.
|
|
79859
|
+
if (this.promptTool === "lasso" && this.lassoVerts.length > 0) {
|
|
79860
|
+
this.renderLassoOverlay(targetCtx);
|
|
79861
|
+
}
|
|
79546
79862
|
}
|
|
79547
|
-
|
|
79863
|
+
/** A seed marker (small filled dot + ring) for point prompts. */
|
|
79864
|
+
drawSeedMarker(ctx, x, y, fg) {
|
|
79865
|
+
ctx.save();
|
|
79866
|
+
const col = fg ? "rgba(120,255,180,0.95)" : "rgba(255,120,120,0.95)";
|
|
79867
|
+
ctx.fillStyle = col;
|
|
79868
|
+
ctx.strokeStyle = "rgba(0,0,0,0.55)";
|
|
79869
|
+
ctx.lineWidth = 1.5;
|
|
79870
|
+
ctx.beginPath();
|
|
79871
|
+
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
|
|
79872
|
+
ctx.fill();
|
|
79873
|
+
ctx.stroke();
|
|
79874
|
+
ctx.restore();
|
|
79875
|
+
}
|
|
79876
|
+
/** Render the lasso editing overlay (curve + fill + vertices + hover ✕). */
|
|
79877
|
+
renderLassoOverlay(ctx) {
|
|
79878
|
+
var _a;
|
|
79879
|
+
const fg = this.lassoVerts[0].label === 1;
|
|
79880
|
+
const stroke = fg ? "rgba(120,255,180,0.95)" : "rgba(255,120,120,0.95)";
|
|
79881
|
+
const fill = fg ? "rgba(120,255,180,0.14)" : "rgba(255,120,120,0.14)";
|
|
79882
|
+
// PATH: angularly-ordered vertices (simple, non-crossing loop) in screen space.
|
|
79883
|
+
const ov = this.orderedLassoVerts().map((v) => { var _a; return (_a = this.voxelToScreen(v.vx, v.vy)) !== null && _a !== void 0 ? _a : { x: v.sx, y: v.sy }; });
|
|
79884
|
+
const path = ov.length >= 3 ? this.sampleClosedSpline(ov, 16) : ov;
|
|
79885
|
+
ctx.save();
|
|
79886
|
+
if (path.length >= 2) {
|
|
79887
|
+
ctx.beginPath();
|
|
79888
|
+
ctx.moveTo(path[0].x, path[0].y);
|
|
79889
|
+
for (let i = 1; i < path.length; i++)
|
|
79890
|
+
ctx.lineTo(path[i].x, path[i].y);
|
|
79891
|
+
ctx.closePath();
|
|
79892
|
+
ctx.fillStyle = fill;
|
|
79893
|
+
ctx.fill();
|
|
79894
|
+
ctx.lineCap = "round";
|
|
79895
|
+
ctx.lineJoin = "round";
|
|
79896
|
+
ctx.lineWidth = 1.75;
|
|
79897
|
+
ctx.strokeStyle = stroke;
|
|
79898
|
+
ctx.stroke();
|
|
79899
|
+
}
|
|
79900
|
+
// Vertex handles — drawn from the ORIGINAL vertex order so lassoHoverIdx (an
|
|
79901
|
+
// index into lassoVerts from the hit-test) marks the correct one for the ✕.
|
|
79902
|
+
for (let i = 0; i < this.lassoVerts.length; i++) {
|
|
79903
|
+
const v = this.lassoVerts[i];
|
|
79904
|
+
const p = (_a = this.voxelToScreen(v.vx, v.vy)) !== null && _a !== void 0 ? _a : { x: v.sx, y: v.sy };
|
|
79905
|
+
if (i === this.lassoHoverIdx) {
|
|
79906
|
+
// Red ✕ — click to delete this vertex.
|
|
79907
|
+
ctx.strokeStyle = "rgba(255,90,90,0.98)";
|
|
79908
|
+
ctx.lineWidth = 2;
|
|
79909
|
+
ctx.beginPath();
|
|
79910
|
+
ctx.moveTo(p.x - 5, p.y - 5);
|
|
79911
|
+
ctx.lineTo(p.x + 5, p.y + 5);
|
|
79912
|
+
ctx.moveTo(p.x + 5, p.y - 5);
|
|
79913
|
+
ctx.lineTo(p.x - 5, p.y + 5);
|
|
79914
|
+
ctx.stroke();
|
|
79915
|
+
}
|
|
79916
|
+
else {
|
|
79917
|
+
ctx.fillStyle = stroke;
|
|
79918
|
+
ctx.strokeStyle = "rgba(0,0,0,0.55)";
|
|
79919
|
+
ctx.lineWidth = 1.25;
|
|
79920
|
+
ctx.beginPath();
|
|
79921
|
+
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
|
|
79922
|
+
ctx.fill();
|
|
79923
|
+
ctx.stroke();
|
|
79924
|
+
}
|
|
79925
|
+
}
|
|
79926
|
+
ctx.restore();
|
|
79927
|
+
}
|
|
79928
|
+
}
|
|
79929
|
+
/** Screen-pixel radius for hit-testing a hovered vertex. */
|
|
79930
|
+
AiAssistTool.LASSO_HIT_R = 8;
|
|
79548
79931
|
|
|
79549
79932
|
/**
|
|
79550
79933
|
* DrawToolCore — Tool orchestration and event routing.
|
|
@@ -82579,16 +82962,39 @@ class NrrdTools {
|
|
|
82579
82962
|
// — Driver methods called by the app-layer composable —
|
|
82580
82963
|
aiSetPromptTool(tool) { this.drawCore.aiAssistTool.setPromptTool(tool); }
|
|
82581
82964
|
aiSetPolarity(label) { this.drawCore.aiAssistTool.setPolarity(label); }
|
|
82582
|
-
/**
|
|
82583
|
-
|
|
82965
|
+
/** Select the active Segmentation by its label value (the AI paints into it). */
|
|
82966
|
+
aiSetActiveSegment(label) { this.drawCore.aiAssistTool.setActiveSegment(label); }
|
|
82967
|
+
/** Set a Segmentation's colour (its label's colorMap entry; 2D overlay repaints). */
|
|
82968
|
+
aiSetSegmentColor(label, color) {
|
|
82969
|
+
this.drawCore.aiAssistTool.setSegmentColor(label, color);
|
|
82970
|
+
}
|
|
82971
|
+
/** "New segmentation": freeze current regions + switch to a new label. */
|
|
82972
|
+
aiNewSegment(label) { this.drawCore.aiAssistTool.newSegment(label); }
|
|
82973
|
+
/** Delete a Segmentation: erase all its label's voxels from scratch + committed. */
|
|
82974
|
+
aiClearSegment(label) { this.drawCore.aiAssistTool.clearSegment(label); }
|
|
82584
82975
|
/** Set the scribble brush radius (px). */
|
|
82585
82976
|
aiSetScribbleSize(size) { this.drawCore.aiAssistTool.setScribbleSize(size); }
|
|
82586
82977
|
/** Register the callback invoked when a prompt gesture completes (app → backend). */
|
|
82587
82978
|
aiOnPrompt(cb) { this.drawCore.aiAssistTool.onPrompt = cb; }
|
|
82979
|
+
/** Register the callback fired when the lasso vertex set changes (drives the Finish button). */
|
|
82980
|
+
aiOnLassoChange(cb) { this.drawCore.aiAssistTool.onLassoChange = cb; }
|
|
82588
82981
|
/** Apply a backend mask result into the scratch volume (overlay repaints next frame). */
|
|
82589
82982
|
aiApplyMask(result) { this.drawCore.aiAssistTool.applyMask(result); }
|
|
82590
82983
|
/** Clear the in-progress prompt set (e.g. slice change). */
|
|
82591
82984
|
aiClearPrompts() { this.drawCore.aiAssistTool.resetPrompts(); }
|
|
82985
|
+
// — Lasso v2 (discrete-vertex closed-curve) controls —
|
|
82986
|
+
/** Close + send the in-progress lasso to the backend (needs ≥3 vertices). */
|
|
82987
|
+
aiFinishLasso() { this.drawCore.aiAssistTool.finishLasso(); }
|
|
82988
|
+
/** Undo the last lasso vertex add/delete. */
|
|
82989
|
+
aiLassoUndo() { this.drawCore.aiAssistTool.lassoUndoAction(); }
|
|
82990
|
+
/** Redo the last undone lasso vertex change. */
|
|
82991
|
+
aiLassoRedo() { this.drawCore.aiAssistTool.lassoRedoAction(); }
|
|
82992
|
+
/** Abandon the in-progress lasso (Esc). */
|
|
82993
|
+
aiCancelLasso() { this.drawCore.aiAssistTool.cancelLasso(); }
|
|
82994
|
+
/** True while the user is placing lasso vertices. */
|
|
82995
|
+
aiIsLassoEditing() { return this.drawCore.aiAssistTool.isLassoEditing(); }
|
|
82996
|
+
/** Number of placed lasso vertices (drives the Finish button). */
|
|
82997
|
+
aiLassoVertCount() { return this.drawCore.aiAssistTool.getLassoVertCount(); }
|
|
82592
82998
|
/** "New region": freeze current regions (they persist) + start a fresh prompt set. */
|
|
82593
82999
|
aiCommitRegion() { this.drawCore.aiAssistTool.commitRegion(); }
|
|
82594
83000
|
/** Discard all AI scratch painting since enter (sandbox discard). */
|
|
@@ -82599,13 +83005,26 @@ class NrrdTools {
|
|
|
82599
83005
|
aiGetScratchSlices() {
|
|
82600
83006
|
return this.drawCore.aiAssistTool.getScratchSlices();
|
|
82601
83007
|
}
|
|
83008
|
+
/** Per-segmentation serialization (one binary RLE per label per z-slice) for the
|
|
83009
|
+
* multi-label / per-colour 3D build. */
|
|
83010
|
+
aiGetScratchSegments() {
|
|
83011
|
+
return this.drawCore.aiAssistTool.getScratchSegments();
|
|
83012
|
+
}
|
|
82602
83013
|
/**
|
|
82603
|
-
* Merge the AI scratch into a target layer
|
|
82604
|
-
* merge —
|
|
82605
|
-
*
|
|
82606
|
-
*
|
|
83014
|
+
* Merge the AI scratch into a target layer with an explicit per-segmentation
|
|
83015
|
+
* mapping, as a single undoable group (sandbox merge — non-destructive + one
|
|
83016
|
+
* Ctrl+Z). `mapping` is `{ AI scratch label -> target channel(1-8) }`; any label
|
|
83017
|
+
* absent from the mapping (or mapped to 0) is DISCARDED (not merged). Several AI
|
|
83018
|
+
* labels MAY map to the SAME channel — after merge the channel is the identity
|
|
83019
|
+
* (CLAUDE.md decision M2), so the merged voxels take that channel's fixed colour.
|
|
83020
|
+
*
|
|
83021
|
+
* UNION semantics (M4): an AI voxel only fills a target voxel that is currently
|
|
83022
|
+
* EMPTY (0) — existing manual annotation is NEVER erased. Where two AI labels map
|
|
83023
|
+
* to different channels and overlap the same voxel, the first one written wins
|
|
83024
|
+
* (iteration order), an accepted edge case for a single-value-per-voxel mask.
|
|
83025
|
+
* Scans all z-slices, so voxels painted from any view are caught.
|
|
82607
83026
|
*/
|
|
82608
|
-
|
|
83027
|
+
aiCommitToLayerMapped(targetLayer, mapping) {
|
|
82609
83028
|
const scratch = this.drawCore.aiAssistTool.getScratchVolume();
|
|
82610
83029
|
if (!scratch)
|
|
82611
83030
|
return;
|
|
@@ -82627,10 +83046,21 @@ class NrrdTools {
|
|
|
82627
83046
|
continue;
|
|
82628
83047
|
const oldSlice = target.getSliceUint8(z, "z").data; // copy
|
|
82629
83048
|
const newSlice = oldSlice.slice();
|
|
83049
|
+
let changed = false;
|
|
82630
83050
|
for (let i = 0; i < newSlice.length; i++) {
|
|
82631
|
-
|
|
82632
|
-
|
|
83051
|
+
const label = sc[i];
|
|
83052
|
+
if (label === 0)
|
|
83053
|
+
continue;
|
|
83054
|
+
const ch = mapping[label];
|
|
83055
|
+
if (!ch)
|
|
83056
|
+
continue; // unmapped / discarded segmentation
|
|
83057
|
+
if (newSlice[i] === 0) { // union: fill only EMPTY voxels (M4)
|
|
83058
|
+
newSlice[i] = ch;
|
|
83059
|
+
changed = true;
|
|
83060
|
+
}
|
|
82633
83061
|
}
|
|
83062
|
+
if (!changed)
|
|
83063
|
+
continue; // nothing landed on this slice → no delta
|
|
82634
83064
|
target.setSliceUint8(z, newSlice, "z");
|
|
82635
83065
|
deltas.push({ layerId: targetLayer, axis: "z", sliceIndex: z, oldSlice, newSlice: newSlice.slice() });
|
|
82636
83066
|
}
|
|
@@ -82638,6 +83068,19 @@ class NrrdTools {
|
|
|
82638
83068
|
this.drawCore.undoManager.pushGroup(deltas);
|
|
82639
83069
|
this.reloadMasksFromVolume();
|
|
82640
83070
|
}
|
|
83071
|
+
/**
|
|
83072
|
+
* Back-compat shim: merge with an IDENTITY mapping (each AI label → the same
|
|
83073
|
+
* channel number) via the mapped path above. Newer callers should use
|
|
83074
|
+
* `aiCommitToLayerMapped` with an explicit per-segmentation mapping.
|
|
83075
|
+
*/
|
|
83076
|
+
aiCommitToLayer(targetLayer = "layer1") {
|
|
83077
|
+
var _a;
|
|
83078
|
+
const segs = this.drawCore.aiAssistTool.getScratchSegments();
|
|
83079
|
+
const mapping = {};
|
|
83080
|
+
for (const s of (_a = segs === null || segs === void 0 ? void 0 : segs.segments) !== null && _a !== void 0 ? _a : [])
|
|
83081
|
+
mapping[s.label] = s.label;
|
|
83082
|
+
this.aiCommitToLayerMapped(targetLayer, mapping);
|
|
83083
|
+
}
|
|
82641
83084
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
82642
83085
|
// 11. Clear / Reset
|
|
82643
83086
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -83001,7 +83444,23 @@ function evaluateElement(element, weights) {
|
|
|
83001
83444
|
}
|
|
83002
83445
|
|
|
83003
83446
|
// import * as kiwrious from "copper3d_plugin_heart_k";
|
|
83004
|
-
|
|
83005
|
-
|
|
83447
|
+
// "v3.6.6" is injected at build time by rollup @rollup/plugin-replace, sourced from package.json version.
|
|
83448
|
+
// When copper3d is consumed from local source (no rollup replace step), the identifier is undefined and
|
|
83449
|
+
// referencing it throws a ReferenceError — guard it so local loading still works.
|
|
83450
|
+
let _revision = "unknown";
|
|
83451
|
+
try {
|
|
83452
|
+
_revision = "v3.6.6";
|
|
83453
|
+
}
|
|
83454
|
+
catch (_a) {
|
|
83455
|
+
/* "v3.6.6" not injected (local source build) */
|
|
83456
|
+
}
|
|
83457
|
+
const REVISION = _revision;
|
|
83458
|
+
// Expose on global so the version can be read in a production browser console via window.__COPPER3D_VERSION__
|
|
83459
|
+
if (typeof window !== "undefined") {
|
|
83460
|
+
window.__COPPER3D_VERSION__ = REVISION;
|
|
83461
|
+
}
|
|
83462
|
+
if (typeof console !== "undefined") {
|
|
83463
|
+
console.log(`%cCopper3D Visualisation %c${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
|
|
83464
|
+
}
|
|
83006
83465
|
|
|
83007
83466
|
export { AI_CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, GaussianSmoother, MeshNodeTool, NrrdTools, REVISION, SurfaceAnnotator, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };
|