UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

245 lines 11 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Object3D, Quaternion, Vector3 } from "three"; import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js"; import { WaitForSeconds } from "../engine/engine_coroutine.js"; import { InstantiateOptions } from "../engine/engine_gameobject.js"; import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { Behaviour, GameObject } from "./Component.js"; import { DragControls, DragMode } from "./DragControls.js"; import { SyncedTransform } from "./SyncedTransform.js"; import { ObjectRaycaster } from "./ui/Raycaster.js"; /** * The [Duplicatable](https://engine.needle.tools/docs/api/Duplicatable) component creates clones of a GameObject when clicked/tapped/dragged. * Perfect for spawning objects, creating drag-and-drop inventories, or multiplayer object creation. * * ![](https://cloud.needle.tools/-/media/J_ij9vxhh1zhS8h2ftGBXQ.gif) * * **How it works:** * - When the user clicks on this object, it creates a clone of the assigned `object` * - The clone is automatically set up with {@link DragControls} so users can drag it * - If networking is enabled, clones are synced via {@link SyncedTransform} * - Rate limiting prevents spam (controlled by `limitCount`) * * **Setup tips:** * - Assign `object` to a template object (it will be hidden and used as source) * - If `object` is not assigned, the component's own GameObject is used as template * - Add an {@link ObjectRaycaster} to enable pointer detection (added automatically if missing) * * @example Basic duplicatable button * ```ts * const duplicatable = spawnButton.addComponent(Duplicatable); * duplicatable.object = templateObject; // Object to clone * duplicatable.parent = spawnContainer; // Where to place clones * duplicatable.limitCount = 10; // Max 10 per second * ``` * * @summary Duplicates a GameObject on pointer events * @category Interactivity * @group Components * @see {@link DragControls} for dragging the duplicated objects * @see {@link SyncedTransform} for networking support * @see {@link GameObject.instantiateSynced} for the underlying instantiation * @link https://engine.needle.tools/samples/collaborative-sandbox/ */ export class Duplicatable extends Behaviour { /** * Parent object for spawned duplicates. * If not set, duplicates are parented to this GameObject's parent. */ parent = null; /** * Template object to duplicate. This object will be hidden and used as the source for clones. * If not assigned, this GameObject itself is used as the template. */ object = null; /** * Maximum duplications allowed per second to prevent spam. * The counter decreases by 1 each second. * @default 60 */ limitCount = 60; _currentCount = 0; _startPosition = null; _startQuaternion = null; start() { this._currentCount = 0; this._startPosition = null; this._startQuaternion = null; if (!this.object) { this.object = this.gameObject; } if (this.object) { if (this.object === this.gameObject) { // console.error("Can not duplicate self"); // return; const instanceIdProvider = new InstantiateIdProvider(this.guid); this.object = GameObject.instantiate(this.object, { idProvider: instanceIdProvider, keepWorldPosition: false, }); const duplicatable = GameObject.getComponent(this.object, Duplicatable); duplicatable?.destroy(); let dragControls = this.object.getComponentInChildren(DragControls); if (!dragControls) { dragControls = this.object.addComponent(DragControls, { dragMode: DragMode.SnapToSurfaces }); dragControls.guid = instanceIdProvider.generateUUID(); } let syncedTransfrom = GameObject.getComponent(dragControls.gameObject, SyncedTransform); if (!syncedTransfrom) { syncedTransfrom = dragControls.gameObject.addComponent(SyncedTransform); syncedTransfrom.guid = instanceIdProvider.generateUUID(); } } this.object.visible = false; // legacy – DragControls was required for duplication and so often the component is still there; we work around that by disabling it here const dragControls = this.gameObject.getComponent(DragControls); if (dragControls) { // if (isDevEnvironment()) console.warn(`Please remove DragControls from \"${dragControls.name}\": it's not needed anymore when the object also has a Duplicatable component`); dragControls.enabled = false; } // when this is in a moveable parent in multiuser scenario somehow the object position gets an offset and might stay that way // this is just a workaround to set the object position before duplicating this._startPosition = this.object.position?.clone() ?? new Vector3(0, 0, 0); this._startQuaternion = this.object.quaternion?.clone() ?? new Quaternion(0, 0, 0, 1); } if (!this.gameObject.getComponentInParent(ObjectRaycaster)) this.gameObject.addComponent(ObjectRaycaster); } onEnable() { this.startCoroutine(this.cloneLimitIntervalFn()); } _forwardPointerEvents = new Map(); onPointerEnter(args) { if (args.used) return; if (!this.object) return; if (!this.context.connection.allowEditing) return; if (args.button !== 0) return; this.context.input.setCursor("pointer"); } onPointerExit(args) { if (args.used) return; if (!this.object) return; if (!this.context.connection.allowEditing) return; if (args.button !== 0) return; this.context.input.unsetCursor("pointer"); } /** @internal */ onPointerDown(args) { if (args.used) return; if (!this.object) return; if (!this.context.connection.allowEditing) return; if (args.button !== 0) return; const res = this.handleDuplication(); if (res) { const dragControls = GameObject.getComponent(res, DragControls); if (!dragControls) { if (isDevEnvironment()) console.warn(`Duplicated object (${res.name}) does not have DragControls`); } else { dragControls.onPointerDown(args); this._forwardPointerEvents.set(args.event.space, dragControls); } } else { if (this._currentCount >= this.limitCount) { console.warn(`[Duplicatable] Limit of ${this.limitCount} objects created within a few seconds reached. Please wait a moment before creating more objects.`); } else { console.warn(`[Duplicatable] Could not duplicate object.`); } } } /** @internal */ onPointerUp(args) { if (args.used) return; const dragControls = this._forwardPointerEvents.get(args.event.space); if (dragControls) { dragControls.onPointerUp(args); this._forwardPointerEvents.delete(args.event.space); } } *cloneLimitIntervalFn() { while (this.activeAndEnabled && !this.destroyed) { if (this._currentCount > 0) { this._currentCount -= 1; } else if (this._currentCount < 0) { this._currentCount = 0; } yield WaitForSeconds(1); } } handleDuplication() { if (!this.object) return null; if (this.limitCount > 0 && this._currentCount >= this.limitCount) return null; if (this.object === this.gameObject) return null; if (GameObject.isDestroyed(this.object)) { this.object = null; return null; } if (this.object.matrixAutoUpdate === false) { this.object.updateMatrix(); if (isDevEnvironment()) { console.warn(`Object "${this.object.name}" has matrixAutoUpdate disabled. This can cause duplicated objects to have incorrect position/rotation/scale. Consider enabling matrixAutoUpdate or calling updateMatrix() before duplication.`); showBalloonWarning("Duplicatable: Object has matrixAutoUpdate disabled"); } } this.object.visible = true; if (this._startPosition) this.object.position.copy(this._startPosition); if (this._startQuaternion) this.object.quaternion.copy(this._startQuaternion); const opts = new InstantiateOptions(); if (!this.parent) this.parent = this.gameObject.parent; if (this.parent) { opts.parent = this.parent.guid ?? this.parent.userData?.guid; opts.keepWorldPosition = true; } opts.position = this.worldPosition; opts.rotation = this.worldQuaternion; opts.context = this.context; this._currentCount += 1; const newInstance = GameObject.instantiateSynced(this.object, opts); console.assert(newInstance !== this.object, "Duplicated object is original"); this.object.visible = false; // see if this fixes object being offset when duplicated and dragged - it looks like three clone has shared position/quaternion objects? if (this._startPosition) this.object.position.clone().copy(this._startPosition); if (this._startQuaternion) this.object.quaternion.clone().copy(this._startQuaternion); return newInstance; } } __decorate([ serializable(Object3D) ], Duplicatable.prototype, "parent", void 0); __decorate([ serializable(Object3D) ], Duplicatable.prototype, "object", void 0); __decorate([ serializable() ], Duplicatable.prototype, "limitCount", void 0); //# sourceMappingURL=Duplicatable.js.map