u-space 0.0.0-alpha.2 → 0.0.2

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,97 @@
1
+ import { type ColorRepresentation } from 'three/webgpu';
2
+ import { type Viewer, Topology, type TopologyParameters, type TopologyData } from 'u-space';
3
+ export interface TopologyDrawerOptions extends TopologyParameters {
4
+ /** Y coordinate of the fallback ground plane (used when no scene object is hit). Default: 0 */
5
+ groundY?: number;
6
+ /** Color of the preview line shown while drawing. Default: 0xffff00 */
7
+ previewColor?: ColorRepresentation;
8
+ /** Snap to an existing node when clicking within this world-unit radius. Default: 1.0 */
9
+ snapRadius?: number;
10
+ }
11
+ /**
12
+ * Interactive plugin for drawing topology graphs on any surface in the scene.
13
+ *
14
+ * Detection uses `viewer.interactionManager` — events fired on hit objects bubble
15
+ * up the parent chain and are caught by `scene.addEventListener`.
16
+ * An invisible ground plane is added to the scene as a fallback so that clicks
17
+ * on empty space (no other object hit) still produce a valid world point.
18
+ *
19
+ * Controls (while enabled):
20
+ * - Left-click : Place a node on the surface under the cursor
21
+ * (snaps to nearest existing node within snapRadius)
22
+ * - Right-click / Escape : Finish the current path chain
23
+ * - Ctrl/Cmd + Z : Undo the last action
24
+ */
25
+ declare class TopologyDrawer {
26
+ viewer: Viewer;
27
+ topology: Topology;
28
+ private _snapRadius;
29
+ private _scene;
30
+ private _groundMesh;
31
+ private _previewLine;
32
+ private _previewGeometry;
33
+ private _lastNodeId;
34
+ private _nodeCounter;
35
+ private _history;
36
+ private _enabled;
37
+ private _prevFrameloop;
38
+ private _prevPointerMoveEnabled;
39
+ constructor(viewer: Viewer, options?: TopologyDrawerOptions);
40
+ private _findNearestNode;
41
+ private _setPreviewLine;
42
+ /**
43
+ * Resolve the world-space placement point from an InteractionEvent.
44
+ *
45
+ * - If the original hit was a topology node, return its registered position
46
+ * (more accurate than the sphere surface point).
47
+ * - If the hit was a topology edge, return the surface hit point so a new
48
+ * node can be inserted at that location and connected.
49
+ * - Otherwise use intersect.point directly.
50
+ */
51
+ private _resolvePoint;
52
+ private _placePoint;
53
+ /**
54
+ * Insert a new node into an existing edge (split it).
55
+ *
56
+ * The original edge (cutFrom ↔ cutTo) is removed and replaced with two
57
+ * edges: cutFrom → newNode → cutTo.
58
+ * If a path chain is active (_lastNodeId set, and different from the edge
59
+ * endpoints), an additional edge _lastNodeId → newNode is added.
60
+ */
61
+ private _splitEdge;
62
+ private _onSceneClick;
63
+ private _onScenePointerMove;
64
+ private _onSceneContextMenu;
65
+ private _onKeyDown;
66
+ /** Finish the current path chain. Next click starts an unconnected new path. */
67
+ finishPath(): void;
68
+ /** Undo the last action (node, edge, or finishPath). */
69
+ undo(): void;
70
+ /** Remove all drawn nodes, edges, and path meshes. */
71
+ clear(): void;
72
+ /** Export the current topology as a JSON-serializable object. Delegates to `topology.exportData()`. */
73
+ exportData(): TopologyData;
74
+ /**
75
+ * Load a previously exported topology, replacing any current drawing.
76
+ * Delegates to `topology.importData()` and resets the drawer's internal node counter.
77
+ */
78
+ importData(data: TopologyData): void;
79
+ /**
80
+ * Enable drawing mode.
81
+ *
82
+ * - Adds the topology, preview line, and fallback ground plane to the scene.
83
+ * - Registers `click`, `pointermove`, `contextmenu` listeners on `viewer.scene`
84
+ * (events are bubbled up by InteractionManager from whichever object is hit).
85
+ * - Enables `pointerMoveEventsEnabled` on the InteractionManager for the
86
+ * preview line to track the cursor.
87
+ */
88
+ enable(): this;
89
+ /**
90
+ * Disable drawing mode.
91
+ * The drawn topology remains visible in the scene.
92
+ */
93
+ disable(): this;
94
+ /** Fully dispose all resources and remove the topology from the scene. */
95
+ dispose(): void;
96
+ }
97
+ export { TopologyDrawer };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("three/webgpu"),r=require("u-space");class h{viewer;topology;_snapRadius;_scene;_groundMesh;_previewLine;_previewGeometry;_lastNodeId=null;_nodeCounter=0;_history=[];_enabled=!1;_prevFrameloop="demand";_prevPointerMoveEnabled=!1;constructor(e,t){this.viewer=e,this._snapRadius=t?.snapRadius??1,this._scene=e.scene,this.topology=new r.Topology({nodeColor:t?.nodeColor,nodeRadius:t?.nodeRadius,edgeColor:t?.edgeColor,edgeRadius:t?.edgeRadius,pathColor:t?.pathColor,pathRadius:t?.pathRadius});const o=new d.PlaneGeometry(1e5,1e5);o.rotateX(-Math.PI/2),this._groundMesh=new d.Mesh(o,new d.MeshBasicNodeMaterial({visible:!1})),this._groundMesh.position.y=t?.groundY??0,this._previewGeometry=new d.BufferGeometry,this._previewGeometry.setAttribute("position",new d.Float32BufferAttribute(new Float32Array(6),3)),this._previewLine=new d.Line(this._previewGeometry,new d.LineBasicNodeMaterial({color:t?.previewColor??16776960,depthTest:!1})),this._previewLine.renderOrder=10,this._previewLine.visible=!1}_findNearestNode(e){let t=this._snapRadius,o=null;return this.topology.nodes.forEach((i,s)=>{const n=e.distanceTo(i);n<t&&(t=n,o=s)}),o}_setPreviewLine(e,t){const o=this._previewGeometry.attributes.position;o.setXYZ(0,e.x,e.y,e.z),o.setXYZ(1,t.x,t.y,t.z),o.needsUpdate=!0,this._previewLine.visible=!0}_resolvePoint(e){const t=e.intersect?.point;if(!t)return null;const{type:o,id:i}=e.target.userData;return o==="node"&&i?this.topology.nodes.get(i)?.clone()??t.clone():t.clone()}_placePoint(e){const t=this._findNearestNode(e);if(t){this._lastNodeId&&this._lastNodeId!==t&&(this.topology.addEdge(this._lastNodeId,t),this._history.push({type:"addEdge",from:this._lastNodeId,to:t,prevLastNodeId:this._lastNodeId}),this._lastNodeId=t,this.topology.renderGraph());return}const o=`node_${this._nodeCounter++}`;this.topology.addNode(o,e);const i={type:"addNode",nodeId:o,prevLastNodeId:this._lastNodeId};this._lastNodeId&&(this.topology.addEdge(this._lastNodeId,o),i.addedEdge={from:this._lastNodeId,to:o}),this._history.push(i),this._lastNodeId=o,this.topology.renderGraph()}_splitEdge(e,t,o,i){const s=`node_${this._nodeCounter++}`;this.topology.addNode(s,e),this.topology.removeEdge(t,o),this.topology.addEdge(t,s),this.topology.addEdge(s,o);const n={type:"splitEdge",nodeId:s,cutFrom:t,cutTo:o,cutWeight:i,prevLastNodeId:this._lastNodeId};this._lastNodeId&&this._lastNodeId!==t&&this._lastNodeId!==o&&(this.topology.addEdge(this._lastNodeId,s),n.chainEdge={from:this._lastNodeId,to:s}),this._history.push(n),this._lastNodeId=s,this.topology.renderGraph()}_onSceneClick=({event:e})=>{const t=e.target.userData;if(t.type==="edge"&&t.from&&t.to){const i=e.intersect?.point;if(!i)return;this._splitEdge(i.clone(),t.from,t.to,t.weight??0);return}const o=this._resolvePoint(e);o&&this._placePoint(o)};_onScenePointerMove=({event:e})=>{if(!this._lastNodeId)return;const t=e.intersect?.point;if(!t)return;const o=this.topology.nodes.get(this._lastNodeId);o&&this._setPreviewLine(o,t)};_onSceneContextMenu=({event:e})=>{e.originalEvent.preventDefault(),this.finishPath()};_onKeyDown=e=>{e.key==="Escape"?this.finishPath():(e.ctrlKey||e.metaKey)&&e.key==="z"&&(e.preventDefault(),this.undo())};finishPath(){this._history.push({type:"finishPath",prevLastNodeId:this._lastNodeId}),this._lastNodeId=null,this._previewLine.visible=!1}undo(){const e=this._history.pop();if(e)if(e.type==="addNode"?(e.addedEdge&&this.topology.removeEdge(e.addedEdge.from,e.addedEdge.to),this.topology.removeNode(e.nodeId),this._lastNodeId=e.prevLastNodeId,this.topology.renderGraph()):e.type==="addEdge"?(this.topology.removeEdge(e.from,e.to),this._lastNodeId=e.prevLastNodeId,this.topology.renderGraph()):e.type==="splitEdge"?(this.topology.removeEdge(e.cutFrom,e.nodeId),this.topology.removeEdge(e.nodeId,e.cutTo),e.chainEdge&&this.topology.removeEdge(e.chainEdge.from,e.chainEdge.to),this.topology.removeNode(e.nodeId),this.topology.addEdge(e.cutFrom,e.cutTo,e.cutWeight),this._lastNodeId=e.prevLastNodeId,this.topology.renderGraph()):e.type==="finishPath"&&(this._lastNodeId=e.prevLastNodeId),this._lastNodeId){const t=this.topology.nodes.get(this._lastNodeId);t&&this._setPreviewLine(t,t)}else this._previewLine.visible=!1}clear(){this.topology.clearGraph(),this.topology.clearPaths(),this.topology.nodes.clear(),this.topology.adjacencyMap.clear(),this._lastNodeId=null,this._nodeCounter=0,this._history=[],this._previewLine.visible=!1}exportData(){return this.topology.exportData()}importData(e){this._lastNodeId=null,this._history=[],this._previewLine.visible=!1,this.topology.importData(e),Object.keys(e.nodes).forEach(t=>{const o=parseInt(t.replace("node_",""),10);!isNaN(o)&&o>=this._nodeCounter&&(this._nodeCounter=o+1)})}enable(){return this._enabled?this:(this._enabled=!0,this._prevFrameloop=this.viewer.frameloop,this._prevPointerMoveEnabled=this.viewer.interactionManager.pointerMoveEventsEnabled,this.viewer.frameloop="always",this.viewer.interactionManager.pointerMoveEventsEnabled=!0,this.viewer.scene.add(this._groundMesh),this.viewer.scene.add(this.topology),this.viewer.scene.add(this._previewLine),this._scene.addEventListener("click",this._onSceneClick),this._scene.addEventListener("pointermove",this._onScenePointerMove),this._scene.addEventListener("rightclick",this._onSceneContextMenu),window.addEventListener("keydown",this._onKeyDown),this)}disable(){return this._enabled?(this._enabled=!1,this.viewer.frameloop=this._prevFrameloop,this.viewer.interactionManager.pointerMoveEventsEnabled=this._prevPointerMoveEnabled,this.viewer.scene.remove(this._groundMesh),this.viewer.scene.remove(this._previewLine),this._previewLine.visible=!1,this._scene.removeEventListener("click",this._onSceneClick),this._scene.removeEventListener("pointermove",this._onScenePointerMove),this._scene.removeEventListener("rightclick",this._onSceneContextMenu),window.removeEventListener("keydown",this._onKeyDown),this):this}dispose(){this.disable(),this.viewer.scene.remove(this.topology),this._groundMesh.geometry.dispose(),this._groundMesh.material.dispose(),this._previewGeometry.dispose(),this._previewLine.material.dispose(),this.topology.dispose()}}exports.TopologyDrawer=h;
@@ -0,0 +1 @@
1
+ export * from './TopologyDrawer';
@@ -0,0 +1,182 @@
1
+ import { PlaneGeometry as n, Mesh as r, MeshBasicNodeMaterial as h, BufferGeometry as a, Float32BufferAttribute as l, Line as p, LineBasicNodeMaterial as _ } from "three/webgpu";
2
+ import { Topology as v } from "u-space";
3
+ class u {
4
+ viewer;
5
+ topology;
6
+ _snapRadius;
7
+ // Typed reference to viewer.scene for InteractionEventMap listeners
8
+ _scene;
9
+ // Invisible ground plane added to scene as raycast fallback for empty space
10
+ _groundMesh;
11
+ _previewLine;
12
+ _previewGeometry;
13
+ _lastNodeId = null;
14
+ _nodeCounter = 0;
15
+ _history = [];
16
+ _enabled = !1;
17
+ _prevFrameloop = "demand";
18
+ _prevPointerMoveEnabled = !1;
19
+ constructor(e, t) {
20
+ this.viewer = e, this._snapRadius = t?.snapRadius ?? 1, this._scene = e.scene, this.topology = new v({
21
+ nodeColor: t?.nodeColor,
22
+ nodeRadius: t?.nodeRadius,
23
+ edgeColor: t?.edgeColor,
24
+ edgeRadius: t?.edgeRadius,
25
+ pathColor: t?.pathColor,
26
+ pathRadius: t?.pathRadius
27
+ });
28
+ const o = new n(1e5, 1e5);
29
+ o.rotateX(-Math.PI / 2), this._groundMesh = new r(o, new h({ visible: !1 })), this._groundMesh.position.y = t?.groundY ?? 0, this._previewGeometry = new a(), this._previewGeometry.setAttribute("position", new l(new Float32Array(6), 3)), this._previewLine = new p(
30
+ this._previewGeometry,
31
+ new _({ color: t?.previewColor ?? 16776960, depthTest: !1 })
32
+ ), this._previewLine.renderOrder = 10, this._previewLine.visible = !1;
33
+ }
34
+ // ─── Private helpers ──────────────────────────────────────────────────────
35
+ _findNearestNode(e) {
36
+ let t = this._snapRadius, o = null;
37
+ return this.topology.nodes.forEach((i, s) => {
38
+ const d = e.distanceTo(i);
39
+ d < t && (t = d, o = s);
40
+ }), o;
41
+ }
42
+ _setPreviewLine(e, t) {
43
+ const o = this._previewGeometry.attributes.position;
44
+ o.setXYZ(0, e.x, e.y, e.z), o.setXYZ(1, t.x, t.y, t.z), o.needsUpdate = !0, this._previewLine.visible = !0;
45
+ }
46
+ /**
47
+ * Resolve the world-space placement point from an InteractionEvent.
48
+ *
49
+ * - If the original hit was a topology node, return its registered position
50
+ * (more accurate than the sphere surface point).
51
+ * - If the hit was a topology edge, return the surface hit point so a new
52
+ * node can be inserted at that location and connected.
53
+ * - Otherwise use intersect.point directly.
54
+ */
55
+ _resolvePoint(e) {
56
+ const t = e.intersect?.point;
57
+ if (!t) return null;
58
+ const { type: o, id: i } = e.target.userData;
59
+ return o === "node" && i ? this.topology.nodes.get(i)?.clone() ?? t.clone() : t.clone();
60
+ }
61
+ _placePoint(e) {
62
+ const t = this._findNearestNode(e);
63
+ if (t) {
64
+ this._lastNodeId && this._lastNodeId !== t && (this.topology.addEdge(this._lastNodeId, t), this._history.push({
65
+ type: "addEdge",
66
+ from: this._lastNodeId,
67
+ to: t,
68
+ prevLastNodeId: this._lastNodeId
69
+ }), this._lastNodeId = t, this.topology.renderGraph());
70
+ return;
71
+ }
72
+ const o = `node_${this._nodeCounter++}`;
73
+ this.topology.addNode(o, e);
74
+ const i = { type: "addNode", nodeId: o, prevLastNodeId: this._lastNodeId };
75
+ this._lastNodeId && (this.topology.addEdge(this._lastNodeId, o), i.addedEdge = { from: this._lastNodeId, to: o }), this._history.push(i), this._lastNodeId = o, this.topology.renderGraph();
76
+ }
77
+ /**
78
+ * Insert a new node into an existing edge (split it).
79
+ *
80
+ * The original edge (cutFrom ↔ cutTo) is removed and replaced with two
81
+ * edges: cutFrom → newNode → cutTo.
82
+ * If a path chain is active (_lastNodeId set, and different from the edge
83
+ * endpoints), an additional edge _lastNodeId → newNode is added.
84
+ */
85
+ _splitEdge(e, t, o, i) {
86
+ const s = `node_${this._nodeCounter++}`;
87
+ this.topology.addNode(s, e), this.topology.removeEdge(t, o), this.topology.addEdge(t, s), this.topology.addEdge(s, o);
88
+ const d = {
89
+ type: "splitEdge",
90
+ nodeId: s,
91
+ cutFrom: t,
92
+ cutTo: o,
93
+ cutWeight: i,
94
+ prevLastNodeId: this._lastNodeId
95
+ };
96
+ this._lastNodeId && this._lastNodeId !== t && this._lastNodeId !== o && (this.topology.addEdge(this._lastNodeId, s), d.chainEdge = { from: this._lastNodeId, to: s }), this._history.push(d), this._lastNodeId = s, this.topology.renderGraph();
97
+ }
98
+ // ─── Scene-level InteractionManager event handlers ────────────────────────
99
+ _onSceneClick = ({ event: e }) => {
100
+ const t = e.target.userData;
101
+ if (t.type === "edge" && t.from && t.to) {
102
+ const i = e.intersect?.point;
103
+ if (!i) return;
104
+ this._splitEdge(i.clone(), t.from, t.to, t.weight ?? 0);
105
+ return;
106
+ }
107
+ const o = this._resolvePoint(e);
108
+ o && this._placePoint(o);
109
+ };
110
+ _onScenePointerMove = ({ event: e }) => {
111
+ if (!this._lastNodeId) return;
112
+ const t = e.intersect?.point;
113
+ if (!t) return;
114
+ const o = this.topology.nodes.get(this._lastNodeId);
115
+ o && this._setPreviewLine(o, t);
116
+ };
117
+ _onSceneContextMenu = ({ event: e }) => {
118
+ e.originalEvent.preventDefault(), this.finishPath();
119
+ };
120
+ _onKeyDown = (e) => {
121
+ e.key === "Escape" ? this.finishPath() : (e.ctrlKey || e.metaKey) && e.key === "z" && (e.preventDefault(), this.undo());
122
+ };
123
+ // ─── Public API ───────────────────────────────────────────────────────────
124
+ /** Finish the current path chain. Next click starts an unconnected new path. */
125
+ finishPath() {
126
+ this._history.push({ type: "finishPath", prevLastNodeId: this._lastNodeId }), this._lastNodeId = null, this._previewLine.visible = !1;
127
+ }
128
+ /** Undo the last action (node, edge, or finishPath). */
129
+ undo() {
130
+ const e = this._history.pop();
131
+ if (e)
132
+ if (e.type === "addNode" ? (e.addedEdge && this.topology.removeEdge(e.addedEdge.from, e.addedEdge.to), this.topology.removeNode(e.nodeId), this._lastNodeId = e.prevLastNodeId, this.topology.renderGraph()) : e.type === "addEdge" ? (this.topology.removeEdge(e.from, e.to), this._lastNodeId = e.prevLastNodeId, this.topology.renderGraph()) : e.type === "splitEdge" ? (this.topology.removeEdge(e.cutFrom, e.nodeId), this.topology.removeEdge(e.nodeId, e.cutTo), e.chainEdge && this.topology.removeEdge(e.chainEdge.from, e.chainEdge.to), this.topology.removeNode(e.nodeId), this.topology.addEdge(e.cutFrom, e.cutTo, e.cutWeight), this._lastNodeId = e.prevLastNodeId, this.topology.renderGraph()) : e.type === "finishPath" && (this._lastNodeId = e.prevLastNodeId), this._lastNodeId) {
133
+ const t = this.topology.nodes.get(this._lastNodeId);
134
+ t && this._setPreviewLine(t, t);
135
+ } else
136
+ this._previewLine.visible = !1;
137
+ }
138
+ /** Remove all drawn nodes, edges, and path meshes. */
139
+ clear() {
140
+ this.topology.clearGraph(), this.topology.clearPaths(), this.topology.nodes.clear(), this.topology.adjacencyMap.clear(), this._lastNodeId = null, this._nodeCounter = 0, this._history = [], this._previewLine.visible = !1;
141
+ }
142
+ /** Export the current topology as a JSON-serializable object. Delegates to `topology.exportData()`. */
143
+ exportData() {
144
+ return this.topology.exportData();
145
+ }
146
+ /**
147
+ * Load a previously exported topology, replacing any current drawing.
148
+ * Delegates to `topology.importData()` and resets the drawer's internal node counter.
149
+ */
150
+ importData(e) {
151
+ this._lastNodeId = null, this._history = [], this._previewLine.visible = !1, this.topology.importData(e), Object.keys(e.nodes).forEach((t) => {
152
+ const o = parseInt(t.replace("node_", ""), 10);
153
+ !isNaN(o) && o >= this._nodeCounter && (this._nodeCounter = o + 1);
154
+ });
155
+ }
156
+ /**
157
+ * Enable drawing mode.
158
+ *
159
+ * - Adds the topology, preview line, and fallback ground plane to the scene.
160
+ * - Registers `click`, `pointermove`, `contextmenu` listeners on `viewer.scene`
161
+ * (events are bubbled up by InteractionManager from whichever object is hit).
162
+ * - Enables `pointerMoveEventsEnabled` on the InteractionManager for the
163
+ * preview line to track the cursor.
164
+ */
165
+ enable() {
166
+ return this._enabled ? this : (this._enabled = !0, this._prevFrameloop = this.viewer.frameloop, this._prevPointerMoveEnabled = this.viewer.interactionManager.pointerMoveEventsEnabled, this.viewer.frameloop = "always", this.viewer.interactionManager.pointerMoveEventsEnabled = !0, this.viewer.scene.add(this._groundMesh), this.viewer.scene.add(this.topology), this.viewer.scene.add(this._previewLine), this._scene.addEventListener("click", this._onSceneClick), this._scene.addEventListener("pointermove", this._onScenePointerMove), this._scene.addEventListener("rightclick", this._onSceneContextMenu), window.addEventListener("keydown", this._onKeyDown), this);
167
+ }
168
+ /**
169
+ * Disable drawing mode.
170
+ * The drawn topology remains visible in the scene.
171
+ */
172
+ disable() {
173
+ return this._enabled ? (this._enabled = !1, this.viewer.frameloop = this._prevFrameloop, this.viewer.interactionManager.pointerMoveEventsEnabled = this._prevPointerMoveEnabled, this.viewer.scene.remove(this._groundMesh), this.viewer.scene.remove(this._previewLine), this._previewLine.visible = !1, this._scene.removeEventListener("click", this._onSceneClick), this._scene.removeEventListener("pointermove", this._onScenePointerMove), this._scene.removeEventListener("rightclick", this._onSceneContextMenu), window.removeEventListener("keydown", this._onKeyDown), this) : this;
174
+ }
175
+ /** Fully dispose all resources and remove the topology from the scene. */
176
+ dispose() {
177
+ this.disable(), this.viewer.scene.remove(this.topology), this._groundMesh.geometry.dispose(), this._groundMesh.material.dispose(), this._previewGeometry.dispose(), this._previewLine.material.dispose(), this.topology.dispose();
178
+ }
179
+ }
180
+ export {
181
+ u as TopologyDrawer
182
+ };
@@ -1,5 +1,5 @@
1
1
  import type { Object3D, Intersection, Object3DEventMap } from 'three/webgpu';
