• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-monorepo / 20661584252

02 Jan 2026 03:50PM UTC coverage: 77.924% (+7.3%) from 70.62%
20661584252

push

github

apowers313
ci: fix flakey performance test

13438 of 17822 branches covered (75.4%)

Branch coverage included in aggregate %.

41247 of 52355 relevant lines covered (78.78%)

145534.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.09
/graphty-element/src/NodeBehavior.ts
1
import {
15✔
2
    ActionManager,
14✔
3
    ExecuteCodeAction,
14✔
4
    Matrix,
14✔
5
    type Observer,
6
    PointerEventTypes,
14✔
7
    type PointerInfoPre,
8
    Ray,
9
    Scene,
10
    Vector3,
14✔
11
} from "@babylonjs/core";
14✔
12

13
import type { Graph } from "./Graph";
14
import type { GraphContext } from "./managers/GraphContext";
15
import type { Node as GraphNode, NodeIdType } from "./Node";
16

17
interface NodeBehaviorOptions {
18
    pinOnDrag?: boolean;
19
}
20

21
// Define drag state interface
22
interface DragState {
23
    dragging: boolean;
24
    dragStartMeshPosition: Vector3 | null;
25
    dragStartWorldPosition: Vector3 | null;
26
    dragPlaneNormal: Vector3 | null;
27
}
28

29
// Click detection state
30
interface ClickState {
31
    pointerDownTime: number;
32
    pointerDownPosition: { x: number; y: number };
33
    hasMoved: boolean;
34
    pointerEvent: PointerEvent | null;
35
}
36

37
// Click detection thresholds
38
const CLICK_MAX_DURATION_MS = 300; // Maximum time for a click (vs long press/drag)
15✔
39
const CLICK_MAX_MOVEMENT_PX = 5; // Maximum pixels of movement for a click
15✔
40

41
/**
42
 * Main drag handler class - unified for both desktop and XR
43
 */
