angular-three-tweakpane 4.2.3 → 4.2.4

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,548 @@
1
+ import * as i0 from '@angular/core';
2
+ import { model, input, inject, computed, untracked, effect, DestroyRef, Directive } from '@angular/core';
3
+ import { BladeController, Emitter, BladeApi, createPlugin } from '@tweakpane/core';
4
+ import * as i1 from 'angular-three-tweakpane';
5
+ import { TweakpaneFolder, TweakpanePane, TweakpaneBlade } from 'angular-three-tweakpane';
6
+
7
+ const SVG_NS = 'http://www.w3.org/2000/svg';
8
+ const VIEW_WIDTH = 220;
9
+ const VIEW_HEIGHT = 112;
10
+ const VIEW_PADDING = 10;
11
+ const TANGENT_LENGTH = 0.18;
12
+ const CURVE_X_EPSILON = 1e-6;
13
+ const MIN_TANGENT_ANGLE = -Math.PI / 2;
14
+ const MAX_TANGENT_ANGLE = Math.PI / 2;
15
+ const MAX_TANGENT_WEIGHT = 3;
16
+ const DEFAULT_CURVE_PARAMS = {
17
+ minX: 0,
18
+ maxX: 1,
19
+ minY: 0,
20
+ maxY: 1,
21
+ };
22
+ class CurveView {
23
+ constructor(document, viewProps) {
24
+ this.element = document.createElement('div');
25
+ this.element.classList.add('tweakpane-curve');
26
+ viewProps.bindClassModifiers(this.element);
27
+ this.label = document.createElement('div');
28
+ this.label.classList.add('tweakpane-curve__label');
29
+ this.element.appendChild(this.label);
30
+ this.svg = document.createElementNS(SVG_NS, 'svg');
31
+ this.svg.classList.add('tweakpane-curve__editor');
32
+ this.svg.setAttribute('viewBox', `0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`);
33
+ this.svg.setAttribute('role', 'application');
34
+ this.svg.setAttribute('aria-label', 'Curve editor');
35
+ this.svg.setAttribute('tabindex', '0');
36
+ this.svg.setAttribute('preserveAspectRatio', 'none');
37
+ this.element.appendChild(this.svg);
38
+ const grid = document.createElementNS(SVG_NS, 'path');
39
+ grid.classList.add('tweakpane-curve__grid');
40
+ grid.setAttribute('d', `M ${VIEW_PADDING} ${VIEW_HEIGHT / 2} H ${VIEW_WIDTH - VIEW_PADDING} M ${VIEW_WIDTH / 2} ${VIEW_PADDING} V ${VIEW_HEIGHT - VIEW_PADDING}`);
41
+ this.svg.appendChild(grid);
42
+ this.path = document.createElementNS(SVG_NS, 'path');
43
+ this.path.classList.add('tweakpane-curve__path');
44
+ this.svg.appendChild(this.path);
45
+ this.tangents = document.createElementNS(SVG_NS, 'g');
46
+ this.svg.appendChild(this.tangents);
47
+ this.points = document.createElementNS(SVG_NS, 'g');
48
+ this.svg.appendChild(this.points);
49
+ }
50
+ render(value, params, selected) {
51
+ const points = normalizeCurveData(value, params).points;
52
+ const commands = [];
53
+ for (let index = 0; index < 64; index++) {
54
+ const x = params.minX + ((params.maxX - params.minX) * index) / 63;
55
+ const y = evaluateCurve(points, x);
56
+ const screen = toScreen({ x, y }, params);
57
+ commands.push(`${index === 0 ? 'M' : 'L'} ${screen.x.toFixed(2)} ${screen.y.toFixed(2)}`);
58
+ }
59
+ this.path.setAttribute('d', commands.join(' '));
60
+ this.tangents.replaceChildren();
61
+ this.points.replaceChildren();
62
+ for (const [index, point] of points.entries()) {
63
+ if (index > 0)
64
+ this.renderTangent(points, index, 'in', params, selected);
65
+ if (index < points.length - 1)
66
+ this.renderTangent(points, index, 'out', params, selected);
67
+ const screen = toScreen(point, params);
68
+ const circle = this.svg.ownerDocument.createElementNS(SVG_NS, 'circle');
69
+ circle.classList.add('tweakpane-curve__point');
70
+ if (selected?.index === index && selected.kind === 'point') {
71
+ circle.classList.add('tweakpane-curve__point--selected');
72
+ }
73
+ circle.dataset['index'] = String(index);
74
+ circle.setAttribute('cx', String(screen.x));
75
+ circle.setAttribute('cy', String(screen.y));
76
+ circle.setAttribute('r', selected?.index === index && selected.kind === 'point' ? '5' : '4');
77
+ this.points.appendChild(circle);
78
+ }
79
+ }
80
+ renderTangent(points, index, kind, params, selected) {
81
+ const point = points[index];
82
+ const handle = getTangentHandle(point, kind, params);
83
+ const pointScreen = toScreen(point, params);
84
+ const handleScreen = toScreen(handle, params);
85
+ const line = this.svg.ownerDocument.createElementNS(SVG_NS, 'line');
86
+ line.classList.add('tweakpane-curve__tangent-line');
87
+ line.setAttribute('x1', String(pointScreen.x));
88
+ line.setAttribute('y1', String(pointScreen.y));
89
+ line.setAttribute('x2', String(handleScreen.x));
90
+ line.setAttribute('y2', String(handleScreen.y));
91
+ this.tangents.appendChild(line);
92
+ const circle = this.svg.ownerDocument.createElementNS(SVG_NS, 'circle');
93
+ circle.classList.add('tweakpane-curve__tangent');
94
+ if (selected?.index === index && selected.kind === kind) {
95
+ circle.classList.add('tweakpane-curve__tangent--selected');
96
+ }
97
+ circle.dataset['index'] = String(index);
98
+ circle.dataset['tangent'] = kind;
99
+ circle.setAttribute('cx', String(handleScreen.x));
100
+ circle.setAttribute('cy', String(handleScreen.y));
101
+ circle.setAttribute('r', selected?.index === index && selected.kind === kind ? '4' : '3');
102
+ this.tangents.appendChild(circle);
103
+ }
104
+ }
105
+ class CurveController extends BladeController {
106
+ constructor(document, params, blade, viewProps) {
107
+ const view = new CurveView(document, viewProps);
108
+ super({ blade, view, viewProps });
109
+ this.params = params;
110
+ this.emitter = new Emitter();
111
+ this.selected = null;
112
+ this.pointerId = null;
113
+ this.curveData = normalizeCurveData(params.value, params);
114
+ this.view.label.textContent = params.label ?? '';
115
+ this.view.label.hidden = !params.label;
116
+ this.render();
117
+ this.onPointerDown = this.onPointerDown.bind(this);
118
+ this.onPointerMove = this.onPointerMove.bind(this);
119
+ this.onPointerEnd = this.onPointerEnd.bind(this);
120
+ this.onDoubleClick = this.onDoubleClick.bind(this);
121
+ this.onContextMenu = this.onContextMenu.bind(this);
122
+ this.onKeyDown = this.onKeyDown.bind(this);
123
+ view.svg.addEventListener('pointerdown', this.onPointerDown);
124
+ view.svg.addEventListener('pointermove', this.onPointerMove);
125
+ view.svg.addEventListener('pointerup', this.onPointerEnd);
126
+ view.svg.addEventListener('pointercancel', this.onPointerEnd);
127
+ view.svg.addEventListener('lostpointercapture', this.onPointerEnd);
128
+ view.svg.addEventListener('dblclick', this.onDoubleClick);
129
+ view.svg.addEventListener('contextmenu', this.onContextMenu);
130
+ view.svg.addEventListener('keydown', this.onKeyDown);
131
+ viewProps.handleDispose(() => this.disposeListeners());
132
+ }
133
+ getValue() {
134
+ return cloneCurveData(this.curveData);
135
+ }
136
+ setValue(value, emit = false, last = true) {
137
+ this.curveData = normalizeCurveData(value, this.params);
138
+ this.render();
139
+ if (emit)
140
+ this.emitter.emit('change', { value: this.getValue(), last });
141
+ }
142
+ onPointerDown(event) {
143
+ if (this.viewProps.globalDisabled.rawValue || this.pointerId !== null)
144
+ return;
145
+ const target = event.target;
146
+ const index = target.dataset['index'];
147
+ if (index === undefined)
148
+ return;
149
+ event.preventDefault();
150
+ const tangent = target.dataset['tangent'];
151
+ this.selected = {
152
+ index: Number(index),
153
+ kind: tangent === 'in' || tangent === 'out' ? tangent : 'point',
154
+ };
155
+ this.pointerId = event.pointerId;
156
+ this.view.svg.setPointerCapture?.(event.pointerId);
157
+ this.render();
158
+ }
159
+ onPointerMove(event) {
160
+ if (event.pointerId !== this.pointerId || this.selected === null)
161
+ return;
162
+ event.preventDefault();
163
+ this.moveSelected(event, false);
164
+ }
165
+ onPointerEnd(event) {
166
+ if (event.pointerId !== this.pointerId)
167
+ return;
168
+ if (this.selected !== null)
169
+ this.moveSelected(event, true);
170
+ if (this.view.svg.hasPointerCapture?.(event.pointerId))
171
+ this.view.svg.releasePointerCapture?.(event.pointerId);
172
+ this.pointerId = null;
173
+ }
174
+ onDoubleClick(event) {
175
+ if (this.viewProps.globalDisabled.rawValue)
176
+ return;
177
+ event.preventDefault();
178
+ const point = fromClient(event, this.view.svg, this.params);
179
+ const added = { ...point, r_in: 0, r_out: 0, w_in: 1, w_out: 1 };
180
+ const points = [...this.curveData.points, added].sort((a, b) => a.x - b.x);
181
+ this.selected = { index: points.indexOf(added), kind: 'point' };
182
+ this.setValue({ ...this.curveData, points }, true, true);
183
+ }
184
+ onContextMenu(event) {
185
+ if (this.viewProps.globalDisabled.rawValue || this.curveData.points.length <= 2)
186
+ return;
187
+ const target = event.target;
188
+ if (target.dataset['tangent'])
189
+ return;
190
+ const index = target.dataset['index'];
191
+ if (index === undefined)
192
+ return;
193
+ event.preventDefault();
194
+ const points = this.curveData.points.filter((_, pointIndex) => pointIndex !== Number(index));
195
+ this.selected = null;
196
+ this.setValue({ ...this.curveData, points }, true, true);
197
+ }
198
+ onKeyDown(event) {
199
+ if (this.viewProps.globalDisabled.rawValue || this.selected === null)
200
+ return;
201
+ if (event.key === 'Delete' || event.key === 'Backspace') {
202
+ event.preventDefault();
203
+ if (this.selected.kind === 'point') {
204
+ if (this.curveData.points.length <= 2)
205
+ return;
206
+ const points = this.curveData.points.filter((_, index) => index !== this.selected?.index);
207
+ this.selected = null;
208
+ this.setValue({ ...this.curveData, points }, true, true);
209
+ }
210
+ else {
211
+ const angleKey = this.selected.kind === 'in' ? 'r_in' : 'r_out';
212
+ const weightKey = this.selected.kind === 'in' ? 'w_in' : 'w_out';
213
+ const selectedIndex = this.selected.index;
214
+ const points = this.curveData.points.map((point, index) => {
215
+ if (index !== selectedIndex)
216
+ return point;
217
+ const reset = { ...point };
218
+ delete reset[angleKey];
219
+ delete reset[weightKey];
220
+ return reset;
221
+ });
222
+ this.setValue({ ...this.curveData, points }, true, true);
223
+ }
224
+ return;
225
+ }
226
+ const direction = event.key === 'ArrowLeft' || event.key === 'ArrowDown'
227
+ ? -1
228
+ : event.key === 'ArrowRight' || event.key === 'ArrowUp'
229
+ ? 1
230
+ : 0;
231
+ if (direction === 0)
232
+ return;
233
+ event.preventDefault();
234
+ if (this.selected.kind !== 'point') {
235
+ const current = this.curveData.points[this.selected.index];
236
+ const angleKey = this.selected.kind === 'in' ? 'r_in' : 'r_out';
237
+ const weightKey = this.selected.kind === 'in' ? 'w_in' : 'w_out';
238
+ const points = this.curveData.points.map((point, index) => {
239
+ if (index !== this.selected?.index)
240
+ return point;
241
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
242
+ return {
243
+ ...point,
244
+ [angleKey]: clamp((current[angleKey] ?? 0) + direction * 0.02, MIN_TANGENT_ANGLE, MAX_TANGENT_ANGLE),
245
+ };
246
+ }
247
+ return {
248
+ ...point,
249
+ [weightKey]: clamp((current[weightKey] ?? 1) + direction * 0.05, 0, MAX_TANGENT_WEIGHT),
250
+ };
251
+ });
252
+ this.setValue({ ...this.curveData, points }, true, true);
253
+ return;
254
+ }
255
+ const xStep = (this.params.maxX - this.params.minX) / 100;
256
+ const yStep = (this.params.maxY - this.params.minY) / 100;
257
+ const current = this.curveData.points[this.selected.index];
258
+ const next = {
259
+ ...current,
260
+ x: current.x + (event.key === 'ArrowLeft' || event.key === 'ArrowRight' ? direction * xStep : 0),
261
+ y: current.y + (event.key === 'ArrowDown' || event.key === 'ArrowUp' ? direction * yStep : 0),
262
+ };
263
+ this.updateSelectedPoint(next, true);
264
+ }
265
+ moveSelected(event, last) {
266
+ if (this.selected?.kind === 'point') {
267
+ this.updateSelectedPoint(fromClient(event, this.view.svg, this.params), last);
268
+ return;
269
+ }
270
+ if (this.selected) {
271
+ this.updateSelectedTangent(fromClient(event, this.view.svg, this.params, false), last);
272
+ }
273
+ }
274
+ updateSelectedPoint(point, last) {
275
+ if (this.selected === null)
276
+ return;
277
+ const selectedIndex = this.selected.index;
278
+ const epsilon = Math.abs(this.params.maxX - this.params.minX) * CURVE_X_EPSILON;
279
+ const minX = selectedIndex > 0 ? this.curveData.points[selectedIndex - 1].x + epsilon : this.params.minX;
280
+ const maxX = selectedIndex < this.curveData.points.length - 1
281
+ ? this.curveData.points[selectedIndex + 1].x - epsilon
282
+ : this.params.maxX;
283
+ const points = this.curveData.points.map((candidate, index) => index === selectedIndex
284
+ ? {
285
+ ...candidate,
286
+ x: clamp(point.x, minX, maxX),
287
+ y: clamp(point.y, this.params.minY, this.params.maxY),
288
+ }
289
+ : candidate);
290
+ this.setValue({ ...this.curveData, points }, true, last);
291
+ }
292
+ updateSelectedTangent(cursor, last) {
293
+ if (this.selected === null || this.selected.kind === 'point')
294
+ return;
295
+ const points = updateCurveTangent(this.curveData.points, this.selected.index, this.selected.kind, cursor, this.params);
296
+ this.setValue({ ...this.curveData, points }, true, last);
297
+ }
298
+ render() {
299
+ this.view.render(this.curveData, this.params, this.selected);
300
+ }
301
+ disposeListeners() {
302
+ const view = this.view.svg;
303
+ view.removeEventListener('pointerdown', this.onPointerDown);
304
+ view.removeEventListener('pointermove', this.onPointerMove);
305
+ view.removeEventListener('pointerup', this.onPointerEnd);
306
+ view.removeEventListener('pointercancel', this.onPointerEnd);
307
+ view.removeEventListener('lostpointercapture', this.onPointerEnd);
308
+ view.removeEventListener('dblclick', this.onDoubleClick);
309
+ view.removeEventListener('contextmenu', this.onContextMenu);
310
+ view.removeEventListener('keydown', this.onKeyDown);
311
+ }
312
+ }
313
+ class TweakpaneCurveApi extends BladeApi {
314
+ get value() {
315
+ return this.controller.getValue();
316
+ }
317
+ set value(value) {
318
+ this.controller.setValue(value);
319
+ }
320
+ on(eventName, handler) {
321
+ this.controller.emitter.on(eventName, handler);
322
+ return this;
323
+ }
324
+ off(eventName, handler) {
325
+ this.controller.emitter.off(eventName, handler);
326
+ return this;
327
+ }
328
+ }
329
+ const CurvePlugin = createPlugin({
330
+ id: 'angular-three-curve',
331
+ type: 'blade',
332
+ accept(params) {
333
+ if (params['view'] !== 'angular-three-curve' || !isCurveData(params['value']))
334
+ return null;
335
+ return {
336
+ params: {
337
+ view: 'angular-three-curve',
338
+ value: params['value'],
339
+ label: typeof params['label'] === 'string' ? params['label'] : undefined,
340
+ minX: numberParam(params['minX'], 0),
341
+ maxX: numberParam(params['maxX'], 1),
342
+ minY: numberParam(params['minY'], 0),
343
+ maxY: numberParam(params['maxY'], 1),
344
+ },
345
+ };
346
+ },
347
+ controller(args) {
348
+ return new CurveController(args.document, args.params, args.blade, args.viewProps);
349
+ },
350
+ api(args) {
351
+ return args.controller instanceof CurveController ? new TweakpaneCurveApi(args.controller) : null;
352
+ },
353
+ });
354
+ const TWEAKPANE_CURVE_PLUGIN = {
355
+ id: 'angular-three-tweakpane-curve',
356
+ plugins: [CurvePlugin],
357
+ css: `
358
+ .tweakpane-curve { padding: 4px 0; }
359
+ .tweakpane-curve__label { color: var(--lbl-fg); font-size: 11px; margin-bottom: 4px; }
360
+ .tweakpane-curve__editor { background: var(--in-bg); border-radius: var(--bld-br); display: block; height: 112px; touch-action: none; width: 100%; }
361
+ .tweakpane-curve__grid { fill: none; stroke: var(--in-fg); stroke-opacity: .12; stroke-width: 1; }
362
+ .tweakpane-curve__path { fill: none; stroke: var(--btn-bg-a); stroke-width: 2; }
363
+ .tweakpane-curve__tangent-line { pointer-events: none; stroke: var(--in-fg); stroke-opacity: .45; stroke-width: 1; }
364
+ .tweakpane-curve__tangent { cursor: crosshair; fill: var(--in-bg); stroke: var(--btn-fg); stroke-width: 1.5; }
365
+ .tweakpane-curve__tangent--selected { fill: var(--btn-bg-a); stroke: var(--btn-fg); }
366
+ .tweakpane-curve__point { cursor: grab; fill: var(--btn-fg); stroke: var(--btn-bg-a); stroke-width: 2; }
367
+ .tweakpane-curve__point--selected { fill: var(--btn-bg-a); stroke: var(--btn-fg); }
368
+ `,
369
+ };
370
+ /** A two-way Tweakpane curve editor compatible with Ecctrl's curve DTO. */
371
+ class TweakpaneCurve {
372
+ constructor() {
373
+ this.value = model.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
374
+ this.label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
375
+ this.params = input({}, ...(ngDevMode ? [{ debugName: "params" }] : /* istanbul ignore next */ []));
376
+ this.folder = inject(TweakpaneFolder);
377
+ this.pane = inject(TweakpanePane);
378
+ this.blade = inject(TweakpaneBlade);
379
+ this.api = computed(() => {
380
+ const folder = this.folder.folder();
381
+ if (!folder)
382
+ return null;
383
+ const params = this.params();
384
+ return folder.addBlade({
385
+ view: 'angular-three-curve',
386
+ value: untracked(this.value),
387
+ label: this.label(),
388
+ ...params,
389
+ });
390
+ }, ...(ngDevMode ? [{ debugName: "api" }] : /* istanbul ignore next */ []));
391
+ this.pane.registerPlugin(TWEAKPANE_CURVE_PLUGIN);
392
+ this.blade.sync(this.api);
393
+ effect((onCleanup) => {
394
+ const api = this.api();
395
+ if (!api)
396
+ return;
397
+ const onChange = (event) => this.value.set(event.value);
398
+ api.on('change', onChange);
399
+ onCleanup(() => {
400
+ api.off('change', onChange);
401
+ api.dispose();
402
+ });
403
+ });
404
+ effect(() => {
405
+ const api = this.api();
406
+ if (api)
407
+ api.value = this.value();
408
+ });
409
+ inject(DestroyRef).onDestroy(() => this.api()?.dispose());
410
+ }
411
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TweakpaneCurve, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
412
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: TweakpaneCurve, isStandalone: true, selector: "tweakpane-curve", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, params: { classPropertyName: "params", publicName: "params", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, hostDirectives: [{ directive: i1.TweakpaneBlade, inputs: ["hidden", "hidden", "disabled", "disabled"] }], ngImport: i0 }); }
413
+ }
414
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TweakpaneCurve, decorators: [{
415
+ type: Directive,
416
+ args: [{
417
+ selector: 'tweakpane-curve',
418
+ hostDirectives: [{ directive: TweakpaneBlade, inputs: ['hidden', 'disabled'] }],
419
+ }]
420
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], params: [{ type: i0.Input, args: [{ isSignal: true, alias: "params", required: false }] }] } });
421
+ function normalizeCurveData(value, params = DEFAULT_CURVE_PARAMS) {
422
+ const resolvedParams = {
423
+ minX: numberParam(params.minX, DEFAULT_CURVE_PARAMS.minX),
424
+ maxX: numberParam(params.maxX, DEFAULT_CURVE_PARAMS.maxX),
425
+ minY: numberParam(params.minY, DEFAULT_CURVE_PARAMS.minY),
426
+ maxY: numberParam(params.maxY, DEFAULT_CURVE_PARAMS.maxY),
427
+ };
428
+ const points = value.points
429
+ .map((point) => ({
430
+ ...point,
431
+ x: clamp(point.x, resolvedParams.minX, resolvedParams.maxX),
432
+ y: clamp(point.y, resolvedParams.minY, resolvedParams.maxY),
433
+ }))
434
+ .sort((a, b) => a.x - b.x)
435
+ .map((point, index, sorted) => {
436
+ const normalized = { ...point };
437
+ if (index === 0) {
438
+ delete normalized.r_in;
439
+ delete normalized.w_in;
440
+ }
441
+ if (index === sorted.length - 1) {
442
+ delete normalized.r_out;
443
+ delete normalized.w_out;
444
+ }
445
+ return normalized;
446
+ });
447
+ return {
448
+ ...value,
449
+ points,
450
+ };
451
+ }
452
+ /** @internal Pure tangent update used by the blade controller and focused tests. */
453
+ function updateCurveTangent(points, index, kind, cursor, params) {
454
+ const point = points[index];
455
+ if (!point)
456
+ return points;
457
+ const direction = kind === 'out' ? 1 : -1;
458
+ const dx = (cursor.x - point.x) * direction;
459
+ const dy = (cursor.y - point.y) * direction;
460
+ const angle = clamp(Math.atan2(dy, dx), MIN_TANGENT_ANGLE, MAX_TANGENT_ANGLE);
461
+ const weight = clamp(Math.hypot(dx, dy) / tangentUnitLength(params), 0, MAX_TANGENT_WEIGHT);
462
+ const angleKey = kind === 'in' ? 'r_in' : 'r_out';
463
+ const weightKey = kind === 'in' ? 'w_in' : 'w_out';
464
+ return points.map((candidate, candidateIndex) => candidateIndex === index ? { ...candidate, [angleKey]: angle, [weightKey]: weight } : candidate);
465
+ }
466
+ function getTangentHandle(point, kind, params) {
467
+ const direction = kind === 'out' ? 1 : -1;
468
+ const angle = kind === 'in' ? (point.r_in ?? 0) : (point.r_out ?? 0);
469
+ const weight = kind === 'in' ? (point.w_in ?? 1) : (point.w_out ?? 1);
470
+ const length = tangentUnitLength(params) * weight * direction;
471
+ return {
472
+ x: point.x + Math.cos(angle) * length,
473
+ y: point.y + Math.sin(angle) * length,
474
+ };
475
+ }
476
+ function tangentUnitLength(params) {
477
+ const xRange = Math.abs(params.maxX - params.minX);
478
+ const yRange = Math.abs(params.maxY - params.minY);
479
+ const ranges = [xRange, yRange].filter((range) => range > 1e-9);
480
+ return (ranges.length > 0 ? Math.min(...ranges) : 1) * TANGENT_LENGTH;
481
+ }
482
+ function isCurveData(value) {
483
+ return !!value && typeof value === 'object' && Array.isArray(value.points);
484
+ }
485
+ function cloneCurveData(value) {
486
+ return { ...value, points: value.points.map((point) => ({ ...point })) };
487
+ }
488
+ function numberParam(value, fallback) {
489
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
490
+ }
491
+ function clamp(value, min, max) {
492
+ return Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max));
493
+ }
494
+ function toScreen(point, params) {
495
+ const width = VIEW_WIDTH - VIEW_PADDING * 2;
496
+ const height = VIEW_HEIGHT - VIEW_PADDING * 2;
497
+ return {
498
+ x: VIEW_PADDING + ((point.x - params.minX) / Math.max(params.maxX - params.minX, 1e-9)) * width,
499
+ y: VIEW_HEIGHT - VIEW_PADDING - ((point.y - params.minY) / Math.max(params.maxY - params.minY, 1e-9)) * height,
500
+ };
501
+ }
502
+ function fromClient(event, element, params, clampToBounds = true) {
503
+ const rect = element.getBoundingClientRect();
504
+ const viewX = ((event.clientX - rect.left) / Math.max(rect.width, 1)) * VIEW_WIDTH;
505
+ const viewY = ((event.clientY - rect.top) / Math.max(rect.height, 1)) * VIEW_HEIGHT;
506
+ const rawX = (viewX - VIEW_PADDING) / (VIEW_WIDTH - VIEW_PADDING * 2);
507
+ const rawY = (viewY - VIEW_PADDING) / (VIEW_HEIGHT - VIEW_PADDING * 2);
508
+ const x = clampToBounds ? clamp(rawX, 0, 1) : rawX;
509
+ const y = clampToBounds ? clamp(rawY, 0, 1) : rawY;
510
+ return {
511
+ x: params.minX + x * (params.maxX - params.minX),
512
+ y: params.maxY - y * (params.maxY - params.minY),
513
+ };
514
+ }
515
+ function evaluateCurve(points, value) {
516
+ if (points.length === 0)
517
+ return 0;
518
+ if (points.length === 1 || value <= points[0].x)
519
+ return points[0].y;
520
+ for (let index = 1; index < points.length; index++) {
521
+ const previous = points[index - 1];
522
+ const next = points[index];
523
+ if (value > next.x)
524
+ continue;
525
+ const width = next.x - previous.x;
526
+ if (width <= 0)
527
+ return previous.y;
528
+ const t = clamp((value - previous.x) / width, 0, 1);
529
+ const t2 = t * t;
530
+ const t3 = t2 * t;
531
+ const linearSlope = (next.y - previous.y) / width;
532
+ const outgoingSlope = linearSlope +
533
+ ((previous.r_out === undefined ? 0 : Math.tan(previous.r_out)) - linearSlope) * (previous.w_out ?? 1);
534
+ const incomingSlope = linearSlope + ((next.r_in === undefined ? 0 : Math.tan(next.r_in)) - linearSlope) * (next.w_in ?? 1);
535
+ return ((2 * t3 - 3 * t2 + 1) * previous.y +
536
+ (t3 - 2 * t2 + t) * width * outgoingSlope +
537
+ (-2 * t3 + 3 * t2) * next.y +
538
+ (t3 - t2) * width * incomingSlope);
539
+ }
540
+ return points[points.length - 1].y;
541
+ }
542
+
543
+ /**
544
+ * Generated bundle index. Do not edit.
545
+ */
546
+
547
+ export { TWEAKPANE_CURVE_PLUGIN, TweakpaneCurve, TweakpaneCurveApi, normalizeCurveData };
548
+ //# sourceMappingURL=angular-three-tweakpane-curve.mjs.map