2
- export type InteractionEventType = 'click' | 'dblclick' | 'contextmenu' | 'pointerdown' | 'pointerup' | 'pointermove' | 'pointerenter' | 'pointerleave';
2
+ export type InteractionEventType = 'click' | 'dblclick' | 'rightclick' | 'pointerdown' | 'pointerup' | 'pointermove' | 'pointerenter' | 'pointerleave';
3
3
  export interface InteractionEventParameters {
4
4
  type: InteractionEventType;
5
5
  target: Object3D<InteractionEventMap>;
@@ -17,10 +17,9 @@ export declare class InteractionManager {
17
17
  private longPressThreshold;
18
18
  private moveThreshold;
19
19
  /**
20
- * The objects to check for intersections.
21
- * If not set, it defaults to the children of the scene.
20
+ * The objects to check for intersections. default to all children of the scene (including nested).
22
21
  */
23
- targetObjects: Object3D<InteractionEventMap>[] | null;
22
+ targetObjects: Object3D<InteractionEventMap>[];
24
23
  /**
25
24
  * Whether to enable pointer move events (pointermove, pointerenter, pointerleave).
26
25
  * Disabled by default for performance optimization.
@@ -1,7 +1,19 @@
1
- import { type Vector3, Group, type ColorRepresentation } from 'three/webgpu';
1
+ import { Vector3, Group, type ColorRepresentation } from 'three/webgpu';
2
2
  import type { InteractionEventMap } from '../interactions';
3
3
  import { TubeMesh } from './TubeMesh';
4
4
  import { BaseGroup } from './BaseGroup';
5
+ export type TopologyData = {
6
+ nodes: Record<string, {
7
+ x: number;
8
+ y: number;
9
+ z: number;
10
+ }>;
11
+ edges: Array<{
12
+ from: string;
13
+ to: string;
14
+ weight: number;
15
+ }>;
16
+ };
5
17
  export interface TopologyParameters {
6
18
  nodeColor?: ColorRepresentation;
7
19
  nodeRadius?: number;
@@ -59,6 +71,13 @@ export declare class Topology extends BaseGroup<InteractionEventMap> {
59
71
  */
60
72
  renderPath(points: Vector3[], color?: ColorRepresentation): TubeMesh | undefined;
61
73
  clearPaths(): void;
74
+ /** Export nodes and edges as a JSON-serializable object. */
75
+ exportData(): TopologyData;
76
+ /**
77
+ * Load topology from a previously exported data object.
78
+ * Replaces all current nodes, edges, and path meshes, then re-renders the graph.
79
+ */
80
+ importData(data: TopologyData): void;
62
81
  dispose(): void;
63
82
  getNeighbors(id: string): Map<string, number> | undefined;
64
83
  }
@@ -1,10 +1,10 @@
1
1
  # Animations API
2
2
 
3
- `u-space` provides a `Tween` class and a `tweenAnimation` helper for animating arbitrary numeric properties. Both are thin wrappers over [`@tweenjs/tween.js`](https://github.com/tweenjs/tween.js) that integrate automatically with the `Viewer` render loop.
3
+ `u-space` 提供了 `Tween` 类和 `tweenAnimation` 辅助函数,用于对任意数值属性进行动画处理。两者都是对 [`@tweenjs/tween.js`](https://github.com/tweenjs/tween.js) 的轻量封装,并自动与 `Viewer` 渲染循环集成。
4
4
 
5
5
  ## `tweenAnimation`
6
6
 
7
- The simplest way to animate any object's properties. Returns a `Promise` that resolves when the animation completes.
7
+ 对任意对象属性执行动画的最简方式,返回一个在动画完成后 resolve 的 `Promise`。
8
8
 
9
9
  ```typescript
10
10
  import { tweenAnimation } from 'u-space';
@@ -13,17 +13,17 @@ const source = { x: 0, y: 0, z: 0 };
13
13
 
14
14
  await tweenAnimation(
15
15
  viewer,
16
- source, // mutable start state (mutated each frame)
17
- { x: 10, y: 5, z: 10 }, // target state
16
+ source, // 可变的起始状态(每帧被修改)
17
+ { x: 10, y: 5, z: 10 }, // 目标状态
18
18
  {
19
- duration: 1500, // ms
19
+ duration: 1500, // 毫秒
20
20
  delay: 0,
21
21
  mode: 'Cubic.InOut',
22
22
  repeat: false,
23
23
  yoyo: false,
24
24
  },
25
25
  (current) => {
26
- // Called every frame with the interpolated values
26
+ // 每帧以插值结果调用
27
27
  myObject.position.set(current.x, current.y, current.z);
28
28
  },
29
29
  );
@@ -31,23 +31,23 @@ await tweenAnimation(
31
31
 
32
32
  ### `AnimationOptions`
33
33
 
34
- | Property | Type | Default | Description |
35
- | :--------- | :-------------------------- | :-------------- | :----------------------------------------------------------- |
36
- | `duration` | `number` | `1000` | Duration in milliseconds. |
37
- | `delay` | `number` | `0` | Start delay in milliseconds. |
38
- | `mode` | `AnimationModeType` | `'Linear.None'` | Easing function. |
39
- | `repeat` | `number \| boolean` | `false` | Number of extra repeats, or `true` for infinite. |
40
- | `yoyo` | `boolean` | `false` | Reverse on each repeat cycle. |
34
+ | 属性 | 类型 | 默认值 | 说明 |
35
+ | :--------- | :-------------------------- | :-------------- | :------------------------------------------------- |
36
+ | `duration` | `number` | `1000` | 动画时长(毫秒)。 |
37
+ | `delay` | `number` | `0` | 开始延迟(毫秒)。 |
38
+ | `mode` | `AnimationModeType` | `'Linear.None'` | 缓动函数。 |
39
+ | `repeat` | `number \| boolean` | `false` | 额外重复次数,或 `true` 表示无限循环。 |
40
+ | `yoyo` | `boolean` | `false` | 在每个循环周期反向播放。 |
41
41
 
42
42
  ### `AnimationModeType`
43
43
 
44
- All standard easing modes are supported:
44
+ 支持所有标准缓动模式:
45
45
 
46
46
  `Linear.None` · `Quadratic.In/Out/InOut` · `Cubic.In/Out/InOut` · `Quartic.In/Out/InOut` · `Quintic.In/Out/InOut` · `Sinusoidal.In/Out/InOut` · `Exponential.In/Out/InOut` · `Circular.In/Out/InOut` · `Elastic.In/Out/InOut` · `Back.In/Out/InOut` · `Bounce.In/Out/InOut`
47
47
 
48
48
  ## `Tween`
49
49
 
50
- A lower-level class for full control. Extends the base `Tween` from `tween.js` and hooks into the `Viewer` event loop via `addEventListener('afterControlsUpdate', ...)`.
50
+ 提供完全控制的底层类。继承自 `tween.js` 的基础 `Tween`,并通过 `addEventListener('afterControlsUpdate', ...)` 挂入 `Viewer` 事件循环。
51
51
 
52
52
  ```typescript
53
53
  import { Tween } from 'u-space';
@@ -61,18 +61,18 @@ const tween = new Tween(viewer, source)
61
61
  myMaterial.opacity = s.opacity;
62
62
  viewer.render();
63
63
  })
64
- .onComplete(() => console.log('done'));
64
+ .onComplete(() => console.log('完成'));
65
65
 
66
66
  tween.start();
67
67
  // tween.stop();
68
68
  ```
69
69
 
70
- ### Methods
70
+ ### 方法
71
71
 
72
- | Method | Description |
73
- | :------------------------ | :------------------------------------------------------------------ |
74
- | `easingByMode(mode)` | Convenience shorthand for `.easing(...)` using `AnimationModeType`. |
75
- | `start(time?)` | Starts the tween and registers it with the viewer loop. |
76
- | `stop()` | Stops the tween and unregisters it from the viewer loop. |
72
+ | 方法 | 说明 |
73
+ | :------------------------ | :-------------------------------------------------------------- |
74
+ | `easingByMode(mode)` | 使用 `AnimationModeType` 设置缓动函数的便捷简写。 |
75
+ | `start(time?)` | 启动补间并注册到 viewer 循环中。 |
76
+ | `stop()` | 停止补间并从 viewer 循环中注销。 |
77
77
 
78
- All other methods (`to`, `delay`, `repeat`, `yoyo`, `onUpdate`, `onComplete`, `onStop`, `onStart`) are inherited from the base `tween.js` `Tween` class.
78
+ 其他方法(`to`、`delay`、`repeat`、`yoyo`、`onUpdate`、`onComplete`、`onStop`、`onStart`)均继承自 `tween.js` 的基础 `Tween` 类。
@@ -1,50 +1,50 @@
1
1
  # Effects API
2
2
 
3
- `u-space` provides two static effect utilities built on the Three.js Shading Language (TSL/WebGPU nodes): `MaterialEffects` for applying highlight states to objects, and `TSLEffects` for generating animated color node patterns.
3
+ `u-space` 提供了两个基于 Three.js 着色语言(TSL/WebGPU 节点)的静态特效工具:`MaterialEffects` 用于为对象应用高亮状态,`TSLEffects` 用于生成动态颜色节点模式。
4
4
 
5
5
  ## `MaterialEffects`
6
6
 
7
- Static utility that applies TSL-based visual effects directly to an object's materials. Works on any `Object3D` — traverses all child meshes automatically.
7
+ 静态工具类,将基于 TSL 的视觉特效直接应用于对象的材质。适用于任何 `Object3D`,会自动遍历所有子网格。
8
8
 
9
9
  ### `MaterialEffects.highlight(object, options?)`
10
10
 
11
- Applies a color/opacity highlight to all meshes in an object. Uses `userData` and TSL node graphs so multiple objects can share the same node graph instance with per-mesh state.
11
+ 为对象中所有网格应用颜色/透明度高亮。使用 `userData` 和 TSL 节点图,多个对象可共享同一节点图实例,各自保持独立状态。
12
12
 
13
13
  ```typescript
14
14
  import { MaterialEffects } from 'u-space';
15
15
 
16
- // Highlight an object red with 50% opacity
16
+ // 以 50% 透明度高亮为红色
17
17
  MaterialEffects.highlight(myModel, {
18
18
  enabled: true,
19
19
  color: 0xff0000,
20
20
  opacity: 0.5,
21
- overwrite: false, // false = tint (multiply), true = replace color
21
+ overwrite: false, // false = 叠加(相乘),true = 完全替换颜色
22
22
  });
23
23
 
24
- // Disable the highlight
24
+ // 禁用高亮
25
25
  MaterialEffects.highlight(myModel, { enabled: false });
26
26
  ```
27
27
 
28
28
  ### `HighlightOptions`
29
29
 
30
- | Property | Type | Default | Description |
31
- | :--------- | :-------------------- | :---------- | :----------------------------------------------------------------------------------- |
32
- | `enabled` | `boolean` | `true` | Enables or disables the highlight effect. |
33
- | `color` | `ColorRepresentation` | `0xff0000` | Highlight color. |
34
- | `opacity` | `number` | `0.5` | Material opacity when highlighted. |
35
- | `overwrite`| `boolean` | `false` | `false` = multiply with original color (tint); `true` = replace color entirely. |
30
+ | 属性 | 类型 | 默认值 | 说明 |
31
+ | :---------- | :-------------------- | :---------- | :--------------------------------------------------------------------------- |
32
+ | `enabled` | `boolean` | `true` | 启用或禁用高亮特效。 |
33
+ | `color` | `ColorRepresentation` | `0xff0000` | 高亮颜色。 |
34
+ | `opacity` | `number` | `0.5` | 高亮时材质的透明度。 |
35
+ | `overwrite` | `boolean` | `false` | `false` = 与原始颜色相乘(叠加);`true` = 完全替换颜色。 |
36
36
 
37
- > **Note:** `highlight` sets `material.transparent = true` on all affected meshes and injects `colorNode`/`opacityNode`. This is currently non-reversible without manually resetting those nodes.
37
+ > **注意:** `highlight` 会在所有受影响的网格上设置 `material.transparent = true`,并注入 `colorNode`/`opacityNode`。目前不可逆,若需恢复须手动重置这些节点。
38
38
 
39
39
  ---
40
40
 
41
41
  ## `TSLEffects`
42
42
 
43
- Static factory that returns TSL color nodes. Assign the result to `material.colorNode` on a `NodeMaterial` to apply animated shader effects. Requires `viewer.frameloop = 'always'` for continuous animation.
43
+ 静态工厂类,返回 TSL 颜色节点。将返回值赋给 `NodeMaterial` 的 `material.colorNode` 即可应用动态着色器特效。持续动画需要 `viewer.frameloop = 'always'`。
44
44
 
45
45
  ### `TSLEffects.flow(parameters?)`
46
46
 
47
- A directional light-sweep effect along the mesh UV X-axis — useful for roads, tubes, and flow lines.
47
+ 沿网格 UV X 轴方向的定向光扫效果,适用于道路、管道和流线。
48
48
 
49
49
  ```typescript
50
50
  import { TSLEffects } from 'u-space';
@@ -59,19 +59,19 @@ myTubeMesh.material.colorNode = TSLEffects.flow({
59
59
  viewer.frameloop = 'always';
60
60
  ```
61
61
 
62
- **Parameters:**
62
+ **参数:**
63
63
 
64
- | Property | Type | Default | Description |
65
- | :---------- | :-------------------- | :---------- | :------------------------------------------------------ |
66
- | `baseColor` | `ColorRepresentation` | `0xffffff` | Background/base color. |
67
- | `flowColor` | `ColorRepresentation` | `0x00ff00` | Sweep highlight color. |
68
- | `speed` | `number` | `1.0` | Animation speed (higher = faster sweep). |
69
- | `scale` | `number` | `3.0` | Spatial frequency of the pattern. |
70
- | `intensity` | `number` | `4.0` | Peak sharpness — higher values create a narrower beam. |
64
+ | 属性 | 类型 | 默认值 | 说明 |
65
+ | :---------- | :-------------------- | :---------- | :------------------------------------------------ |
66
+ | `baseColor` | `ColorRepresentation` | `0xffffff` | 背景/底色。 |
67
+ | `flowColor` | `ColorRepresentation` | `0x00ff00` | 扫光高亮颜色。 |
68
+ | `speed` | `number` | `1.0` | 动画速度(越高扫光越快)。 |
69
+ | `scale` | `number` | `3.0` | 图案的空间频率。 |
70
+ | `intensity` | `number` | `4.0` | 峰值锐度,值越高光束越细。 |
71
71
 
72
72
  ### `TSLEffects.breathe(parameters?)`
73
73
 
74
- A pulsing glow effect that oscillates between two colors over time — suitable for status indicators and alerts.
74
+ 在两种颜色之间随时间振荡的脉冲发光效果,适合状态指示器和警报。
75
75
 
76
76
  ```typescript
77
77
  myMesh.material.colorNode = TSLEffects.breathe({
@@ -82,18 +82,18 @@ myMesh.material.colorNode = TSLEffects.breathe({
82
82
  });
83
83
  ```
84
84
 
85
- **Parameters:**
85
+ **参数:**
86
86
 
87
- | Property | Type | Default | Description |
88
- | :------------ | :-------------------- | :--------- | :----------------------------------------------- |
89
- | `baseColor` | `ColorRepresentation` | `0xffffff` | Color at the low/rest state. |
90
- | `breathColor` | `ColorRepresentation` | `0x00ff00` | Color at peak brightness. |
91
- | `speed` | `number` | `1.0` | Oscillation speed. |
92
- | `intensity` | `number` | `2.0` | Controls how sharp the peak is. |
87
+ | 属性 | 类型 | 默认值 | 说明 |
88
+ | :------------ | :-------------------- | :---------- | :--------------------------------- |
89
+ | `baseColor` | `ColorRepresentation` | `0xffffff` | 低/静息状态的颜色。 |
90
+ | `breathColor` | `ColorRepresentation` | `0x00ff00` | 峰值亮度时的颜色。 |
91
+ | `speed` | `number` | `1.0` | 振荡速度。 |
92
+ | `intensity` | `number` | `2.0` | 控制峰值的锐度。 |
93
93
 
94
94
  ### `TSLEffects.fluid(parameters?)`
95
95
 
96
- A noise-distorted flow effect — useful for water surfaces, plasma, or organic flowing materials.
96
+ 噪声扭曲的流动效果,适用于水面、等离子体或有机流动材质。
97
97
 
98
98
  ```typescript
99
99
  myPlaneMesh.material.colorNode = TSLEffects.fluid({
@@ -106,13 +106,13 @@ myPlaneMesh.material.colorNode = TSLEffects.fluid({
106
106
  });
107
107
  ```
108
108
 
109
- **Parameters:**
109
+ **参数:**
110
110
 
111
- | Property | Type | Default | Description |
112
- | :----------- | :-------------------- | :--------- | :------------------------------------------------------- |
113
- | `baseColor` | `ColorRepresentation` | `0xffffff` | Base color. |
114
- | `flowColor` | `ColorRepresentation` | `0x0000ff` | Fluid highlight color. |
115
- | `speed` | `number` | `1.0` | Animation speed. |
116
- | `scale` | `number` | `1.0` | UV scale for the noise pattern. |
117
- | `intensity` | `number` | `1.0` | Sharpness of the fluid pattern. |
118
- | `distortion` | `number` | `0.5` | How much the noise distorts the UV before sampling. |
111
+ | 属性 | 类型 | 默认值 | 说明 |
112
+ | :----------- | :-------------------- | :---------- | :-------------------------------------------- |
113
+ | `baseColor` | `ColorRepresentation` | `0xffffff` | 基础颜色。 |
114
+ | `flowColor` | `ColorRepresentation` | `0x0000ff` | 流体高亮颜色。 |
115
+ | `speed` | `number` | `1.0` | 动画速度。 |
116
+ | `scale` | `number` | `1.0` | 噪声图案的 UV 缩放比例。 |
117
+ | `intensity` | `number` | `1.0` | 流体图案的锐度。 |
118
+ | `distortion` | `number` | `0.5` | 采样前噪声对 UV 的扭曲程度。 |