44
export class NodeDragHandler {
15✔
45
    private node: GraphNode;
46
    private dragState: DragState;
47
    private clickState: ClickState | null = null;
15✔
48
    private scene: Scene;
49
    private pointerObserver: Observer<PointerInfoPre> | null = null;
15✔
50
    private hoverObserver: Observer<PointerInfoPre> | null = null;
15✔
51
    private isHovered = false;
15✔
52
    private readonly zAxisAmplification: number;
53
    private readonly enableZAmplificationInDesktop: boolean;
54

55
    /**
56
     * Creates a new drag handler for a node.
57
     * @param node - The graph node to enable dragging on
58
     */
59
    constructor(node: GraphNode) {
15✔
60
        this.node = node;
6,807✔
61
        this.scene = node.mesh.getScene();
6,807✔
62
        this.dragState = {
6,807✔
63
            dragging: false,
6,807✔
64
            dragStartMeshPosition: null,
6,807✔
65
            dragStartWorldPosition: null,
6,807✔
66
            dragPlaneNormal: null,
6,807✔
67
        };
6,807✔
68

69
        // Read config from graph context
70
        const context = this.getContext();
6,807✔
71
        const xrConfig = context.getConfig().xr;
6,807✔
72

73
        this.zAxisAmplification = xrConfig?.input.zAxisAmplification ?? 10.0;
6,807!
74
        this.enableZAmplificationInDesktop = xrConfig?.input.enableZAmplificationInDesktop ?? false;
6,807!
75

76
        // Setup pointer event listeners
77
        this.setupPointerEvents();
6,807✔
78
        this.setupHoverEvents();
6,807✔
79
    }
6,807✔
80

81
    // Public API for both desktop and XR
82
    /**
83
     * Initiates a drag operation for the node.
84
     * @param worldPosition - World space position where drag started
85
     */
86
    public onDragStart(worldPosition: Vector3): void {
15✔
87
        // Debug: console.log("🔍 [Drag] Drag Start:", {
88
        //     nodeId: this.node.id,
89
        //     isXRMode: this.isXRMode(),
90
        // });
91

92
        this.dragState.dragging = true;
29✔
93
        this.dragState.dragStartMeshPosition = this.node.mesh.position.clone();
29✔
94
        this.dragState.dragStartWorldPosition = worldPosition.clone();
29✔
95
        this.node.dragging = true;
29✔
96

97
        // Capture the drag plane orientation at drag start
98
        // This prevents the plane from rotating with camera during drag
99
        const camera = this.scene.activeCamera;
29✔
100
        if (camera) {
29✔
101
            this.dragState.dragPlaneNormal = camera.getForwardRay().direction.clone();
29✔
102

103
            // Disable camera input handler during node drag
104
            // This prevents OrbitInputController from rotating camera while dragging nodes
105
            const cameraManager = this.scene.metadata?.cameraManager;
29✔
106
            if (cameraManager) {
29✔
107
                // Debug: console.log("📷 Disabling camera input during node drag");
108
                cameraManager.temporarilyDisableInput();
29✔
109
            }
29✔
110
        }
29✔
111

112
        // Make sure graph is running
113
        const context = this.getContext();
29✔
114
        context.setRunning(true);
29✔
115

116
        // Emit node-drag-start event
117
        const eventManager = context.getEventManager?.();
29✔
118
        if (eventManager) {
29✔
119
            const pos = this.node.mesh.position;
29✔
120
            eventManager.emitNodeEvent("node-drag-start", {
29✔
121
                node: this.node,
29✔
122
                position: { x: pos.x, y: pos.y, z: pos.z },
29✔
123
            });
29✔
124
        }
29✔
125
    }
29✔
126

127
    /**
128
     * Updates node position during drag operation.
129
     * @param worldPosition - Current world space position of the drag pointer
130
     */
131
    public onDragUpdate(worldPosition: Vector3): void {
15✔
132
        if (
8✔
133
            !this.dragState.dragging ||
8✔
134
            !this.dragState.dragStartWorldPosition ||
7✔
135
            !this.dragState.dragStartMeshPosition
7✔
136
        ) {
8!
137
            return;
1✔
138
        }
1✔
139

140
        // Calculate delta from drag start
141
        const delta = worldPosition.subtract(this.dragState.dragStartWorldPosition);
7✔
142

143
        // TODO: Add back delta validation with appropriate threshold for XR mode
144
        // The previous MAX_REASONABLE_DELTA of 5.0 was too small for 10x amplification
145
        // and was blocking ALL movement in XR
146

147
        // Apply movement amplification in XR mode
148
        // In VR, all controller movements are physically constrained (not just Z-axis)
149
        // so we amplify all axes to make node manipulation practical
150
        const shouldAmplify = this.isXRMode() || this.enableZAmplificationInDesktop;
7✔
151

152
        if (shouldAmplify) {
8!
153
            delta.x *= this.zAxisAmplification;
×
154
            delta.y *= this.zAxisAmplification;
×
155
            delta.z *= this.zAxisAmplification;
×
156
        }
✔
157

158
        // Calculate new position
159
        const newPosition = this.dragState.dragStartMeshPosition.add(delta);
7✔
160

161
        // Update mesh position (triggers edge updates automatically)
162
        this.node.mesh.position.copyFrom(newPosition);
7✔
163

164
        // Update layout engine
165
        const context = this.getContext();
7✔
166
        context.getLayoutManager().layoutEngine?.setNodePosition(this.node, {
8✔
167
            x: newPosition.x,
8✔
168
            y: newPosition.y,
8✔
169
            z: newPosition.z,
8✔
170
        });
8✔
171
    }
8✔
172

173
    /**
174
     * Completes a drag operation and updates node state.
175
     */
176
    public onDragEnd(): void {
15✔
177
        if (!this.dragState.dragging) {
28!
178
            return;
×
179
        }
×
180

181
        // Debug: console.log("🏁 NodeDragHandler.onDragEnd called", {
182
        //     nodeId: this.node.id,
183
        //     finalPosition: this.node.mesh.position.asArray(),
184
        // });
185

186
        // Make sure graph is running
187
        const context = this.getContext();
28✔
188
        context.setRunning(true);
28✔
189

190
        // Pin after dragging if configured
191
        if (this.node.pinOnDrag) {
28✔
192
            this.node.pin();
27✔
193
        }
27✔
194

195
        // Re-enable camera input handler after node drag
196
        const cameraManager = this.scene.metadata?.cameraManager;
28✔
197
        if (cameraManager) {
28✔
198
            // Debug: console.log("📷 Re-enabling camera input after node drag");
199
            cameraManager.temporarilyEnableInput();
28✔
200
        }
28✔
201

202
        // Emit node-drag-end event before resetting drag state
203
        const eventManager = context.getEventManager?.();
28✔
204
        if (eventManager) {
28✔
205
            const pos = this.node.mesh.position;
28✔
206
            eventManager.emitNodeEvent("node-drag-end", {
28✔
207
                node: this.node,
28✔
208
                position: { x: pos.x, y: pos.y, z: pos.z },
28✔
209
            });
28✔
210
        }
28✔
211

212
        // Reset drag state
213
        this.node.dragging = false;
28✔
214
        this.dragState.dragging = false;
28✔
215
        this.dragState.dragStartMeshPosition = null;
28✔
216
        this.dragState.dragStartWorldPosition = null;
28✔
217
        this.dragState.dragPlaneNormal = null;
28✔
218
    }
28✔
219

220
    /**
221
     * Set node position directly (for XR mode).
222
     * XRInputHandler calculates the position with pivot transform and amplification,
223
     * then calls this method to update the node and layout engine.
224
     *
225
     * This bypasses the delta calculation in onDragUpdate() which doesn't account
226
     * for pivot rotation changes during drag.
227
     * @param newPosition - New position to set for the node
228
     */
229
    public setPositionDirect(newPosition: Vector3): void {
15✔
230
        if (!this.dragState.dragging) {
×
231
            return;
×
232
        }
×
233

234
        // Update mesh position
235
        this.node.mesh.position.copyFrom(newPosition);
×
236

237
        // Update layout engine
238
        const context = this.getContext();
×
239
        context.getLayoutManager().layoutEngine?.setNodePosition(this.node, {
×
240
            x: newPosition.x,
×
241
            y: newPosition.y,
×
242
            z: newPosition.z,
×
243
        });
×
244
    }
×
245

246
    /**
247
     * Get the node being dragged.
248
     * Used by XRInputHandler to access the node's mesh for position calculations.
249
     * @returns The graph node associated with this drag handler
250
     */
251
    public getNode(): GraphNode {
15✔
252
        return this.node;
×
253
    }
×
254

255
    // Internal methods
256
    private setupPointerEvents(): void {
15✔
257
        // Listen to pointer events for node dragging and clicking
258
        this.pointerObserver = this.scene.onPrePointerObservable.add((pointerInfo) => {
6,807✔
259
            // Skip desktop pointer handling in XR mode - XRInputHandler handles it
260
            // This prevents conflicts where XR generates pointer events that the
261
            // desktop handler would misinterpret with wrong world position calculations
262
            if (this.isXRMode()) {
174✔
263
                return;
×
264
            }
×
265

266
            switch (pointerInfo.type) {
174✔
267
                case PointerEventTypes.POINTERDOWN: {
174✔
268
                    // Check if we clicked on this node
269
                    const pickInfo = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
75✔
270

271
                    // Use nodeId from mesh metadata for comparison
272
                    // This works with both regular and instanced meshes
273
                    const pickedNodeId = pickInfo.pickedMesh?.metadata?.nodeId;
75✔
274

275
                    if (pickedNodeId === this.node.id) {
75✔
276
                        // Initialize click tracking
277
                        this.clickState = {
16✔
278
                            pointerDownTime: Date.now(),
16✔
279
                            pointerDownPosition: {
16✔
280
                                x: this.scene.pointerX,
16✔
281
                                y: this.scene.pointerY,
16✔
282
                            },
16✔
283
                            hasMoved: false,
16✔
284
                            pointerEvent: pointerInfo.event as PointerEvent,
16✔
285
                        };
16✔
286

287
                        // Get world position from pointer
288
                        const ray = this.scene.createPickingRay(
16✔
289
                            this.scene.pointerX,
16✔
290
                            this.scene.pointerY,
16✔
291
                            Matrix.Identity(),
16✔
292
                            this.scene.activeCamera,
16✔
293
                        );
16✔
294
                        const worldPosition = this.getWorldPositionFromRay(ray);
16✔
295
                        this.onDragStart(worldPosition);
16✔
296
                    }
16✔
297

298
                    break;
75✔
299
                }
75✔
300

301
                case PointerEventTypes.POINTERMOVE:
174✔
302
                    if (this.dragState.dragging) {
15✔
303
                        // Track movement for click detection
304
                        if (this.clickState && !this.clickState.hasMoved) {
×
305
                            const dx = this.scene.pointerX - this.clickState.pointerDownPosition.x;
×
306
                            const dy = this.scene.pointerY - this.clickState.pointerDownPosition.y;
×
307
                            const distance = Math.sqrt(dx * dx + dy * dy);
×
308
                            if (distance > CLICK_MAX_MOVEMENT_PX) {
×
309
                                this.clickState.hasMoved = true;
×
310
                            }
×
311
                        }
×
312

313
                        const ray = this.scene.createPickingRay(
×
314
                            this.scene.pointerX,
×
315
                            this.scene.pointerY,
×
316
                            Matrix.Identity(),
×
317
                            this.scene.activeCamera,
×
318
                        );
×
319
                        const worldPosition = this.getWorldPositionFromRay(ray);
×
320
                        this.onDragUpdate(worldPosition);
×
321
                    }
×
322

323
                    break;
15✔
324

325
                case PointerEventTypes.POINTERUP:
174✔
326
                    if (this.dragState.dragging) {
72✔
327
                        // Check if this was a click (short duration, minimal movement)
328
                        const wasClick = this.isClick();
16✔
329

330
                        this.onDragEnd();
16✔
331

332
                        // If it was a click, select this node
333
                        if (wasClick) {
16✔
334
                            this.handleClick();
16✔
335
                        }
16✔
336
                    }
16✔
337

338
                    // Reset click state
339
                    this.clickState = null;
72✔
340
                    break;
72✔
341
                default:
174✔
342
                    // Ignore other pointer events
343
                    break;
12✔
344
            }
174✔
345
        });
6,807✔
346
    }
6,807✔
347

348
    /**
349
     * Setup hover detection for emitting node-hover events.
350
     */
351
    private setupHoverEvents(): void {
15✔
352
        this.hoverObserver = this.scene.onPrePointerObservable.add((pointerInfo) => {
6,807✔
353
            // Skip in XR mode
354
            if (this.isXRMode()) {
174✔
355
                return;
×
356
            }
×
357

358
            // Only process move events for hover detection
359
            if (pointerInfo.type !== PointerEventTypes.POINTERMOVE) {
174✔
360
                return;
159✔
361
            }
159✔
362

363
            // Skip if we're currently dragging
364
            if (this.dragState.dragging) {
42✔
365
                return;
×
366
            }
✔
367

368
            // Check if we're hovering over this node
369
            const pickInfo = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
15✔
370
            const pickedNodeId = pickInfo.pickedMesh?.metadata?.nodeId;
42✔
371
            const isOverThisNode = pickedNodeId === this.node.id;
174✔
372

373
            // Emit node-hover when entering the node (not when already hovering)
374
            if (isOverThisNode && !this.isHovered) {
174✔
375
                this.isHovered = true;
×
376
                const context = this.getContext();
×
377
                const eventManager = context.getEventManager?.();
×
378
                if (eventManager) {
×
379
                    eventManager.emitNodeEvent("node-hover", {
×
380
                        node: this.node,
×
381
                        data: this.node.data,
×
382
                    });
×
383
                }
×
384
            } else if (!isOverThisNode && this.isHovered) {
42✔
385
                // Reset hover state when leaving
386
                this.isHovered = false;
×
387
            }
×
388
        });
6,807✔
389
    }
6,807✔
390

391
    /**
392
     * Check if the current pointer interaction qualifies as a click.
393
     * A click is defined as a short duration interaction with minimal movement.
394
     * @returns True if the interaction qualifies as a click
395
     */
396
    private isClick(): boolean {
15✔
397
        if (!this.clickState) {
16!
398
            return false;
×
399
        }
×
400

401
        const duration = Date.now() - this.clickState.pointerDownTime;
16✔
402
        return duration < CLICK_MAX_DURATION_MS && !this.clickState.hasMoved;
16✔
403
    }
16✔
404

405
    /**
406
     * Handle a click on this node - select it and emit node-click event.
407
     */
408
    private handleClick(): void {
15✔
409
        // Get the selection manager from the graph context
410
        const context = this.getContext();
16✔
411
        const selectionManager = context.getSelectionManager?.();
16✔
412
        if (selectionManager) {
16✔
413
            selectionManager.select(this.node);
16✔
414
        }
16✔
415

416
        // Emit node-click event
417
        const eventManager = context.getEventManager?.();
16✔
418
        if (eventManager && this.clickState?.pointerEvent) {
16✔
419
            eventManager.emitNodeEvent("node-click", {
16✔
420
                node: this.node,
16✔
421
                data: this.node.data,
16✔
422
                event: this.clickState.pointerEvent,
16✔
423
            });
16✔
424
        }
16✔
425
    }
16✔
426

427
    /**
428
     * Public API to select this node.
429
     * Called by XRInputHandler when user taps on a node without dragging.
430
     */
431
    public select(): void {
15✔
432
        this.handleClick();
×
433
    }
×
434

435
    private getWorldPositionFromRay(ray: Ray): Vector3 {
15✔
436
        // Strategy: Plane intersection parallel to camera view
437
        // This maintains predictable drag behavior
438
        const camera = this.scene.activeCamera;
16✔
439
        if (!camera) {
16!
440
            return this.node.mesh.position.clone();
×
441
        }
×
442

443
        const nodePosition = this.node.mesh.position;
16✔
444

445
        // Use stored plane normal from drag start if available
446
        // Otherwise fall back to current camera forward (for initial calculation)
447
        const cameraForward = camera.getForwardRay().direction;
16✔
448
        const planeNormal = this.dragState.dragPlaneNormal ?? cameraForward;
16✔
449

450
        // Calculate distance from camera to node along plane normal
451
        const cameraToNode = nodePosition.subtract(camera.position);
16✔
452
        const depth = Vector3.Dot(cameraToNode, planeNormal);
16✔
453

454
        // Create plane at node depth, with orientation from drag start
455
        const planePoint = camera.position.add(planeNormal.scale(depth));
16✔
456

457
        // Ray-plane intersection
458
        const denominator = Vector3.Dot(ray.direction, planeNormal);
16✔
459
        if (Math.abs(denominator) < 0.0001) {
16!
460
            // Ray parallel to plane, return current position
461
            return this.node.mesh.position.clone();
×
462
        }
×
463

464
        const t = Vector3.Dot(planePoint.subtract(ray.origin), planeNormal) / denominator;
16✔
465

466
        return ray.origin.add(ray.direction.scale(t));
16✔
467
    }
16✔
468

469
    private isXRMode(): boolean {
15✔
470
        // Check if we're in an XR session
471
        // The scene has an xrSession property when XR is active
472
        const xrHelper = this.scene.metadata?.xrHelper;
355✔
473
        return xrHelper?.baseExperience?.state === 2; // WebXRState.IN_XR
355!
474
    }
355✔
475

476
    private getContext(): GraphContext {
15✔
477
        // Check if parentGraph has GraphContext methods
478
        if ("getStyles" in this.node.parentGraph) {
6,887✔
479
            return this.node.parentGraph;
6,887✔
480
        }
6,887!
481

482
        // Otherwise, it's a Graph instance which implements GraphContext
483
        return this.node.parentGraph;
×
484
    }
6,887✔
485

486
    /**
487
     * Cleans up event observers and releases resources.
488
     */
489
    public dispose(): void {
15✔
490
        if (this.pointerObserver) {
1,263✔
491
            this.scene.onPrePointerObservable.remove(this.pointerObserver);
1,262✔
492
            this.pointerObserver = null;
1,262✔
493
        }
1,262✔
494

495
        if (this.hoverObserver) {
1,263✔
496
            this.scene.onPrePointerObservable.remove(this.hoverObserver);
1,262✔
497
            this.hoverObserver = null;
1,262✔
498
        }
1,262✔
499
    }
1,263✔
500
}
15✔
501

