trid3d 0.2.0 → 0.3.0
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/README.md +8 -7
- package/package.json +1 -1
- package/trid/export/mtl.js +119 -0
- package/trid/export/obj.js +107 -21
- package/trid/index.d.ts +417 -8
- package/trid/index.js +19 -3
- package/trid/loaders/mtl.js +63 -0
- package/trid/loaders/obj.js +58 -6
- package/trid/loaders/texture.js +47 -6
- package/trid/math/matrix.js +200 -0
- package/trid/math/noise.js +162 -0
- package/trid/math/rotation.js +152 -0
- package/trid/render/canvasRenderer.js +40 -23
- package/trid/render/draw.js +6 -2
- package/trid/render/meshUtils.js +257 -0
- package/trid/render/proceduralTextures.js +211 -0
- package/trid/render/shapes.js +95 -27
- package/trid/render/webglRenderer.js +98 -27
- package/trid/scene.js +20 -9
- package/trid/sceneGraph.js +131 -0
- package/trid/tools/dragObject.js +33 -9
- package/trid/tools/helpers.js +109 -10
- package/trid/tools/hoverHighlight.js +4 -2
- package/trid/tools/index.js +3 -1
- package/trid/tools/orbitCamera.js +9 -0
- package/trid/tools/transformGizmo.js +316 -0
- package/trid/tools/viewGizmo.js +178 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { cameraForward, cameraRight, cameraUp } from "../math/transform.js";
|
|
2
|
+
import { OrbitCamera } from "./orbitCamera.js";
|
|
3
|
+
|
|
4
|
+
const AXES = [
|
|
5
|
+
{ dir: { x: 1, y: 0, z: 0 }, label: "X", color: "#e8605a" },
|
|
6
|
+
{ dir: { x: 0, y: 1, z: 0 }, label: "Y", color: "#7cc46b" },
|
|
7
|
+
{ dir: { x: 0, y: 0, z: 1 }, label: "Z", color: "#5f9be8" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The X, Y and Z axes in a corner of the canvas, turning with the camera
|
|
14
|
+
* (like Blender's navigation gizmo). Axes pointing away from you are dimmed;
|
|
15
|
+
* the dots without a letter are the negative ends. Click an axis and the
|
|
16
|
+
* `OrbitCamera` smoothly turns to look along it.
|
|
17
|
+
*
|
|
18
|
+
* Add it **after** the OrbitCamera: tools added later get clicks first, so
|
|
19
|
+
* clicks on the gizmo don't start an orbit drag.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* scene.use(new OrbitCamera());
|
|
23
|
+
* const gizmo = scene.use(new ViewGizmo({ corner: "top-right" }));
|
|
24
|
+
* gizmo.lookAlong({ x: 0, y: 1, z: 0 }); // look down from above
|
|
25
|
+
*/
|
|
26
|
+
export class ViewGizmo {
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} [opts]
|
|
29
|
+
* @param {OrbitCamera} [opts.orbit] - Camera controls to turn (default: the scene's first OrbitCamera).
|
|
30
|
+
* Without one, the gizmo only shows the axes
|
|
31
|
+
* @param {"top-right"|"top-left"|"bottom-right"|"bottom-left"} [opts.corner="top-right"] - Where it sits
|
|
32
|
+
* @param {number} [opts.size=42] - Radius in CSS pixels
|
|
33
|
+
* @param {number|{x:number,y:number}} [opts.margin=14] - Distance from the canvas corner in CSS pixels
|
|
34
|
+
* @param {number} [opts.duration=0.35] - Seconds the camera takes to turn to a clicked axis
|
|
35
|
+
*/
|
|
36
|
+
constructor({ orbit, corner = "top-right", size = 42, margin = 14, duration = 0.35 } = {}) {
|
|
37
|
+
this.orbit = orbit ?? null;
|
|
38
|
+
this.corner = corner;
|
|
39
|
+
this.size = size;
|
|
40
|
+
this.margin = typeof margin === "number" ? { x: margin, y: margin } : margin;
|
|
41
|
+
this.duration = duration;
|
|
42
|
+
/** Hide or show the gizmo without removing the tool. */
|
|
43
|
+
this.visible = true;
|
|
44
|
+
|
|
45
|
+
this._scene = null;
|
|
46
|
+
this._bubbles = []; // clickable axis ends, far to near, from the last draw
|
|
47
|
+
this._disc = null;
|
|
48
|
+
this._turn = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
attach(scene) {
|
|
52
|
+
this._scene = scene;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
detach() {
|
|
56
|
+
this._scene = null;
|
|
57
|
+
this._turn = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The OrbitCamera it turns: the one given, or the scene's first. */
|
|
61
|
+
_orbit() {
|
|
62
|
+
return this.orbit ?? this._scene?.tools.list.find((t) => t instanceof OrbitCamera) ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether a canvas pixel is on the gizmo (as last drawn).
|
|
67
|
+
*
|
|
68
|
+
* @param {number} x - Canvas X in pixels
|
|
69
|
+
* @param {number} y - Canvas Y in pixels
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
contains(x, y) {
|
|
73
|
+
return this.visible && !!this._disc && Math.hypot(x - this._disc.x, y - this._disc.y) <= this._disc.r;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Turns the camera smoothly so it looks at its target from `dir`:
|
|
78
|
+
* `{ x: 1, y: 0, z: 0 }` looks from the +X side, `{ x: 0, y: 1, z: 0 }` from above.
|
|
79
|
+
*
|
|
80
|
+
* @param {{x:number,y:number,z:number}} dir - Direction from the target to the camera
|
|
81
|
+
*/
|
|
82
|
+
lookAlong(dir) {
|
|
83
|
+
const orbit = this._orbit();
|
|
84
|
+
if (!orbit) return;
|
|
85
|
+
const len = Math.hypot(dir.x, dir.y, dir.z) || 1;
|
|
86
|
+
// The camera looks against `dir`, toward the target
|
|
87
|
+
const f = { x: -dir.x / len, y: -dir.y / len, z: -dir.z / len };
|
|
88
|
+
const pitch1 = Math.asin(Math.max(-1, Math.min(1, f.y)));
|
|
89
|
+
// Straight up or down: keep the current heading
|
|
90
|
+
const yaw1 = Math.abs(f.y) > 0.999 ? orbit.yaw : Math.atan2(f.x, f.z);
|
|
91
|
+
// Turn the short way round
|
|
92
|
+
let yaw0 = orbit.yaw;
|
|
93
|
+
while (yaw1 - yaw0 > Math.PI) yaw0 += Math.PI * 2;
|
|
94
|
+
while (yaw0 - yaw1 > Math.PI) yaw0 -= Math.PI * 2;
|
|
95
|
+
this._turn = { orbit, yaw0, pitch0: orbit.pitch, yaw1, pitch1, t: 0 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pointerDown(e) {
|
|
99
|
+
if (!this.visible || e.button !== 0 || !this._orbit()) return false;
|
|
100
|
+
// The nearest end wins where two overlap (bubbles are stored far to near)
|
|
101
|
+
const hit = [...this._bubbles].reverse().find((b) => Math.hypot(e.x - b.x, e.y - b.y) <= b.r);
|
|
102
|
+
if (!hit) return false;
|
|
103
|
+
this.lookAlong(hit.dir);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
update(dt, scene) {
|
|
108
|
+
const turn = this._turn;
|
|
109
|
+
if (!turn) return;
|
|
110
|
+
turn.t = this.duration > 0 ? Math.min(1, turn.t + dt / this.duration) : 1;
|
|
111
|
+
const e = 1 - Math.pow(1 - turn.t, 3); // ease out
|
|
112
|
+
turn.orbit.yaw = turn.yaw0 + (turn.yaw1 - turn.yaw0) * e;
|
|
113
|
+
turn.orbit.pitch = turn.pitch0 + (turn.pitch1 - turn.pitch0) * e;
|
|
114
|
+
turn.orbit.holdAutoRotate?.();
|
|
115
|
+
turn.orbit.apply(scene.camera);
|
|
116
|
+
if (turn.t >= 1) this._turn = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
draw(ctx, scene) {
|
|
120
|
+
this._bubbles = [];
|
|
121
|
+
this._disc = null;
|
|
122
|
+
if (!this.visible) return;
|
|
123
|
+
const { canvas, camera } = scene;
|
|
124
|
+
const k = canvas.width / (canvas.clientWidth || canvas.width); // CSS px → canvas px
|
|
125
|
+
const r = this.size * k, mx = this.margin.x * k, my = this.margin.y * k;
|
|
126
|
+
const cx = this.corner.endsWith("right") ? canvas.width - mx - r : mx + r;
|
|
127
|
+
const cy = this.corner.startsWith("top") ? my + r : canvas.height - my - r;
|
|
128
|
+
this._disc = { x: cx, y: cy, r };
|
|
129
|
+
const right = cameraRight(camera), up = cameraUp(camera), fwd = cameraForward(camera);
|
|
130
|
+
|
|
131
|
+
ctx.save();
|
|
132
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.06)";
|
|
133
|
+
ctx.beginPath();
|
|
134
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
135
|
+
ctx.fill();
|
|
136
|
+
|
|
137
|
+
const reach = r * 0.68, dotR = r * 0.2;
|
|
138
|
+
const items = AXES.flatMap((a) => [1, -1].map((sign) => {
|
|
139
|
+
const dir = { x: a.dir.x * sign, y: a.dir.y * sign, z: a.dir.z * sign };
|
|
140
|
+
return {
|
|
141
|
+
...a, sign, dir,
|
|
142
|
+
x: cx + dot(dir, right) * reach,
|
|
143
|
+
y: cy - dot(dir, up) * reach,
|
|
144
|
+
depth: dot(dir, fwd), // > 0: points away from the viewer
|
|
145
|
+
};
|
|
146
|
+
}));
|
|
147
|
+
// Far ones first, so the near ones are drawn on top
|
|
148
|
+
items.sort((a, b) => b.depth - a.depth);
|
|
149
|
+
|
|
150
|
+
for (const it of items) {
|
|
151
|
+
const fade = 0.55 + 0.45 * Math.max(0, -it.depth);
|
|
152
|
+
if (it.sign > 0) {
|
|
153
|
+
ctx.strokeStyle = it.color;
|
|
154
|
+
ctx.globalAlpha = fade;
|
|
155
|
+
ctx.lineWidth = 2 * k;
|
|
156
|
+
ctx.beginPath();
|
|
157
|
+
ctx.moveTo(cx, cy);
|
|
158
|
+
ctx.lineTo(it.x, it.y);
|
|
159
|
+
ctx.stroke();
|
|
160
|
+
}
|
|
161
|
+
ctx.globalAlpha = it.sign > 0 ? fade : fade * 0.6;
|
|
162
|
+
ctx.fillStyle = it.color;
|
|
163
|
+
ctx.beginPath();
|
|
164
|
+
ctx.arc(it.x, it.y, it.sign > 0 ? dotR : dotR * 0.7, 0, Math.PI * 2);
|
|
165
|
+
ctx.fill();
|
|
166
|
+
if (it.sign > 0) {
|
|
167
|
+
ctx.globalAlpha = 1;
|
|
168
|
+
ctx.fillStyle = "#15171c";
|
|
169
|
+
ctx.font = `600 ${Math.round(dotR * 1.1)}px ui-monospace, monospace`;
|
|
170
|
+
ctx.textAlign = "center";
|
|
171
|
+
ctx.textBaseline = "middle";
|
|
172
|
+
ctx.fillText(it.label, it.x, it.y + 0.5 * k);
|
|
173
|
+
}
|
|
174
|
+
this._bubbles.push({ x: it.x, y: it.y, r: dotR * 1.3, dir: it.dir });
|
|
175
|
+
}
|
|
176
|
+
ctx.restore();
|
|
177
|
+
}
|
|
178
|
+
}
|