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.
- package/dist/index.cjs +1 -1
- package/dist/index.js +104 -76
- package/dist/plugins/object-controls/ObjectControls.d.ts +69 -0
- package/dist/plugins/object-controls/index.cjs +1 -0
- package/dist/plugins/object-controls/index.d.ts +1 -0
- package/dist/plugins/object-controls/index.js +44 -0
- package/dist/plugins/topology-drawer/TopologyDrawer.d.ts +97 -0
- package/dist/plugins/topology-drawer/index.cjs +1 -0
- package/dist/plugins/topology-drawer/index.d.ts +1 -0
- package/dist/plugins/topology-drawer/index.js +182 -0
- package/dist/src/interactions/InteractionEvent.d.ts +1 -1
- package/dist/src/interactions/InteractionManager.d.ts +2 -3
- package/dist/src/objects/Topology.d.ts +20 -1
- package/docs/api-animations.md +23 -23
- package/docs/api-effects.md +41 -41
- package/docs/api-interactions.md +47 -47
- package/docs/api-managers.md +24 -24
- package/docs/api-objects.md +85 -85
- package/docs/api-plugins.md +256 -113
- package/docs/api-viewer.md +45 -45
- package/docs/examples-guide.md +31 -31
- package/docs/getting-started.md +37 -36
- package/package.json +4 -3
|
@@ -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' | '
|
|
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>[]
|
|
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 {
|
|
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
|
}
|
package/docs/api-animations.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Animations API
|
|
2
2
|
|
|
3
|
-
`u-space`
|
|
3
|
+
`u-space` 提供了 `Tween` 类和 `tweenAnimation` 辅助函数,用于对任意数值属性进行动画处理。两者都是对 [`@tweenjs/tween.js`](https://github.com/tweenjs/tween.js) 的轻量封装,并自动与 `Viewer` 渲染循环集成。
|
|
4
4
|
|
|
5
5
|
## `tweenAnimation`
|
|
6
6
|
|
|
7
|
-
|
|
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,
|
|
17
|
-
{ x: 10, y: 5, z: 10 },
|
|
16
|
+
source, // 可变的起始状态(每帧被修改)
|
|
17
|
+
{ x: 10, y: 5, z: 10 }, // 目标状态
|
|
18
18
|
{
|
|
19
|
-
duration: 1500,
|
|
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
|
-
//
|
|
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
|
-
|
|
|
35
|
-
| :--------- | :-------------------------- | :-------------- |
|
|
36
|
-
| `duration` | `number` | `1000` |
|
|
37
|
-
| `delay` | `number` | `0` |
|
|
38
|
-
| `mode` | `AnimationModeType` | `'Linear.None'` |
|
|
39
|
-
| `repeat` | `number \| boolean` | `false` |
|
|
40
|
-
| `yoyo` | `boolean` | `false` |
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
64
|
+
.onComplete(() => console.log('完成'));
|
|
65
65
|
|
|
66
66
|
tween.start();
|
|
67
67
|
// tween.stop();
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
###
|
|
70
|
+
### 方法
|
|
71
71
|
|
|
72
|
-
|
|
|
73
|
-
| :------------------------ |
|
|
74
|
-
| `easingByMode(mode)` |
|
|
75
|
-
| `start(time?)` |
|
|
76
|
-
| `stop()` |
|
|
72
|
+
| 方法 | 说明 |
|
|
73
|
+
| :------------------------ | :-------------------------------------------------------------- |
|
|
74
|
+
| `easingByMode(mode)` | 使用 `AnimationModeType` 设置缓动函数的便捷简写。 |
|
|
75
|
+
| `start(time?)` | 启动补间并注册到 viewer 循环中。 |
|
|
76
|
+
| `stop()` | 停止补间并从 viewer 循环中注销。 |
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
其他方法(`to`、`delay`、`repeat`、`yoyo`、`onUpdate`、`onComplete`、`onStop`、`onStart`)均继承自 `tween.js` 的基础 `Tween` 类。
|
package/docs/api-effects.md
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
# Effects API
|
|
2
2
|
|
|
3
|
-
`u-space`
|
|
3
|
+
`u-space` 提供了两个基于 Three.js 着色语言(TSL/WebGPU 节点)的静态特效工具:`MaterialEffects` 用于为对象应用高亮状态,`TSLEffects` 用于生成动态颜色节点模式。
|
|
4
4
|
|
|
5
5
|
## `MaterialEffects`
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
静态工具类,将基于 TSL 的视觉特效直接应用于对象的材质。适用于任何 `Object3D`,会自动遍历所有子网格。
|
|
8
8
|
|
|
9
9
|
### `MaterialEffects.highlight(object, options?)`
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
为对象中所有网格应用颜色/透明度高亮。使用 `userData` 和 TSL 节点图,多个对象可共享同一节点图实例,各自保持独立状态。
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { MaterialEffects } from 'u-space';
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// 以 50% 透明度高亮为红色
|
|
17
17
|
MaterialEffects.highlight(myModel, {
|
|
18
18
|
enabled: true,
|
|
19
19
|
color: 0xff0000,
|
|
20
20
|
opacity: 0.5,
|
|
21
|
-
overwrite: false, // false =
|
|
21
|
+
overwrite: false, // false = 叠加(相乘),true = 完全替换颜色
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// 禁用高亮
|
|
25
25
|
MaterialEffects.highlight(myModel, { enabled: false });
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
### `HighlightOptions`
|
|
29
29
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
| `enabled`
|
|
33
|
-
| `color`
|
|
34
|
-
| `opacity`
|
|
35
|
-
| `overwrite
|
|
30
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
31
|
+
| :---------- | :-------------------- | :---------- | :--------------------------------------------------------------------------- |
|
|
32
|
+
| `enabled` | `boolean` | `true` | 启用或禁用高亮特效。 |
|
|
33
|
+
| `color` | `ColorRepresentation` | `0xff0000` | 高亮颜色。 |
|
|
34
|
+
| `opacity` | `number` | `0.5` | 高亮时材质的透明度。 |
|
|
35
|
+
| `overwrite` | `boolean` | `false` | `false` = 与原始颜色相乘(叠加);`true` = 完全替换颜色。 |
|
|
36
36
|
|
|
37
|
-
>
|
|
37
|
+
> **注意:** `highlight` 会在所有受影响的网格上设置 `material.transparent = true`,并注入 `colorNode`/`opacityNode`。目前不可逆,若需恢复须手动重置这些节点。
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
41
|
## `TSLEffects`
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
静态工厂类,返回 TSL 颜色节点。将返回值赋给 `NodeMaterial` 的 `material.colorNode` 即可应用动态着色器特效。持续动画需要 `viewer.frameloop = 'always'`。
|
|
44
44
|
|
|
45
45
|
### `TSLEffects.flow(parameters?)`
|
|
46
46
|
|
|
47
|
-
|
|
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
|
-
|
|
62
|
+
**参数:**
|
|
63
63
|
|
|
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` |
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
**参数:**
|
|
86
86
|
|
|
87
|
-
|
|
|
88
|
-
| :------------ | :-------------------- |
|
|
89
|
-
| `baseColor` | `ColorRepresentation` | `0xffffff`
|
|
90
|
-
| `breathColor` | `ColorRepresentation` | `0x00ff00`
|
|
91
|
-
| `speed` | `number` | `1.0`
|
|
92
|
-
| `intensity` | `number` | `2.0`
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
**参数:**
|
|
110
110
|
|
|
111
|
-
|
|
|
112
|
-
| :----------- | :-------------------- |
|
|
113
|
-
| `baseColor` | `ColorRepresentation` | `0xffffff`
|
|
114
|
-
| `flowColor` | `ColorRepresentation` | `0x0000ff`
|
|
115
|
-
| `speed` | `number` | `1.0`
|
|
116
|
-
| `scale` | `number` | `1.0`
|
|
117
|
-
| `intensity` | `number` | `1.0`
|
|
118
|
-
| `distortion` | `number` | `0.5`
|
|
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 的扭曲程度。 |
|