502
/**
503
 * Manages node interaction behaviors including dragging and clicking.
504
 * This class uses static methods to provide utility functions for node behavior management.
505
 */
506
// eslint-disable-next-line @typescript-eslint/no-extraneous-class -- Static utility class pattern for node behavior management
507
export class NodeBehavior {
15✔
508
    /**
509
     * Add default interaction behaviors to a node
510
     * @param node - The graph node to add behaviors to
511
     * @param options - Configuration options for node behaviors
512
     */
513
    static addDefaultBehaviors(node: GraphNode, options: NodeBehaviorOptions = {}): void {
15✔
514
        node.mesh.isPickable = true;
6,807✔
515

516
        // Set pinOnDrag config
517
        node.pinOnDrag = options.pinOnDrag ?? true;
6,807✔
518

519
        // Create unified drag handler (replaces SixDofDragBehavior)
520
        const dragHandler = new NodeDragHandler(node);
6,807✔
521
        node.dragHandler = dragHandler;
6,807✔
522

523
        this.addClickBehavior(node);
6,807✔
524
    }
6,807✔
525

526
    /**
527
     * Add click behavior for node expansion
528
     * @param node - The graph node to add click behavior to
529
     */
530
    private static addClickBehavior(node: GraphNode): void {
15✔
531
        // click behavior setup
532
        const context = this.getContext(node);
6,807✔
533
        const scene = context.getScene();
6,807✔
534
        node.mesh.actionManager = node.mesh.actionManager ?? new ActionManager(scene);
6,807✔
535

536
        // Available triggers:
537
        // ActionManager.OnDoublePickTrigger
538
        // ActionManager.OnRightPickTrigger
539
        // ActionManager.OnCenterPickTrigger
540
        // ActionManager.OnLongPressTrigger
541

542
        // Only Graph has fetchNodes/fetchEdges, not GraphContext
543
        // For now, check if parentGraph is the full Graph instance
544
        const graph = node.parentGraph as Graph & { fetchNodes?: unknown; fetchEdges?: unknown };
6,807✔
545
        if (graph.fetchNodes && graph.fetchEdges) {
6,807!
546
            const { fetchNodes, fetchEdges } = graph;
2✔
547

548
            node.mesh.actionManager.registerAction(
2✔
549
                new ExecuteCodeAction(
2✔
550
                    {
2✔
551
                        trigger: ActionManager.OnDoublePickTrigger,
2✔
552
                        // trigger: ActionManager.OnLongPressTrigger,
553
                    },
2✔
554
                    () => {
2✔
555
                        // make sure the graph is running
556
                        context.setRunning(true);
1✔
557

558
                        // fetch all edges for current node
559
                        const edgeSet = fetchEdges(node, graph as unknown as Graph);
1✔
560
                        const edges = Array.from(edgeSet);
1✔
561

562
                        // create set of unique node ids
563
                        const nodeIds = new Set<NodeIdType>();
1✔
564
                        edges.forEach((e) => {
1✔
565
                            nodeIds.add(e.src);
2✔
566
                            nodeIds.add(e.dst);
2✔
567
                        });
1✔
568
                        nodeIds.delete(node.id);
1✔
569

570
                        // fetch all nodes from associated edges
571
                        const nodes = fetchNodes(nodeIds, graph);
1✔
572

573
                        // add all the nodes and edges we collected
574
                        const dataManager = context.getDataManager();
1✔
575
                        dataManager.addNodes([...nodes]);
1✔
576
                        dataManager.addEdges([...edges]);
1✔
577

578
                        // TODO: fetch and add secondary edges
579
                    },
1✔
580
                ),
2✔
581
            );
2✔
582
        }
2✔
583
    }
6,807✔
584

585
    /**
586
     * Helper to get GraphContext from a Node
587
     * @param node - The graph node to get context from
588
     * @returns The graph context for the node
589
     */
590
    private static getContext(node: GraphNode): GraphContext {
15✔
591
        // Check if parentGraph has GraphContext methods
592
        if ("getStyles" in node.parentGraph) {
6,807✔
593
            return node.parentGraph;
6,807✔
594
        }
6,807!
595

596
        // Otherwise, it's a Graph instance which implements GraphContext
597
        return node.parentGraph;
×
598
    }
6,807✔
599
}
15✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc