@holoscript/plugin-film3d-volumetrics 2.0.2 → 2.0.3
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/package.json +4 -4
- package/src/crdt-spatial-augment.ts +11 -2
- package/src/index.ts +21 -7
- package/src/traits/CinematicCameraTrait.ts +72 -12
- package/src/traits/GCodeSlicerTrait.ts +43 -19
- package/src/traits/GaussianSplatTrait.ts +39 -8
- package/src/traits/NeRFTrait.ts +45 -9
- package/src/traits/VolumetricTrait.ts +44 -10
- package/src/traits/types.ts +18 -3
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holoscript/plugin-film3d-volumetrics",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@holoscript/crdt-spatial": "^1.0.2"
|
|
7
7
|
},
|
|
8
8
|
"peerDependencies": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"@holoscript/core": ">=8.0.0",
|
|
10
|
+
"loro-crdt": "^1.0.0"
|
|
11
11
|
},
|
|
12
12
|
"peerDependenciesMeta": {
|
|
13
13
|
"loro-crdt": {
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"test": "vitest run --passWithNoTests",
|
|
24
24
|
"test:coverage": "vitest run --coverage --passWithNoTests"
|
|
25
25
|
}
|
|
26
|
-
}
|
|
26
|
+
}
|
|
@@ -8,8 +8,17 @@ declare module '@holoscript/crdt-spatial' {
|
|
|
8
8
|
// @ts-ignore -- augmenting an already-exported const from the runtime module
|
|
9
9
|
export const FILM3D_VOLUMETRICS_ROOT: 'film3d_volumetrics';
|
|
10
10
|
export function ensureFilm3dVolumetricsRoot(doc: LoroDoc): LoroMap<Record<string, unknown>>;
|
|
11
|
-
export function registerVolumetricNode(
|
|
12
|
-
|
|
11
|
+
export function registerVolumetricNode(
|
|
12
|
+
doc: LoroDoc,
|
|
13
|
+
nodeId: string,
|
|
14
|
+
meta: { format: string }
|
|
15
|
+
): void;
|
|
16
|
+
export function setVolumetricChunk(
|
|
17
|
+
doc: LoroDoc,
|
|
18
|
+
nodeId: string,
|
|
19
|
+
chunkIndex: number,
|
|
20
|
+
data: Uint8Array
|
|
21
|
+
): void;
|
|
13
22
|
export function setVolumetricVoxelPayload(doc: LoroDoc, nodeId: string, data: Uint8Array): void;
|
|
14
23
|
export function unregisterVolumetricNode(doc: LoroDoc, nodeId: string): void;
|
|
15
24
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import './crdt-spatial-augment';
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
createVolumetricHandler,
|
|
5
|
+
type VolumetricConfig,
|
|
6
|
+
type VolumetricFormat,
|
|
7
|
+
} from './traits/VolumetricTrait';
|
|
4
8
|
export { createGaussianSplatHandler, type GaussianSplatConfig } from './traits/GaussianSplatTrait';
|
|
5
9
|
export { createNeRFHandler, type NeRFConfig, type NeRFMethod } from './traits/NeRFTrait';
|
|
6
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
createCinematicCameraHandler,
|
|
12
|
+
type CinematicCameraConfig,
|
|
13
|
+
type CameraMovement,
|
|
14
|
+
} from './traits/CinematicCameraTrait';
|
|
7
15
|
export {
|
|
8
16
|
createGCodeSlicerHandler,
|
|
9
17
|
type GCodeSlicerConfig,
|
|
@@ -15,7 +23,7 @@ export {
|
|
|
15
23
|
buildAdhesionLayerPlan,
|
|
16
24
|
buildInsetPerimeterTraversal,
|
|
17
25
|
buildSemanticGCodePreamble,
|
|
18
|
-
buildTraversalStackFromMesh
|
|
26
|
+
buildTraversalStackFromMesh,
|
|
19
27
|
} from './traits/GCodeSlicerTrait';
|
|
20
28
|
export * from './traits/types';
|
|
21
29
|
|
|
@@ -25,7 +33,7 @@ import {
|
|
|
25
33
|
registerVolumetricNode,
|
|
26
34
|
setVolumetricChunk,
|
|
27
35
|
setVolumetricVoxelPayload,
|
|
28
|
-
unregisterVolumetricNode
|
|
36
|
+
unregisterVolumetricNode,
|
|
29
37
|
} from '@holoscript/crdt-spatial';
|
|
30
38
|
import { createVolumetricHandler } from './traits/VolumetricTrait';
|
|
31
39
|
import { createGaussianSplatHandler } from './traits/GaussianSplatTrait';
|
|
@@ -39,7 +47,7 @@ export {
|
|
|
39
47
|
registerVolumetricNode,
|
|
40
48
|
setVolumetricChunk,
|
|
41
49
|
setVolumetricVoxelPayload,
|
|
42
|
-
unregisterVolumetricNode
|
|
50
|
+
unregisterVolumetricNode,
|
|
43
51
|
};
|
|
44
52
|
|
|
45
53
|
import type { LoroDoc } from 'loro-crdt';
|
|
@@ -61,6 +69,12 @@ export const pluginMeta = {
|
|
|
61
69
|
version: '1.0.0',
|
|
62
70
|
traits: ['volumetric', 'gaussian_splat', 'nerf', 'cinematic_camera', 'gcode_slicer'],
|
|
63
71
|
/** Root map on the shared Loro doc replicated by LoroWebRTCProvider */
|
|
64
|
-
crdtVolumetricsRoot: FILM3D_VOLUMETRICS_ROOT
|
|
72
|
+
crdtVolumetricsRoot: FILM3D_VOLUMETRICS_ROOT,
|
|
65
73
|
};
|
|
66
|
-
export const traitHandlers = [
|
|
74
|
+
export const traitHandlers = [
|
|
75
|
+
createVolumetricHandler(),
|
|
76
|
+
createGaussianSplatHandler(),
|
|
77
|
+
createNeRFHandler(),
|
|
78
|
+
createCinematicCameraHandler(),
|
|
79
|
+
createGCodeSlicerHandler(),
|
|
80
|
+
];
|
|
@@ -1,25 +1,85 @@
|
|
|
1
1
|
/** @cinematic_camera Trait — Film-grade virtual camera control. @trait cinematic_camera */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export type CameraMovement =
|
|
5
|
-
|
|
4
|
+
export type CameraMovement =
|
|
5
|
+
| 'static'
|
|
6
|
+
| 'dolly'
|
|
7
|
+
| 'truck'
|
|
8
|
+
| 'pedestal'
|
|
9
|
+
| 'pan'
|
|
10
|
+
| 'tilt'
|
|
11
|
+
| 'roll'
|
|
12
|
+
| 'crane'
|
|
13
|
+
| 'steadicam'
|
|
14
|
+
| 'drone'
|
|
15
|
+
| 'handheld';
|
|
16
|
+
export interface CinematicCameraConfig {
|
|
17
|
+
focalLengthMm: number;
|
|
18
|
+
sensorWidth: number;
|
|
19
|
+
sensorHeight: number;
|
|
20
|
+
tStop: number;
|
|
21
|
+
focusDistanceM: number;
|
|
22
|
+
movement: CameraMovement;
|
|
23
|
+
shutterAngle: number;
|
|
24
|
+
iso: number;
|
|
25
|
+
whiteBalanceK: number;
|
|
26
|
+
anamorphic: boolean;
|
|
27
|
+
}
|
|
6
28
|
|
|
7
|
-
const defaultConfig: CinematicCameraConfig = {
|
|
29
|
+
const defaultConfig: CinematicCameraConfig = {
|
|
30
|
+
focalLengthMm: 50,
|
|
31
|
+
sensorWidth: 36,
|
|
32
|
+
sensorHeight: 24,
|
|
33
|
+
tStop: 2.8,
|
|
34
|
+
focusDistanceM: 3,
|
|
35
|
+
movement: 'static',
|
|
36
|
+
shutterAngle: 180,
|
|
37
|
+
iso: 800,
|
|
38
|
+
whiteBalanceK: 5600,
|
|
39
|
+
anamorphic: false,
|
|
40
|
+
};
|
|
8
41
|
|
|
9
42
|
export function createCinematicCameraHandler(): TraitHandler<CinematicCameraConfig> {
|
|
10
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
name: 'cinematic_camera',
|
|
45
|
+
defaultConfig,
|
|
11
46
|
onAttach(n: HSPlusNode, c: CinematicCameraConfig, ctx: TraitContext) {
|
|
12
47
|
const fovDeg = 2 * Math.atan(c.sensorWidth / (2 * c.focalLengthMm)) * (180 / Math.PI);
|
|
13
|
-
n.__camState = {
|
|
14
|
-
|
|
48
|
+
n.__camState = {
|
|
49
|
+
fovDeg,
|
|
50
|
+
isRecording: false,
|
|
51
|
+
frameCount: 0,
|
|
52
|
+
dofBlurRadius: c.focalLengthMm / (2 * c.tStop),
|
|
53
|
+
};
|
|
54
|
+
ctx.emit?.('camera:configured', {
|
|
55
|
+
focal: c.focalLengthMm,
|
|
56
|
+
fov: fovDeg,
|
|
57
|
+
movement: c.movement,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
onDetach(n: HSPlusNode, _c: CinematicCameraConfig, ctx: TraitContext) {
|
|
61
|
+
delete n.__camState;
|
|
62
|
+
ctx.emit?.('camera:removed');
|
|
63
|
+
},
|
|
64
|
+
onUpdate(n: HSPlusNode, _c: CinematicCameraConfig, _ctx: TraitContext, _d: number) {
|
|
65
|
+
const s = n.__camState as Record<string, unknown> | undefined;
|
|
66
|
+
if (s?.isRecording) (s.frameCount as number)++;
|
|
15
67
|
},
|
|
16
|
-
onDetach(n: HSPlusNode, _c: CinematicCameraConfig, ctx: TraitContext) { delete n.__camState; ctx.emit?.('camera:removed'); },
|
|
17
|
-
onUpdate(n: HSPlusNode, _c: CinematicCameraConfig, _ctx: TraitContext, _d: number) { const s = n.__camState as Record<string, unknown> | undefined; if (s?.isRecording) (s.frameCount as number)++; },
|
|
18
68
|
onEvent(n: HSPlusNode, _c: CinematicCameraConfig, ctx: TraitContext, e: TraitEvent) {
|
|
19
|
-
const s = n.__camState as Record<string, unknown> | undefined;
|
|
20
|
-
if (
|
|
21
|
-
if (e.type === 'camera:
|
|
22
|
-
|
|
69
|
+
const s = n.__camState as Record<string, unknown> | undefined;
|
|
70
|
+
if (!s) return;
|
|
71
|
+
if (e.type === 'camera:record') {
|
|
72
|
+
s.isRecording = true;
|
|
73
|
+
ctx.emit?.('camera:recording');
|
|
74
|
+
}
|
|
75
|
+
if (e.type === 'camera:cut') {
|
|
76
|
+
s.isRecording = false;
|
|
77
|
+
ctx.emit?.('camera:cut', { frames: s.frameCount });
|
|
78
|
+
s.frameCount = 0;
|
|
79
|
+
}
|
|
80
|
+
if (e.type === 'camera:rack_focus') {
|
|
81
|
+
ctx.emit?.('camera:focus_pulling', { to: e.payload?.distance });
|
|
82
|
+
}
|
|
23
83
|
},
|
|
24
84
|
};
|
|
25
85
|
}
|
|
@@ -67,10 +67,15 @@ const defaultConfig: GCodeSlicerConfig = {
|
|
|
67
67
|
adhesionLayerCount: 3,
|
|
68
68
|
adhesionLayerHeightMm: 0.25,
|
|
69
69
|
adhesionBrimMm: 2,
|
|
70
|
-
printSpeedMmS: 50
|
|
70
|
+
printSpeedMmS: 50,
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
-
function bbox2D(vertices: [number, number, number][]): {
|
|
73
|
+
function bbox2D(vertices: [number, number, number][]): {
|
|
74
|
+
minX: number;
|
|
75
|
+
maxX: number;
|
|
76
|
+
minY: number;
|
|
77
|
+
maxY: number;
|
|
78
|
+
} {
|
|
74
79
|
let minX = Infinity;
|
|
75
80
|
let maxX = -Infinity;
|
|
76
81
|
let minY = Infinity;
|
|
@@ -92,7 +97,9 @@ export function buildAdhesionLayerPlan(c: GCodeSlicerConfig): AdhesionLayerPlanE
|
|
|
92
97
|
const out: AdhesionLayerPlanEntry[] = [];
|
|
93
98
|
let z = 0;
|
|
94
99
|
for (let i = 0; i < c.adhesionLayerCount; i++) {
|
|
95
|
-
const temp =
|
|
100
|
+
const temp =
|
|
101
|
+
c.nozzleTempC +
|
|
102
|
+
(i === 0 ? c.firstLayerNozzleBoostC : Math.max(0, c.firstLayerNozzleBoostC - i * 3));
|
|
96
103
|
out.push({ layerIndex: i, zMm: z + c.adhesionLayerHeightMm, nozzleTempC: Math.round(temp) });
|
|
97
104
|
z += c.adhesionLayerHeightMm;
|
|
98
105
|
}
|
|
@@ -120,7 +127,7 @@ export function buildInsetPerimeterTraversal(
|
|
|
120
127
|
[maxX, minY, layerZMm],
|
|
121
128
|
[maxX, maxY, layerZMm],
|
|
122
129
|
[minX, maxY, layerZMm],
|
|
123
|
-
[minX, minY, layerZMm]
|
|
130
|
+
[minX, minY, layerZMm],
|
|
124
131
|
];
|
|
125
132
|
}
|
|
126
133
|
const edge = (x0: number, y0: number, x1: number, y1: number): [number, number, number][] => {
|
|
@@ -135,7 +142,7 @@ export function buildInsetPerimeterTraversal(
|
|
|
135
142
|
...edge(ix0, iy0, ix1, iy0),
|
|
136
143
|
...edge(ix1, iy0, ix1, iy1).slice(1),
|
|
137
144
|
...edge(ix1, iy1, ix0, iy1).slice(1),
|
|
138
|
-
...edge(ix0, iy1, ix0, iy0).slice(1)
|
|
145
|
+
...edge(ix0, iy1, ix0, iy0).slice(1),
|
|
139
146
|
];
|
|
140
147
|
}
|
|
141
148
|
|
|
@@ -148,7 +155,7 @@ export function buildSemanticGCodePreamble(c: GCodeSlicerConfig, mesh?: MeshSlic
|
|
|
148
155
|
`M140 S${Math.round(c.bedTempC)} ; bed target`,
|
|
149
156
|
`M104 S${Math.round(c.nozzleTempC)} ; nozzle standby → target`,
|
|
150
157
|
`M190 S${Math.round(c.bedTempC)} ; wait for bed`,
|
|
151
|
-
`M109 S${Math.round(c.nozzleTempC + c.firstLayerNozzleBoostC)} ; first layer nozzle (boosted)
|
|
158
|
+
`M109 S${Math.round(c.nozzleTempC + c.firstLayerNozzleBoostC)} ; first layer nozzle (boosted)`,
|
|
152
159
|
];
|
|
153
160
|
const adhesion = buildAdhesionLayerPlan(c);
|
|
154
161
|
for (const layer of adhesion) {
|
|
@@ -164,18 +171,23 @@ export function buildSemanticGCodePreamble(c: GCodeSlicerConfig, mesh?: MeshSlic
|
|
|
164
171
|
`; vertices=${mesh.verticesMm.length}`
|
|
165
172
|
);
|
|
166
173
|
}
|
|
167
|
-
lines.push(
|
|
174
|
+
lines.push(
|
|
175
|
+
`; layerHeight=${c.layerHeightMm} infill=${c.infillPercent}% F=${c.printSpeedMmS}mm/s`
|
|
176
|
+
);
|
|
168
177
|
return lines.join('\n');
|
|
169
178
|
}
|
|
170
179
|
|
|
171
|
-
export function buildTraversalStackFromMesh(
|
|
180
|
+
export function buildTraversalStackFromMesh(
|
|
181
|
+
c: GCodeSlicerConfig,
|
|
182
|
+
mesh: MeshSliceInput
|
|
183
|
+
): TraversalLayerPlan[] {
|
|
172
184
|
if (!mesh.verticesMm.length) return [];
|
|
173
185
|
const plans: TraversalLayerPlan[] = [];
|
|
174
186
|
const adhesion = buildAdhesionLayerPlan(c);
|
|
175
187
|
for (const layer of adhesion) {
|
|
176
188
|
plans.push({
|
|
177
189
|
layerZMm: layer.zMm,
|
|
178
|
-
pointsMm: buildInsetPerimeterTraversal(mesh.verticesMm, layer.zMm, c.adhesionBrimMm)
|
|
190
|
+
pointsMm: buildInsetPerimeterTraversal(mesh.verticesMm, layer.zMm, c.adhesionBrimMm),
|
|
179
191
|
});
|
|
180
192
|
}
|
|
181
193
|
let z = adhesion.length ? adhesion[adhesion.length - 1]!.zMm : 0;
|
|
@@ -184,7 +196,11 @@ export function buildTraversalStackFromMesh(c: GCodeSlicerConfig, mesh: MeshSlic
|
|
|
184
196
|
z += c.layerHeightMm;
|
|
185
197
|
plans.push({
|
|
186
198
|
layerZMm: z,
|
|
187
|
-
pointsMm: buildInsetPerimeterTraversal(
|
|
199
|
+
pointsMm: buildInsetPerimeterTraversal(
|
|
200
|
+
mesh.verticesMm,
|
|
201
|
+
z,
|
|
202
|
+
c.adhesionBrimMm + c.layerHeightMm * 2
|
|
203
|
+
),
|
|
188
204
|
});
|
|
189
205
|
}
|
|
190
206
|
return plans;
|
|
@@ -205,9 +221,11 @@ export function createGCodeSlicerHandler(): TraitHandler<GCodeSlicerConfig> {
|
|
|
205
221
|
mesh,
|
|
206
222
|
adhesionPlan,
|
|
207
223
|
traversal,
|
|
208
|
-
gcodePreamble: buildSemanticGCodePreamble(c, mesh.verticesMm.length ? mesh : undefined)
|
|
224
|
+
gcodePreamble: buildSemanticGCodePreamble(c, mesh.verticesMm.length ? mesh : undefined),
|
|
209
225
|
};
|
|
210
|
-
ctx.emit?.('gcode_slicer:ready', {
|
|
226
|
+
ctx.emit?.('gcode_slicer:ready', {
|
|
227
|
+
semantic: { adhesionPlan, traversalLayers: traversal.length },
|
|
228
|
+
});
|
|
211
229
|
},
|
|
212
230
|
onDetach(n: HSPlusNode, _c: GCodeSlicerConfig, ctx: TraitContext) {
|
|
213
231
|
delete n.__slicerState;
|
|
@@ -223,7 +241,7 @@ export function createGCodeSlicerHandler(): TraitHandler<GCodeSlicerConfig> {
|
|
|
223
241
|
s.gcodePreamble = buildSemanticGCodePreamble(c, mesh.verticesMm.length ? mesh : undefined);
|
|
224
242
|
ctx.emit?.('gcode_slicer:semantic_updated', {
|
|
225
243
|
adhesionPlan: s.adhesionPlan,
|
|
226
|
-
traversalLayers: s.traversal?.length ?? 0
|
|
244
|
+
traversalLayers: s.traversal?.length ?? 0,
|
|
227
245
|
});
|
|
228
246
|
},
|
|
229
247
|
onEvent(n: HSPlusNode, c: GCodeSlicerConfig, ctx: TraitContext, e: TraitEvent) {
|
|
@@ -239,7 +257,10 @@ export function createGCodeSlicerHandler(): TraitHandler<GCodeSlicerConfig> {
|
|
|
239
257
|
s.adhesionPlan = buildAdhesionLayerPlan(c);
|
|
240
258
|
s.traversal = buildTraversalStackFromMesh(c, mesh);
|
|
241
259
|
s.gcodePreamble = buildSemanticGCodePreamble(c, mesh);
|
|
242
|
-
ctx.emit?.('gcode_slicer:mesh_bound', {
|
|
260
|
+
ctx.emit?.('gcode_slicer:mesh_bound', {
|
|
261
|
+
vertexCount: verts.length,
|
|
262
|
+
layers: s.traversal?.length ?? 0,
|
|
263
|
+
});
|
|
243
264
|
}
|
|
244
265
|
|
|
245
266
|
if (e.type === 'gcode_slicer:slice' && !s.isSlicing) {
|
|
@@ -250,9 +271,12 @@ export function createGCodeSlicerHandler(): TraitHandler<GCodeSlicerConfig> {
|
|
|
250
271
|
const meshVol = s.mesh?.verticesMm.length
|
|
251
272
|
? bbox2D(s.mesh.verticesMm)
|
|
252
273
|
: { minX: 0, maxX: 50, minY: 0, maxY: 50 };
|
|
253
|
-
const xyArea =
|
|
254
|
-
|
|
255
|
-
|
|
274
|
+
const xyArea =
|
|
275
|
+
Math.max(1, meshVol.maxX - meshVol.minX) * Math.max(1, meshVol.maxY - meshVol.minY);
|
|
276
|
+
const volumeEstimate =
|
|
277
|
+
xyArea * (c.adhesionLayerCount * c.adhesionLayerHeightMm + c.layerHeightMm * 12);
|
|
278
|
+
s.estimatedPrintTimeMs =
|
|
279
|
+
(volumeEstimate / Math.max(0.01, c.printSpeedMmS * c.layerHeightMm)) * 1000;
|
|
256
280
|
|
|
257
281
|
setTimeout(() => {
|
|
258
282
|
s.isSlicing = false;
|
|
@@ -264,10 +288,10 @@ export function createGCodeSlicerHandler(): TraitHandler<GCodeSlicerConfig> {
|
|
|
264
288
|
estimatedTimeMs: s.estimatedPrintTimeMs,
|
|
265
289
|
preamble,
|
|
266
290
|
adhesionPlan: s.adhesionPlan,
|
|
267
|
-
traversal: s.traversal
|
|
291
|
+
traversal: s.traversal,
|
|
268
292
|
});
|
|
269
293
|
}, 1500);
|
|
270
294
|
}
|
|
271
|
-
}
|
|
295
|
+
},
|
|
272
296
|
};
|
|
273
297
|
}
|
|
@@ -1,22 +1,53 @@
|
|
|
1
1
|
/** @gaussian_splat Trait — 3D Gaussian Splatting rendering. @trait gaussian_splat */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export interface GaussianSplatConfig {
|
|
4
|
+
export interface GaussianSplatConfig {
|
|
5
|
+
splatCount: number;
|
|
6
|
+
shDegree: number;
|
|
7
|
+
sortMethod: 'radix' | 'bitonic' | 'cpu';
|
|
8
|
+
enableAntialiasing: boolean;
|
|
9
|
+
opacityThreshold: number;
|
|
10
|
+
maxSplatSize: number;
|
|
11
|
+
lodLevels: number;
|
|
12
|
+
}
|
|
5
13
|
|
|
6
|
-
const defaultConfig: GaussianSplatConfig = {
|
|
14
|
+
const defaultConfig: GaussianSplatConfig = {
|
|
15
|
+
splatCount: 0,
|
|
16
|
+
shDegree: 3,
|
|
17
|
+
sortMethod: 'radix',
|
|
18
|
+
enableAntialiasing: true,
|
|
19
|
+
opacityThreshold: 0.005,
|
|
20
|
+
maxSplatSize: 0.1,
|
|
21
|
+
lodLevels: 4,
|
|
22
|
+
};
|
|
7
23
|
|
|
8
24
|
export function createGaussianSplatHandler(): TraitHandler<GaussianSplatConfig> {
|
|
9
|
-
return {
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
return {
|
|
26
|
+
name: 'gaussian_splat',
|
|
27
|
+
defaultConfig,
|
|
28
|
+
onAttach(n: HSPlusNode, c: GaussianSplatConfig, ctx: TraitContext) {
|
|
29
|
+
n.__gsplatState = { loaded: false, visibleSplats: 0, sortTimeMs: 0, lodLevel: 0 };
|
|
30
|
+
ctx.emit?.('gsplat:initialized', { count: c.splatCount, sort: c.sortMethod });
|
|
31
|
+
},
|
|
32
|
+
onDetach(n: HSPlusNode, _c: GaussianSplatConfig, ctx: TraitContext) {
|
|
33
|
+
delete n.__gsplatState;
|
|
34
|
+
ctx.emit?.('gsplat:disposed');
|
|
35
|
+
},
|
|
12
36
|
onUpdate(n: HSPlusNode, _c: GaussianSplatConfig, ctx: TraitContext, _d: number) {
|
|
13
37
|
const s = n.__gsplatState as Record<string, unknown> | undefined;
|
|
14
38
|
if (s?.loaded) ctx.emit?.('gsplat:frame', { visible: s.visibleSplats, sortMs: s.sortTimeMs });
|
|
15
39
|
},
|
|
16
40
|
onEvent(n: HSPlusNode, _c: GaussianSplatConfig, ctx: TraitContext, e: TraitEvent) {
|
|
17
|
-
const s = n.__gsplatState as Record<string, unknown> | undefined;
|
|
18
|
-
if (
|
|
19
|
-
if (e.type === 'gsplat:
|
|
41
|
+
const s = n.__gsplatState as Record<string, unknown> | undefined;
|
|
42
|
+
if (!s) return;
|
|
43
|
+
if (e.type === 'gsplat:load') {
|
|
44
|
+
s.loaded = true;
|
|
45
|
+
ctx.emit?.('gsplat:loaded');
|
|
46
|
+
}
|
|
47
|
+
if (e.type === 'gsplat:set_lod') {
|
|
48
|
+
s.lodLevel = e.payload?.level;
|
|
49
|
+
ctx.emit?.('gsplat:lod_changed', { level: s.lodLevel });
|
|
50
|
+
}
|
|
20
51
|
},
|
|
21
52
|
};
|
|
22
53
|
}
|
package/src/traits/NeRFTrait.ts
CHANGED
|
@@ -2,19 +2,55 @@
|
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
4
|
export type NeRFMethod = 'instant_ngp' | 'nerfacto' | 'tensorf' | 'mip_nerf' | 'zip_nerf';
|
|
5
|
-
export interface NeRFConfig {
|
|
5
|
+
export interface NeRFConfig {
|
|
6
|
+
method: NeRFMethod;
|
|
7
|
+
resolution: number;
|
|
8
|
+
nearPlane: number;
|
|
9
|
+
farPlane: number;
|
|
10
|
+
samplesPerRay: number;
|
|
11
|
+
batchSize: number;
|
|
12
|
+
enableDeformation: boolean;
|
|
13
|
+
}
|
|
6
14
|
|
|
7
|
-
const defaultConfig: NeRFConfig = {
|
|
15
|
+
const defaultConfig: NeRFConfig = {
|
|
16
|
+
method: 'instant_ngp',
|
|
17
|
+
resolution: 512,
|
|
18
|
+
nearPlane: 0.01,
|
|
19
|
+
farPlane: 100,
|
|
20
|
+
samplesPerRay: 64,
|
|
21
|
+
batchSize: 4096,
|
|
22
|
+
enableDeformation: false,
|
|
23
|
+
};
|
|
8
24
|
|
|
9
25
|
export function createNeRFHandler(): TraitHandler<NeRFConfig> {
|
|
10
|
-
return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
return {
|
|
27
|
+
name: 'nerf',
|
|
28
|
+
defaultConfig,
|
|
29
|
+
onAttach(n: HSPlusNode, c: NeRFConfig, ctx: TraitContext) {
|
|
30
|
+
n.__nerfState = { isRendering: false, fps: 0, trainStep: 0, psnr: 0 };
|
|
31
|
+
ctx.emit?.('nerf:loaded', { method: c.method });
|
|
32
|
+
},
|
|
33
|
+
onDetach(n: HSPlusNode, _c: NeRFConfig, ctx: TraitContext) {
|
|
34
|
+
delete n.__nerfState;
|
|
35
|
+
ctx.emit?.('nerf:unloaded');
|
|
36
|
+
},
|
|
37
|
+
onUpdate(n: HSPlusNode, _c: NeRFConfig, _ctx: TraitContext, _d: number) {
|
|
38
|
+
const s = n.__nerfState as Record<string, unknown> | undefined;
|
|
39
|
+
if (s?.isRendering) {
|
|
40
|
+
(s.trainStep as number)++;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
14
43
|
onEvent(n: HSPlusNode, _c: NeRFConfig, ctx: TraitContext, e: TraitEvent) {
|
|
15
|
-
const s = n.__nerfState as Record<string, unknown> | undefined;
|
|
16
|
-
if (
|
|
17
|
-
if (e.type === 'nerf:
|
|
44
|
+
const s = n.__nerfState as Record<string, unknown> | undefined;
|
|
45
|
+
if (!s) return;
|
|
46
|
+
if (e.type === 'nerf:render') {
|
|
47
|
+
s.isRendering = true;
|
|
48
|
+
ctx.emit?.('nerf:rendering');
|
|
49
|
+
}
|
|
50
|
+
if (e.type === 'nerf:stop') {
|
|
51
|
+
s.isRendering = false;
|
|
52
|
+
ctx.emit?.('nerf:stopped', { steps: s.trainStep });
|
|
53
|
+
}
|
|
18
54
|
},
|
|
19
55
|
};
|
|
20
56
|
}
|
|
@@ -2,17 +2,40 @@
|
|
|
2
2
|
import {
|
|
3
3
|
registerVolumetricNode,
|
|
4
4
|
setVolumetricVoxelPayload,
|
|
5
|
-
unregisterVolumetricNode
|
|
5
|
+
unregisterVolumetricNode,
|
|
6
6
|
} from '@holoscript/crdt-spatial';
|
|
7
7
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
8
8
|
|
|
9
|
-
export type VolumetricFormat =
|
|
10
|
-
|
|
9
|
+
export type VolumetricFormat =
|
|
10
|
+
| 'point_cloud'
|
|
11
|
+
| 'voxel'
|
|
12
|
+
| 'mesh_sequence'
|
|
13
|
+
| 'depth_map'
|
|
14
|
+
| 'holographic';
|
|
15
|
+
export interface VolumetricConfig {
|
|
16
|
+
format: VolumetricFormat;
|
|
17
|
+
resolution: number;
|
|
18
|
+
frameRate: number;
|
|
19
|
+
compressionLevel: number;
|
|
20
|
+
streamingEnabled: boolean;
|
|
21
|
+
boundingBoxM: [number, number, number];
|
|
22
|
+
colorDepth: 8 | 10 | 12;
|
|
23
|
+
}
|
|
11
24
|
|
|
12
|
-
const defaultConfig: VolumetricConfig = {
|
|
25
|
+
const defaultConfig: VolumetricConfig = {
|
|
26
|
+
format: 'point_cloud',
|
|
27
|
+
resolution: 1024,
|
|
28
|
+
frameRate: 30,
|
|
29
|
+
compressionLevel: 5,
|
|
30
|
+
streamingEnabled: false,
|
|
31
|
+
boundingBoxM: [2, 2, 2],
|
|
32
|
+
colorDepth: 8,
|
|
33
|
+
};
|
|
13
34
|
|
|
14
35
|
export function createVolumetricHandler(): TraitHandler<VolumetricConfig> {
|
|
15
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
name: 'volumetric',
|
|
38
|
+
defaultConfig,
|
|
16
39
|
onAttach(n: HSPlusNode, c: VolumetricConfig, ctx: TraitContext) {
|
|
17
40
|
if (!n.id) {
|
|
18
41
|
n.id = `vol_${Math.random().toString(36).slice(2, 11)}`;
|
|
@@ -34,14 +57,25 @@ export function createVolumetricHandler(): TraitHandler<VolumetricConfig> {
|
|
|
34
57
|
ctx.emit?.('volumetric:unloaded');
|
|
35
58
|
},
|
|
36
59
|
onUpdate(n: HSPlusNode, c: VolumetricConfig, ctx: TraitContext, delta: number) {
|
|
37
|
-
const s = n.__volState as Record<string, unknown> | undefined;
|
|
60
|
+
const s = n.__volState as Record<string, unknown> | undefined;
|
|
61
|
+
if (!s || !s.isPlaying) return;
|
|
38
62
|
(s.currentFrame as number) += c.frameRate * (delta / 1000);
|
|
39
|
-
if ((s.currentFrame as number) >= (s.totalFrames as number)) {
|
|
63
|
+
if ((s.currentFrame as number) >= (s.totalFrames as number)) {
|
|
64
|
+
s.currentFrame = 0;
|
|
65
|
+
ctx.emit?.('volumetric:loop');
|
|
66
|
+
}
|
|
40
67
|
},
|
|
41
68
|
onEvent(n: HSPlusNode, _c: VolumetricConfig, ctx: TraitContext, e: TraitEvent) {
|
|
42
|
-
const s = n.__volState as Record<string, unknown> | undefined;
|
|
43
|
-
if (
|
|
44
|
-
if (e.type === 'volumetric:
|
|
69
|
+
const s = n.__volState as Record<string, unknown> | undefined;
|
|
70
|
+
if (!s) return;
|
|
71
|
+
if (e.type === 'volumetric:play') {
|
|
72
|
+
s.isPlaying = true;
|
|
73
|
+
ctx.emit?.('volumetric:playing');
|
|
74
|
+
}
|
|
75
|
+
if (e.type === 'volumetric:pause') {
|
|
76
|
+
s.isPlaying = false;
|
|
77
|
+
ctx.emit?.('volumetric:paused');
|
|
78
|
+
}
|
|
45
79
|
const doc = ctx.spatialSync?.doc;
|
|
46
80
|
const id = n.id;
|
|
47
81
|
if (e.type === 'volumetric:sync_bytes' && doc && id) {
|
package/src/traits/types.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import type { LoroDoc } from 'loro-crdt';
|
|
2
2
|
|
|
3
|
-
export interface HSPlusNode {
|
|
3
|
+
export interface HSPlusNode {
|
|
4
|
+
id?: string;
|
|
5
|
+
properties?: Record<string, unknown>;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
4
8
|
export interface TraitContext {
|
|
5
9
|
emit?: (event: string, payload?: unknown) => void;
|
|
6
10
|
/** Injected by HoloLand / spatial host so volumetric payloads sync on the shared Loro + WebRTC doc. */
|
|
7
11
|
spatialSync?: { doc: LoroDoc };
|
|
8
12
|
[key: string]: unknown;
|
|
9
13
|
}
|
|
10
|
-
export interface TraitEvent {
|
|
11
|
-
|
|
14
|
+
export interface TraitEvent {
|
|
15
|
+
type: string;
|
|
16
|
+
payload?: Record<string, unknown>;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface TraitHandler<T = unknown> {
|
|
20
|
+
name: string;
|
|
21
|
+
defaultConfig: T;
|
|
22
|
+
onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void;
|
|
23
|
+
onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void;
|
|
24
|
+
onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void;
|
|
25
|
+
onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void;
|
|
26
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025-2026 HoloScript Contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|