@thatopen/services 0.3.10 → 0.3.12
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/cli/commands/revit.d.ts +9 -0
- package/dist/cli/lib/revit-addin.d.ts +20 -0
- package/dist/cli.js +101 -0
- package/dist/cloud/LasToPotree.d.ts +59 -0
- package/dist/cloud/SplatToFq.d.ts +38 -0
- package/dist/cloud/index.d.ts +2 -0
- package/docs/builtin/paths.json +86 -86
- package/docs/cloud/GaussianSplatConverter.md +43 -0
- package/docs/cloud/PointCloudConverter.md +53 -0
- package/docs/revit-collab-quickstart.md +151 -0
- package/package.json +95 -95
- package/src/built-in/ClashesManager/example.ts +283 -283
- package/src/built-in/UIManager/src/clashes-panel/example.ts +160 -160
- package/src/built-in/UIManager/src/models-list/example.ts +122 -118
- package/src/cli/templates/app/README.md +217 -241
- package/src/cli/templates/app/package.json +33 -33
- package/src/cli/templates/app/src/setups/camera-tools.ts +0 -63
- package/src/cli/templates/app/src/setups/camera-views.ts +0 -201
- package/src/cli/templates/app/src/setups/card-header.ts +0 -35
- package/src/cli/templates/app/src/setups/clipper-tool.ts +0 -773
- package/src/cli/templates/app/src/setups/clipper.ts +0 -75
- package/src/cli/templates/app/src/setups/cloud-runner.ts +0 -13
- package/src/cli/templates/app/src/setups/exploded-view.ts +0 -246
- package/src/cli/templates/app/src/setups/helper-panel.ts +0 -88
- package/src/cli/templates/app/src/setups/hider.ts +0 -98
- package/src/cli/templates/app/src/setups/index.ts +0 -23
- package/src/cli/templates/app/src/setups/inspection.ts +0 -136
- package/src/cli/templates/app/src/setups/measurement-panel.ts +0 -188
- package/src/cli/templates/app/src/setups/measurement-tool.ts +0 -588
- package/src/cli/templates/app/src/setups/measurements.ts +0 -133
- package/src/cli/templates/app/src/setups/navigation-gizmo.ts +0 -316
- package/src/cli/templates/app/src/setups/plans-panel.ts +0 -619
- package/src/cli/templates/app/src/setups/right-sidebar.ts +0 -178
- package/src/cli/templates/app/src/setups/styles-panel.ts +0 -110
- package/src/cli/templates/app/src/setups/styles.ts +0 -375
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +0 -156
- package/src/cli/templates/app/src/setups/tool-mode.ts +0 -23
- package/src/cli/templates/app/src/setups/toolbar.ts +0 -229
- package/src/cli/templates/app/src/setups/ui-manager.ts +0 -27
|
@@ -1,773 +0,0 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import * as OBC from "@thatopen/components";
|
|
3
|
-
import * as OBF from "@thatopen/components-front";
|
|
4
|
-
import { toolModeManager, type ManagedTool } from "./tool-mode-manager";
|
|
5
|
-
import type { InstanceRow } from "./inspection";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Clipping / section-plane TOOL (logic only; the UI lives in clipper-panel.ts).
|
|
9
|
-
*
|
|
10
|
-
* Two parts:
|
|
11
|
-
*
|
|
12
|
-
* 1. PLANES (OBC.Clipper) — placing mode (double-click a surface drops a plane on
|
|
13
|
-
* the picked face), drag the gizmo to move, Delete removes the hovered plane,
|
|
14
|
-
* per-plane + master enable, clear-all. `localClippingPlanes` is on (before any
|
|
15
|
-
* plane) so cuts sit exactly on the plane.
|
|
16
|
-
*
|
|
17
|
-
* 2. SECTION STYLING (OBF.ClipStyler) — one global "Section" style (a fill + an
|
|
18
|
-
* edge line) applied to the whole model ("All") at every clip plane, matching
|
|
19
|
-
* the stock ClipStyler example. The deferred-overlay treatment (depthTest/
|
|
20
|
-
* depthWrite off, line resolution, renderOrder-on-top, registration into
|
|
21
|
-
* `postproduction.basePass.isolatedMaterials`) lives centrally in ClipEdges, so
|
|
22
|
-
* here we only supply the fill/edge colours + opacity. Colour changes rebuild
|
|
23
|
-
* the section edges (cheap).
|
|
24
|
-
*/
|
|
25
|
-
type ClipEdges = ReturnType<OBF.ClipStyler["createFromClipping"]>;
|
|
26
|
-
|
|
27
|
-
export interface SectionStyle {
|
|
28
|
-
fill: string; // "#rrggbb"
|
|
29
|
-
line: string; // "#rrggbb"
|
|
30
|
-
opacity: number; // 0..1 fill opacity
|
|
31
|
-
width: number; // edge line width (px)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ClipperTool {
|
|
35
|
-
readonly clipper: OBC.Clipper;
|
|
36
|
-
/** Fires when the set of planes (or their enabled state) changes. */
|
|
37
|
-
readonly onChanged: OBC.Event<void>;
|
|
38
|
-
/** Fires when the section style (colours/opacity/visibility) changes. */
|
|
39
|
-
readonly onStyleChanged: OBC.Event<void>;
|
|
40
|
-
|
|
41
|
-
// ── Planes ──
|
|
42
|
-
setPlacing(on: boolean): void;
|
|
43
|
-
isPlacing(): boolean;
|
|
44
|
-
setEnabled(on: boolean): void;
|
|
45
|
-
isEnabled(): boolean;
|
|
46
|
-
/** Enable/disable a single plane AND show/hide its section accordingly. */
|
|
47
|
-
setPlaneEnabled(id: string, on: boolean): void;
|
|
48
|
-
deletePlane(id: string): void;
|
|
49
|
-
clearAll(): void;
|
|
50
|
-
/** Per-plane rows for the Objects outliner (W2): hide / enable / delete. */
|
|
51
|
-
instances(): InstanceRow[];
|
|
52
|
-
|
|
53
|
-
// ── Single global section style ──
|
|
54
|
-
getSectionStyle(): SectionStyle;
|
|
55
|
-
setSectionFill(hex: string): void;
|
|
56
|
-
setSectionLine(hex: string): void;
|
|
57
|
-
setSectionOpacity(value: number): void;
|
|
58
|
-
setSectionWidth(value: number): void;
|
|
59
|
-
/** Master visibility of the section fills/edges. */
|
|
60
|
-
setStylingVisible(on: boolean): void;
|
|
61
|
-
isStylingVisible(): boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const CLASSIFICATION = "Categories"; // OBC.Classifier.byCategory default
|
|
65
|
-
export const STYLE_NAME = "Section";
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Register the global "Section" ClipStyler style idempotently, so other
|
|
69
|
-
* consumers (e.g. the floor-plans panel calling `createFromView`) can reference
|
|
70
|
-
* it by name WITHOUT depending on `clipperTool()` having constructed first.
|
|
71
|
-
* No-op if it already exists — `clipperTool` registers a live, user-editable
|
|
72
|
-
* instance in its `rebuildStyles`, and this must not clobber that one.
|
|
73
|
-
*/
|
|
74
|
-
export const ensureSectionStyle = (components: OBC.Components) => {
|
|
75
|
-
const styler = components.get(OBF.ClipStyler);
|
|
76
|
-
if (styler.styles.get(STYLE_NAME)) return;
|
|
77
|
-
styler.styles.set(STYLE_NAME, {
|
|
78
|
-
// NearPlaneLineMaterial: a fat line (real px width) that ALSO discards
|
|
79
|
-
// segments behind the camera plane in-shader, so it gives thick edges WITHOUT
|
|
80
|
-
// the near-plane "infinity streak" the stock fat LineMaterial produced.
|
|
81
|
-
linesMaterial: new OBF.NearPlaneLineMaterial({
|
|
82
|
-
color: "#111111",
|
|
83
|
-
linewidth: 2,
|
|
84
|
-
}),
|
|
85
|
-
fillsMaterial: new THREE.MeshBasicMaterial({
|
|
86
|
-
color: "#a96eec",
|
|
87
|
-
side: THREE.DoubleSide,
|
|
88
|
-
}),
|
|
89
|
-
});
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const clipperToolImpl = (components: OBC.Components): ClipperTool => {
|
|
93
|
-
const clipper = components.get(OBC.Clipper);
|
|
94
|
-
clipper.localClippingPlanes = true; // before any plane is created
|
|
95
|
-
clipper.enabled = true;
|
|
96
|
-
clipper.visible = true;
|
|
97
|
-
// Constant-size plane representation (world units): disable auto-scaling (which
|
|
98
|
-
// otherwise sizes each plane to the model/surface) and pin a fixed size — tune
|
|
99
|
-
// the single value to taste.
|
|
100
|
-
clipper.autoScalePlanes = false;
|
|
101
|
-
clipper.size = 5;
|
|
102
|
-
|
|
103
|
-
const styler = components.get(OBF.ClipStyler);
|
|
104
|
-
const classifier = components.get(OBC.Classifier);
|
|
105
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
106
|
-
const worlds = components.get(OBC.Worlds);
|
|
107
|
-
const getWorld = () => [...worlds.list.values()][0] as OBC.World | undefined;
|
|
108
|
-
|
|
109
|
-
const onChanged = new OBC.Event<void>();
|
|
110
|
-
const onStyleChanged = new OBC.Event<void>();
|
|
111
|
-
|
|
112
|
-
const manager = toolModeManager(components);
|
|
113
|
-
|
|
114
|
-
// ── Plane placing / canvas binding ───────────────────────────────
|
|
115
|
-
let placing = false;
|
|
116
|
-
let canvas: HTMLElement | undefined;
|
|
117
|
-
|
|
118
|
-
// Registered with the ToolMode manager: when another tool takes over, exit
|
|
119
|
-
// placing mode locally (the manager handles hover/select suppression).
|
|
120
|
-
const managed: ManagedTool = {
|
|
121
|
-
id: "clipper",
|
|
122
|
-
label: "Drawing clipping plane",
|
|
123
|
-
icon: "mdi:scissors-cutting",
|
|
124
|
-
onDeactivate: () => {
|
|
125
|
-
placing = false;
|
|
126
|
-
if (canvas) canvas.style.cursor = "";
|
|
127
|
-
onChanged.trigger();
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const onDblClick = () => {
|
|
132
|
-
const world = getWorld();
|
|
133
|
-
if (!placing || !world) return;
|
|
134
|
-
void clipper.create(world);
|
|
135
|
-
};
|
|
136
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
137
|
-
if (event.code !== "Delete" && event.code !== "Backspace") return;
|
|
138
|
-
const world = getWorld();
|
|
139
|
-
if (world) void clipper.delete(world);
|
|
140
|
-
};
|
|
141
|
-
const ensureCanvas = () => {
|
|
142
|
-
if (canvas) return canvas;
|
|
143
|
-
const renderer = getWorld()?.renderer as
|
|
144
|
-
| OBF.PostproductionRenderer
|
|
145
|
-
| undefined;
|
|
146
|
-
const c = renderer?.three.domElement;
|
|
147
|
-
if (c) {
|
|
148
|
-
canvas = c;
|
|
149
|
-
canvas.addEventListener("dblclick", onDblClick);
|
|
150
|
-
window.addEventListener("keydown", onKeyDown);
|
|
151
|
-
}
|
|
152
|
-
return canvas;
|
|
153
|
-
};
|
|
154
|
-
ensureCanvas();
|
|
155
|
-
|
|
156
|
-
// ── Section styling state ────────────────────────────────────────
|
|
157
|
-
// `categories` is only kept to satisfy buildEdgesFor's classify gate (the
|
|
158
|
-
// section builds once the model is classified); it no longer drives styling.
|
|
159
|
-
let categories: string[] = [];
|
|
160
|
-
const edgesByPlane = new Map<string, ClipEdges>();
|
|
161
|
-
const planesWired = new Set<string>();
|
|
162
|
-
// Planes currently being dragged. SimplePlane fires TransformControls "change"
|
|
163
|
-
// every drag tick → notifyManager → clipper.list.onItemSet, which would
|
|
164
|
-
// re-section the plane LIVE on every frame. That spams overlapping async
|
|
165
|
-
// rebuilds (which race and drop edges — the "missing edges while dragging") and
|
|
166
|
-
// costs a full re-section per frame. We instead BLINK: hide on drag-start, skip
|
|
167
|
-
// the per-frame rebuild while dragging, and rebuild once on release.
|
|
168
|
-
const draggingPlanes = new Set<string>();
|
|
169
|
-
// Gizmo (TransformControls) + section-plane-mesh materials we registered into
|
|
170
|
-
// the postproduction overlay set per plane, so we can unregister on delete.
|
|
171
|
-
const gizmoMatsByPlane = new Map<string, THREE.Material[]>();
|
|
172
|
-
|
|
173
|
-
const sectionStyle: SectionStyle = {
|
|
174
|
-
// Pre-compensated purple: the deferred postproduction linearizes scene
|
|
175
|
-
// colors (sRGB→linear ≈ c^2.4), so #6528d7 would render as #2105ad. Feeding
|
|
176
|
-
// the linear→sRGB inverse (#a96eec) makes the fill GRADE to the app purple
|
|
177
|
-
// #6528d7 on screen, while still shaded naturally by the PEN scene.
|
|
178
|
-
fill: "#a96eec",
|
|
179
|
-
line: "#111111",
|
|
180
|
-
opacity: 1,
|
|
181
|
-
width: 2,
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// One global "Section" style (fill + edge line) applied to the whole model
|
|
185
|
-
// ("All"), matching the stock ClipStyler example. The deferred-overlay
|
|
186
|
-
// treatment (depthTest/depthWrite off, line resolution, renderOrder-on-top,
|
|
187
|
-
// overlay registration) lives centrally in ClipEdges.
|
|
188
|
-
const rebuildStyles = () => {
|
|
189
|
-
styler.styles.set(STYLE_NAME, {
|
|
190
|
-
// NearPlaneLineMaterial: fat line (real px width via `linewidth`) whose
|
|
191
|
-
// shader discards segments behind the camera plane, so we get thick section
|
|
192
|
-
// edges with NO near-plane "infinity streak". Re-enables the Edge-width
|
|
193
|
-
// control (the setter drives `linewidth`). ClipEdges' fat-line path
|
|
194
|
-
// (isLineMaterial) builds a LineSegments2 and keeps `resolution` in sync.
|
|
195
|
-
linesMaterial: new OBF.NearPlaneLineMaterial({
|
|
196
|
-
color: sectionStyle.line,
|
|
197
|
-
linewidth: sectionStyle.width,
|
|
198
|
-
}),
|
|
199
|
-
fillsMaterial: new THREE.MeshBasicMaterial({
|
|
200
|
-
color: sectionStyle.fill,
|
|
201
|
-
side: THREE.DoubleSide,
|
|
202
|
-
transparent: sectionStyle.opacity < 1,
|
|
203
|
-
opacity: sectionStyle.opacity,
|
|
204
|
-
}),
|
|
205
|
-
});
|
|
206
|
-
};
|
|
207
|
-
rebuildStyles();
|
|
208
|
-
|
|
209
|
-
const buildItems = () => {
|
|
210
|
-
return { All: { style: STYLE_NAME } } as Record<
|
|
211
|
-
string,
|
|
212
|
-
{ style: string; data?: Record<string, string[]> }
|
|
213
|
-
>;
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const buildEdgesFor = (planeId: string) => {
|
|
217
|
-
const plane = clipper.list.get(planeId);
|
|
218
|
-
if (!getWorld() || categories.length === 0 || !plane) return;
|
|
219
|
-
// A DISABLED plane applies no cut, so it must show no section — hide its
|
|
220
|
-
// existing fill/edges and don't compute a new one. Re-enabling fires
|
|
221
|
-
// onItemSet again (via SimplePlane.notifyManager), which rebuilds + shows it.
|
|
222
|
-
if (!plane.enabled) {
|
|
223
|
-
const existing = edgesByPlane.get(planeId);
|
|
224
|
-
if (existing) existing.visible = false;
|
|
225
|
-
// Request a frame so the now-hidden section disappears immediately rather
|
|
226
|
-
// than lingering until the next camera move (the deferred overlay only
|
|
227
|
-
// re-composites on demand).
|
|
228
|
-
getWorld()?.renderer?.update();
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
styler.world = getWorld() ?? null;
|
|
232
|
-
// Drop any previous edges for this plane (DataMap delete disposes them).
|
|
233
|
-
if (styler.list.has(planeId)) styler.list.delete(planeId);
|
|
234
|
-
edgesByPlane.delete(planeId);
|
|
235
|
-
|
|
236
|
-
const items = buildItems();
|
|
237
|
-
if (Object.keys(items).length === 0) return;
|
|
238
|
-
// link:false → no auto onDraggingEnded/onDisposed listeners (we manage those
|
|
239
|
-
// once per plane below, so rebuilds don't accumulate handlers).
|
|
240
|
-
const edges = styler.createFromClipping(planeId, {
|
|
241
|
-
id: planeId,
|
|
242
|
-
items,
|
|
243
|
-
link: false,
|
|
244
|
-
world: getWorld() ?? undefined,
|
|
245
|
-
});
|
|
246
|
-
edgesByPlane.set(planeId, edges);
|
|
247
|
-
// edges.update() is async (awaits getSection); the new overlay materials
|
|
248
|
-
// only exist once it resolves. Kick a render THEN so the recoloured/rebuilt
|
|
249
|
-
// section appears immediately instead of staying blank until a camera move
|
|
250
|
-
// invalidates the deferred frame.
|
|
251
|
-
void Promise.resolve(edges.update())
|
|
252
|
-
.then(() => {
|
|
253
|
-
// The regenerated section fill/edge meshes keep a stale bounding sphere,
|
|
254
|
-
// so three.js frustum-culls them even when they're on screen (the fill
|
|
255
|
-
// flickers out as the camera moves). Disable culling on them — they're
|
|
256
|
-
// tiny section overlays, so the cull saving is irrelevant.
|
|
257
|
-
edges.three.traverse((o) => {
|
|
258
|
-
o.frustumCulled = false;
|
|
259
|
-
});
|
|
260
|
-
getWorld()?.renderer?.update();
|
|
261
|
-
})
|
|
262
|
-
.catch((error) =>
|
|
263
|
-
console.warn("[clipper] section build skipped:", error),
|
|
264
|
-
);
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const rebuildAllEdges = () => {
|
|
268
|
-
for (const [id] of clipper.list) buildEdgesFor(id);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// One drag listener per plane; it updates whatever edges currently belong to
|
|
272
|
-
// the plane (survives rebuilds since it reads edgesByPlane live).
|
|
273
|
-
const wirePlane = (planeId: string) => {
|
|
274
|
-
if (planesWired.has(planeId)) return;
|
|
275
|
-
const plane = clipper.list.get(planeId);
|
|
276
|
-
if (!plane) return;
|
|
277
|
-
planesWired.add(planeId);
|
|
278
|
-
// Suppress the dynamic-anchor pivot dot while the plane gizmo is being
|
|
279
|
-
// dragged (it would otherwise pop up on the press). The dot renders off
|
|
280
|
-
// world.onDynamicAnchorSet, so turning dynamicAnchor off for the drag stops
|
|
281
|
-
// it cleanly with no change to viewports-manager.
|
|
282
|
-
plane.onDraggingStarted.add(() => {
|
|
283
|
-
draggingPlanes.add(planeId); // suppress the per-frame live re-section (blink)
|
|
284
|
-
const world = getWorld() as unknown as { dynamicAnchor?: boolean } | undefined;
|
|
285
|
-
if (world) world.dynamicAnchor = false;
|
|
286
|
-
// Re-prune the TransformControls guide lines (the ±1e6 AXIS/X/Y/Z helper
|
|
287
|
-
// lines that read as thin "infinity" lines): three creates/shows them
|
|
288
|
-
// lazily, so the one-shot prune at registerGizmo time can run before they
|
|
289
|
-
// exist. By drag time they're present — prune + hide again (idempotent).
|
|
290
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
291
|
-
const helper = (plane as any).controls?.getHelper?.();
|
|
292
|
-
pruneInfiniteGuides(helper);
|
|
293
|
-
hideGizmoHelpers(helper);
|
|
294
|
-
tintGizmo(planeId); // keep the handle purple through the drag highlight
|
|
295
|
-
// Hide this plane's section fill + edges while dragging — they'd be stale
|
|
296
|
-
// mid-move anyway, and hiding dodges the per-frame re-section cost. Re-shown
|
|
297
|
-
// + recomputed on release below.
|
|
298
|
-
const edges = edgesByPlane.get(planeId);
|
|
299
|
-
if (edges) edges.visible = false;
|
|
300
|
-
});
|
|
301
|
-
plane.onDraggingEnded.add(() => {
|
|
302
|
-
draggingPlanes.delete(planeId); // re-enable rebuilds; do the one rebuild below
|
|
303
|
-
const world = getWorld() as unknown as { dynamicAnchor?: boolean } | undefined;
|
|
304
|
-
if (world) world.dynamicAnchor = true;
|
|
305
|
-
const edges = edgesByPlane.get(planeId);
|
|
306
|
-
if (edges) {
|
|
307
|
-
// Rebuild the section at the NEW plane position FIRST; only reveal it once
|
|
308
|
-
// the rebuild resolves. Showing before the update lets the stale geometry
|
|
309
|
-
// (still at the pre-drag position) render for a frame → a flash of the old
|
|
310
|
-
// section snapping to the new one. update() is async (awaits getSection),
|
|
311
|
-
// so flip visible + kick a render in its completion, never before it.
|
|
312
|
-
void Promise.resolve(edges.update())
|
|
313
|
-
.then(() => {
|
|
314
|
-
edges.visible = true;
|
|
315
|
-
getWorld()?.renderer?.update();
|
|
316
|
-
})
|
|
317
|
-
.catch((error) =>
|
|
318
|
-
console.warn("[clipper] section update skipped:", error),
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
// Classify loaded models by IFC category, refresh the category list + styles,
|
|
325
|
-
// and (re)build all section edges. Resilient: models can be mid-load or
|
|
326
|
-
// disposed when fragments.list fires (the project has multiple frags), so the
|
|
327
|
-
// worker may not yet/no longer have a model id — byCategory then throws
|
|
328
|
-
// "Model not found". We debounce (coalesce the multi-frag load), wrap in
|
|
329
|
-
// try/catch, and retry once the list settles, so a transient miss never
|
|
330
|
-
// surfaces as an uncaught runtime error.
|
|
331
|
-
let classifyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
332
|
-
const scheduleClassify = () => {
|
|
333
|
-
if (classifyTimer) clearTimeout(classifyTimer);
|
|
334
|
-
classifyTimer = setTimeout(() => void classify(), 250);
|
|
335
|
-
};
|
|
336
|
-
const classify = async () => {
|
|
337
|
-
if (fragments.list.size === 0) {
|
|
338
|
-
categories = [];
|
|
339
|
-
rebuildAllEdges();
|
|
340
|
-
onStyleChanged.trigger();
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
try {
|
|
344
|
-
await classifier.byCategory();
|
|
345
|
-
} catch (error) {
|
|
346
|
-
// A model wasn't queryable yet (mid-load) — retry after it settles.
|
|
347
|
-
console.warn("[clipper] classification deferred:", error);
|
|
348
|
-
scheduleClassify();
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
const groups = classifier.list.get(CLASSIFICATION);
|
|
352
|
-
categories = groups ? [...groups.keys()].sort() : [];
|
|
353
|
-
rebuildAllEdges();
|
|
354
|
-
onStyleChanged.trigger();
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
fragments.list.onItemSet.add(scheduleClassify);
|
|
358
|
-
fragments.list.onItemDeleted.add(scheduleClassify);
|
|
359
|
-
if (fragments.list.size > 0) scheduleClassify();
|
|
360
|
-
|
|
361
|
-
// The postproduction overlay material set, or undefined on a non-postproduction
|
|
362
|
-
// renderer / before it's initialised (the basePass getter throws if early).
|
|
363
|
-
const isolatedMaterials = (): THREE.Material[] | undefined => {
|
|
364
|
-
const renderer = getWorld()?.renderer as
|
|
365
|
-
| { postproduction?: { basePass?: { isolatedMaterials?: THREE.Material[] } } }
|
|
366
|
-
| undefined;
|
|
367
|
-
try {
|
|
368
|
-
return renderer?.postproduction?.basePass?.isolatedMaterials;
|
|
369
|
-
} catch {
|
|
370
|
-
return undefined;
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
// The clipper's gizmo (TransformControls helper) + section-plane mesh are basic
|
|
375
|
-
// single-output materials. classify() hides them by OBJECT visibility, but
|
|
376
|
-
// TransformControls re-asserts handle visibility in updateMatrixWorld, so they
|
|
377
|
-
// leak into the deferred MRT capture and spam "missing fragment shader
|
|
378
|
-
// outputs". Register their materials into the overlay set so the deferred path
|
|
379
|
-
// hides them at MATERIAL level during capture and redraws them on top; mark
|
|
380
|
-
// preserveBlending so the overlay redraw keeps the gizmo's normal look.
|
|
381
|
-
// three.js TransformControls draws infinite axis/delta GUIDE lines (named
|
|
382
|
-
// X/Y/Z/AXIS/DELTA, extending ±1e6) to show the active drag axis. In the
|
|
383
|
-
// deferred overlay those render as black streaks running to the horizon. Remove
|
|
384
|
-
// them — detected by a huge geometry extent so we drop only the guides, never
|
|
385
|
-
// the small draggable handles.
|
|
386
|
-
const pruneInfiniteGuides = (root: THREE.Object3D | undefined) => {
|
|
387
|
-
if (!root) return;
|
|
388
|
-
const doomed: THREE.Object3D[] = [];
|
|
389
|
-
root.traverse((o) => {
|
|
390
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
391
|
-
const anyO = o as any;
|
|
392
|
-
if (
|
|
393
|
-
!(anyO.isLine || anyO.isLineSegments || anyO.isLine2 || anyO.isLineSegments2)
|
|
394
|
-
) {
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const geo = (o as THREE.Line).geometry as THREE.BufferGeometry | undefined;
|
|
398
|
-
if (!geo) return;
|
|
399
|
-
geo.computeBoundingBox?.();
|
|
400
|
-
const bb = geo.boundingBox;
|
|
401
|
-
if (bb && bb.min.distanceTo(bb.max) > 1e4) doomed.push(o);
|
|
402
|
-
});
|
|
403
|
-
for (const o of doomed) o.removeFromParent();
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
// The clip plane's TransformControls draws helper clutter that has nothing to do
|
|
407
|
-
// with the draggable handle: an opaque WHITE bounds cylinder (renders as a solid
|
|
408
|
-
// white volume) plus translucent plane/scale/rotate guide quads, pickers and
|
|
409
|
-
// helper octahedrons. With postproduction ON the deferred capture hides them, but
|
|
410
|
-
// the forward path (the cheap resize path turns postpro off) draws them — that's
|
|
411
|
-
// the "white volume" + "guide quads on resize". Hide every NON-handle helper at
|
|
412
|
-
// MATERIAL level and flag it `keepHidden` so the PostproductionRenderer.enabled
|
|
413
|
-
// setter no longer force-reveals it. We keep only the actual translate arrows:
|
|
414
|
-
// in three's TransformControls the arrow/shaft materials are fully opaque
|
|
415
|
-
// (opacity 1), while every guide/picker/helper material is translucent
|
|
416
|
-
// (opacity < 1) — a stable signal set once at material creation. The opaque
|
|
417
|
-
// white bounds cylinder is the lone opaque exception, caught explicitly.
|
|
418
|
-
// material.visible is never touched by TransformControls (it only re-asserts
|
|
419
|
-
// object.visible per frame), so this sticks; raycast picking ignores it so
|
|
420
|
-
// dragging still works.
|
|
421
|
-
const hideGizmoHelpers = (root: THREE.Object3D | undefined) => {
|
|
422
|
-
if (!root) return;
|
|
423
|
-
root.traverse((o) => {
|
|
424
|
-
const mesh = o as THREE.Mesh;
|
|
425
|
-
if (!mesh.isMesh) return;
|
|
426
|
-
const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
427
|
-
for (const m of list) {
|
|
428
|
-
if (!m) continue;
|
|
429
|
-
const basic = m as THREE.MeshBasicMaterial;
|
|
430
|
-
const translucent = basic.transparent && (basic.opacity ?? 1) < 1;
|
|
431
|
-
const opaqueWhite =
|
|
432
|
-
!basic.transparent &&
|
|
433
|
-
(basic.opacity ?? 1) >= 1 &&
|
|
434
|
-
basic.color?.getHex?.() === 0xffffff &&
|
|
435
|
-
mesh.geometry?.type === "CylinderGeometry";
|
|
436
|
-
if (translucent || opaqueWhite) {
|
|
437
|
-
m.visible = false;
|
|
438
|
-
m.userData.keepHidden = true;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
const registerGizmo = (planeId: string) => {
|
|
445
|
-
const isolated = isolatedMaterials();
|
|
446
|
-
if (!isolated) return;
|
|
447
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
448
|
-
const plane = clipper.list.get(planeId) as any;
|
|
449
|
-
if (!plane) return;
|
|
450
|
-
// Strip the infinite axis/delta guide lines first so we neither register nor
|
|
451
|
-
// draw them, and hide the opaque white bounds cylinder (the white volume).
|
|
452
|
-
pruneInfiniteGuides(plane.controls?.getHelper?.());
|
|
453
|
-
hideGizmoHelpers(plane.controls?.getHelper?.());
|
|
454
|
-
const mats: THREE.Material[] = [];
|
|
455
|
-
const collect = (root: THREE.Object3D | undefined) =>
|
|
456
|
-
root?.traverse((o) => {
|
|
457
|
-
const m = (o as THREE.Mesh).material;
|
|
458
|
-
if (!m) return;
|
|
459
|
-
for (const mat of Array.isArray(m) ? m : [m]) if (mat) mats.push(mat);
|
|
460
|
-
});
|
|
461
|
-
collect(plane.helper);
|
|
462
|
-
collect(plane.controls?.getHelper?.());
|
|
463
|
-
for (const m of mats) {
|
|
464
|
-
m.userData.preserveBlending = true;
|
|
465
|
-
if (!isolated.includes(m)) isolated.push(m);
|
|
466
|
-
}
|
|
467
|
-
gizmoMatsByPlane.set(planeId, mats);
|
|
468
|
-
|
|
469
|
-
// three creates/shows the ±1e6 AXIS/X/Y/Z guide lines lazily, so they may not
|
|
470
|
-
// exist yet at this synchronous pass. Re-prune next frame to catch them even
|
|
471
|
-
// before the first drag (they can render visible pre-interaction).
|
|
472
|
-
requestAnimationFrame(() => {
|
|
473
|
-
const helper = plane.controls?.getHelper?.();
|
|
474
|
-
pruneInfiniteGuides(helper);
|
|
475
|
-
hideGizmoHelpers(helper);
|
|
476
|
-
getWorld()?.renderer?.update();
|
|
477
|
-
});
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
const unregisterGizmo = (planeId: string) => {
|
|
481
|
-
const mats = gizmoMatsByPlane.get(planeId);
|
|
482
|
-
if (!mats) return;
|
|
483
|
-
const isolated = isolatedMaterials();
|
|
484
|
-
if (isolated) {
|
|
485
|
-
for (const m of mats) {
|
|
486
|
-
const i = isolated.indexOf(m);
|
|
487
|
-
if (i >= 0) isolated.splice(i, 1);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
gizmoMatsByPlane.delete(planeId);
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
// ── Gizmo POLISH: recolor the draggable handle to the app purple ──────────
|
|
494
|
-
// The clip-plane handle (three TransformControls arrows) ships in stock axis
|
|
495
|
-
// colours, which read as "raw". The gizmo is overlay-composited (ungraded), so
|
|
496
|
-
// a literal #6528d7 renders as the true app purple. The only catch: three
|
|
497
|
-
// re-applies a cached original colour (`material._color`) every
|
|
498
|
-
// updateMatrixWorld before layering the hover/active highlight — so we recolour
|
|
499
|
-
// BOTH the live colour AND that cache, or our purple snaps back on hover/drag.
|
|
500
|
-
// Targets only the opaque, visible arrow handles (the translucent guides + the
|
|
501
|
-
// white bounds cylinder are skipped, same signal hideGizmoHelpers uses).
|
|
502
|
-
const HANDLE_PURPLE = 0x6528d7;
|
|
503
|
-
const tintGizmo = (planeId: string) => {
|
|
504
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
505
|
-
const plane = clipper.list.get(planeId) as any;
|
|
506
|
-
const helper = plane?.controls?.getHelper?.();
|
|
507
|
-
if (!helper) return;
|
|
508
|
-
// SimplePlane sets showX/showY false → only the Z (normal) translate gizmo
|
|
509
|
-
// shows. three's TC translate gizmo puts an arrowhead at BOTH +z and −z (plus
|
|
510
|
-
// a thin line); the −z one is the "bodyless" arrow pointing away from the
|
|
511
|
-
// plane. Drop it. (object.visible would be re-asserted each frame, so
|
|
512
|
-
// removeFromParent like the guide-line prune.) The drag hit-area is the
|
|
513
|
-
// separate centered _arrowBoundBox picker, so removing the visible back
|
|
514
|
-
// arrowhead doesn't affect dragging.
|
|
515
|
-
const doomed: THREE.Object3D[] = [];
|
|
516
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
517
|
-
helper.traverse((o: any) => {
|
|
518
|
-
if (!o.isMesh) return;
|
|
519
|
-
const list = Array.isArray(o.material) ? o.material : [o.material];
|
|
520
|
-
const opaqueVisible = list.some(
|
|
521
|
-
(m: any) => m && m.visible !== false && (m.opacity ?? 1) >= 1,
|
|
522
|
-
);
|
|
523
|
-
// Back arrowhead: three's setupGizmo BAKES the layout offset into the
|
|
524
|
-
// GEOMETRY and resets mesh.position to (0,0,0) — so detect by the geometry's
|
|
525
|
-
// bbox center, not mesh.position. The translate cones are CylinderGeometry;
|
|
526
|
-
// the one baked at z≈-0.5 is the bodyless back arrow (the +0.5 front arrow
|
|
527
|
-
// and the z≈0 line/shaft stay; invisible pickers excluded by opaqueVisible).
|
|
528
|
-
const geo = o.geometry as THREE.BufferGeometry | undefined;
|
|
529
|
-
if (geo && !geo.boundingBox) geo.computeBoundingBox?.();
|
|
530
|
-
const bb = geo?.boundingBox;
|
|
531
|
-
const centerZ = bb ? (bb.min.z + bb.max.z) / 2 : 0;
|
|
532
|
-
if (opaqueVisible && geo?.type === "CylinderGeometry" && centerZ < -0.2) {
|
|
533
|
-
doomed.push(o);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
for (const m of list) {
|
|
537
|
-
if (!m?.color?.setHex) continue;
|
|
538
|
-
const opacity = m.opacity ?? 1;
|
|
539
|
-
// three's TC arrow materials are transparent:true WITH opacity 1, so we
|
|
540
|
-
// must NOT require !transparent (that skipped the handles entirely — why
|
|
541
|
-
// the recolor looked like nothing changed). Recolor every VISIBLE handle
|
|
542
|
-
// (opacity >= 1), matching hideGizmoHelpers' kept set; skip only the faded
|
|
543
|
-
// guides/pickers (opacity < 1) and the opaque white bounds cylinder.
|
|
544
|
-
const translucent = m.transparent && opacity < 1;
|
|
545
|
-
const whiteCylinder =
|
|
546
|
-
opacity >= 1 &&
|
|
547
|
-
m.color.getHex?.() === 0xffffff &&
|
|
548
|
-
o.geometry?.type === "CylinderGeometry";
|
|
549
|
-
if (translucent || whiteCylinder) continue;
|
|
550
|
-
m.color.setHex(HANDLE_PURPLE);
|
|
551
|
-
if (m._color?.setHex) m._color.setHex(HANDLE_PURPLE); // patch TC's cache
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
for (const o of doomed) o.removeFromParent();
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
// Plane lifecycle → edges + panel refresh.
|
|
558
|
-
clipper.list.onItemSet.add(({ key }) => {
|
|
559
|
-
wirePlane(key);
|
|
560
|
-
registerGizmo(key);
|
|
561
|
-
tintGizmo(key);
|
|
562
|
-
// three creates/colours the handle materials lazily, so re-tint next frame to
|
|
563
|
-
// catch anything not present in this synchronous pass.
|
|
564
|
-
requestAnimationFrame(() => {
|
|
565
|
-
tintGizmo(key);
|
|
566
|
-
getWorld()?.renderer?.update();
|
|
567
|
-
});
|
|
568
|
-
// While the plane is being dragged, skip the live re-section (blink): the
|
|
569
|
-
// section stays hidden until release, where onDraggingEnded rebuilds it once.
|
|
570
|
-
if (!draggingPlanes.has(key)) buildEdgesFor(key);
|
|
571
|
-
onChanged.trigger();
|
|
572
|
-
});
|
|
573
|
-
clipper.list.onItemDeleted.add((key) => {
|
|
574
|
-
if (styler.list.has(key)) styler.list.delete(key);
|
|
575
|
-
edgesByPlane.delete(key);
|
|
576
|
-
planesWired.delete(key);
|
|
577
|
-
unregisterGizmo(key);
|
|
578
|
-
onChanged.trigger();
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// ── In-place section style mutation ──────────────────────────────
|
|
582
|
-
// Mutate the LIVE drawn materials rather than rebuilding the ClipEdges: a
|
|
583
|
-
// rebuild tears down + recreates the overlay meshes, which vanish until a
|
|
584
|
-
// camera move re-composites. Local-clipping mode CLONES the style materials per
|
|
585
|
-
// ClipEdges, so we must touch the actual rendered clones (inside each
|
|
586
|
-
// ClipEdges.three), not the style template — and also the template so newly
|
|
587
|
-
// built sections (and W2's plan views) match. Colour/width are pure uniforms
|
|
588
|
-
// (no recompile); only a transparent-flag flip needs needsUpdate.
|
|
589
|
-
// rAF-coalesced render kick: dragging a colour/opacity/width control fires many
|
|
590
|
-
// input events per frame, each of which would otherwise force a full deferred
|
|
591
|
-
// re-composite. Collapse them to at most one recomposite per frame.
|
|
592
|
-
let _renderScheduled = false;
|
|
593
|
-
const requestRender = () => {
|
|
594
|
-
if (_renderScheduled) return;
|
|
595
|
-
_renderScheduled = true;
|
|
596
|
-
requestAnimationFrame(() => {
|
|
597
|
-
_renderScheduled = false;
|
|
598
|
-
getWorld()?.renderer?.update();
|
|
599
|
-
});
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
const templateMaterials = () =>
|
|
603
|
-
styler.styles.get(STYLE_NAME) as
|
|
604
|
-
| { linesMaterial?: THREE.Material; fillsMaterial?: THREE.Material }
|
|
605
|
-
| undefined;
|
|
606
|
-
|
|
607
|
-
const eachSectionMaterial = (
|
|
608
|
-
which: "line" | "fill",
|
|
609
|
-
fn: (m: THREE.Material) => void,
|
|
610
|
-
) => {
|
|
611
|
-
// Detect line vs fill by OBJECT type (covers both the fat LineSegments2 and a
|
|
612
|
-
// plain THREE.LineSegments) rather than a material flag, so it's robust to
|
|
613
|
-
// whichever line material the style uses.
|
|
614
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
615
|
-
const isLineObj = (o: any) =>
|
|
616
|
-
!!(o.isLine || o.isLineSegments || o.isLine2 || o.isLineSegments2);
|
|
617
|
-
const tpl = templateMaterials();
|
|
618
|
-
const tplMat = which === "line" ? tpl?.linesMaterial : tpl?.fillsMaterial;
|
|
619
|
-
if (tplMat) fn(tplMat);
|
|
620
|
-
for (const [, edges] of edgesByPlane) {
|
|
621
|
-
edges.three.traverse((o) => {
|
|
622
|
-
const m = (o as THREE.Mesh).material as THREE.Material | undefined;
|
|
623
|
-
if (!m) return;
|
|
624
|
-
if ((which === "line") === isLineObj(o)) fn(m);
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
return {
|
|
630
|
-
clipper,
|
|
631
|
-
onChanged,
|
|
632
|
-
onStyleChanged,
|
|
633
|
-
|
|
634
|
-
setPlacing(on) {
|
|
635
|
-
placing = on;
|
|
636
|
-
const c = ensureCanvas();
|
|
637
|
-
if (c) c.style.cursor = on ? "crosshair" : "";
|
|
638
|
-
// Route through the central manager: entering placing makes this the sole
|
|
639
|
-
// active tool (suppressing hover/select + exiting any other tool); exiting
|
|
640
|
-
// restores them.
|
|
641
|
-
if (on) manager.setActive(managed);
|
|
642
|
-
else manager.clearActive(managed);
|
|
643
|
-
},
|
|
644
|
-
isPlacing: () => placing,
|
|
645
|
-
setEnabled(on) {
|
|
646
|
-
clipper.enabled = on;
|
|
647
|
-
for (const [, plane] of clipper.list) plane.enabled = on;
|
|
648
|
-
// Explicitly resync every section to its plane's enabled state (don't rely
|
|
649
|
-
// on SimplePlane.notifyManager → onItemSet, which the DataMap can dedup).
|
|
650
|
-
rebuildAllEdges();
|
|
651
|
-
onChanged.trigger();
|
|
652
|
-
},
|
|
653
|
-
isEnabled: () => clipper.enabled,
|
|
654
|
-
setPlaneEnabled(id, on) {
|
|
655
|
-
const plane = clipper.list.get(id);
|
|
656
|
-
if (!plane) return;
|
|
657
|
-
plane.enabled = on;
|
|
658
|
-
// buildEdgesFor hides the section when disabled, rebuilds + shows it when
|
|
659
|
-
// enabled — called directly so it never depends on onItemSet firing.
|
|
660
|
-
buildEdgesFor(id);
|
|
661
|
-
onChanged.trigger();
|
|
662
|
-
},
|
|
663
|
-
deletePlane(id) {
|
|
664
|
-
const world = getWorld();
|
|
665
|
-
if (world) void clipper.delete(world, id);
|
|
666
|
-
},
|
|
667
|
-
clearAll() {
|
|
668
|
-
clipper.deleteAll();
|
|
669
|
-
onChanged.trigger();
|
|
670
|
-
},
|
|
671
|
-
instances() {
|
|
672
|
-
const rows: InstanceRow[] = [];
|
|
673
|
-
let i = 0;
|
|
674
|
-
for (const [id, plane] of clipper.list) {
|
|
675
|
-
const idx = ++i;
|
|
676
|
-
const p = plane as unknown as { visible: boolean; enabled: boolean };
|
|
677
|
-
rows.push({
|
|
678
|
-
id,
|
|
679
|
-
kind: "clip",
|
|
680
|
-
type: "Clip plane",
|
|
681
|
-
label: `Clip plane ${idx}`,
|
|
682
|
-
visible: p.visible,
|
|
683
|
-
enabled: p.enabled,
|
|
684
|
-
setVisible: (on) => {
|
|
685
|
-
// Hide the gizmo + plane mesh AND the section fill/edges together.
|
|
686
|
-
p.visible = on;
|
|
687
|
-
const edges = edgesByPlane.get(id);
|
|
688
|
-
if (edges) edges.visible = on;
|
|
689
|
-
requestRender();
|
|
690
|
-
onChanged.trigger();
|
|
691
|
-
},
|
|
692
|
-
setEnabled: (on) => {
|
|
693
|
-
plane.enabled = on;
|
|
694
|
-
buildEdgesFor(id); // hides section when off, rebuilds + shows when on
|
|
695
|
-
onChanged.trigger();
|
|
696
|
-
},
|
|
697
|
-
remove: () => {
|
|
698
|
-
const world = getWorld();
|
|
699
|
-
if (world) void clipper.delete(world, id);
|
|
700
|
-
},
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
return rows;
|
|
704
|
-
},
|
|
705
|
-
|
|
706
|
-
getSectionStyle: () => ({ ...sectionStyle }),
|
|
707
|
-
setSectionFill(hex) {
|
|
708
|
-
sectionStyle.fill = hex;
|
|
709
|
-
// MeshBasicMaterial.color is a Color — mutate in place (uniform, no recompile).
|
|
710
|
-
eachSectionMaterial("fill", (m) =>
|
|
711
|
-
(m as THREE.MeshBasicMaterial).color.set(hex),
|
|
712
|
-
);
|
|
713
|
-
requestRender();
|
|
714
|
-
onStyleChanged.trigger();
|
|
715
|
-
},
|
|
716
|
-
setSectionLine(hex) {
|
|
717
|
-
sectionStyle.line = hex;
|
|
718
|
-
// LineMaterial.color assigns straight to the diffuse uniform — give it a
|
|
719
|
-
// THREE.Color (a string uploads as black); uniform-only, no recompile.
|
|
720
|
-
eachSectionMaterial("line", (m) => {
|
|
721
|
-
(m as unknown as { color: THREE.Color }).color = new THREE.Color(hex);
|
|
722
|
-
});
|
|
723
|
-
requestRender();
|
|
724
|
-
onStyleChanged.trigger();
|
|
725
|
-
},
|
|
726
|
-
setSectionOpacity(value) {
|
|
727
|
-
sectionStyle.opacity = value;
|
|
728
|
-
// Only the opacity uniform — keep `transparent` TRUE (set in the lib so the
|
|
729
|
-
// section sorts in front of the translucent hover). Opacity still applies:
|
|
730
|
-
// the overlay pass writes (colour, alpha) and straight-alpha composites it,
|
|
731
|
-
// so the fill stays translucent without toggling the transparent flag (which
|
|
732
|
-
// would drop the section back into the opaque group, behind the hover).
|
|
733
|
-
eachSectionMaterial("fill", (m) => {
|
|
734
|
-
m.opacity = value;
|
|
735
|
-
});
|
|
736
|
-
requestRender();
|
|
737
|
-
onStyleChanged.trigger();
|
|
738
|
-
},
|
|
739
|
-
setSectionWidth(value) {
|
|
740
|
-
sectionStyle.width = value;
|
|
741
|
-
// LineMaterial.linewidth is a uniform — no recompile.
|
|
742
|
-
eachSectionMaterial("line", (m) => {
|
|
743
|
-
(m as unknown as { linewidth: number }).linewidth = value;
|
|
744
|
-
});
|
|
745
|
-
requestRender();
|
|
746
|
-
onStyleChanged.trigger();
|
|
747
|
-
},
|
|
748
|
-
setStylingVisible(on) {
|
|
749
|
-
styler.visible = on;
|
|
750
|
-
onStyleChanged.trigger();
|
|
751
|
-
},
|
|
752
|
-
isStylingVisible: () => styler.visible,
|
|
753
|
-
};
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
// Real OBC.Component wrapper so the ClipperTool wrapper is a per-`components`
|
|
757
|
-
// singleton, resolved by UUID via `components.get(...)`. Because `components` is
|
|
758
|
-
// a single shared instance across bundle boundaries, the toolbar built-in and
|
|
759
|
-
// the app's side panels (clipper-panel, objects) all get the SAME ClipperTool.
|
|
760
|
-
export class ClipperToolComponent extends OBC.Component {
|
|
761
|
-
static readonly uuid = "9b2c7e10-3a4f-4c21-8b6d-1f0a2c3d4e51" as const;
|
|
762
|
-
enabled = true;
|
|
763
|
-
readonly tool: ClipperTool;
|
|
764
|
-
constructor(components: OBC.Components) {
|
|
765
|
-
super(components);
|
|
766
|
-
components.add(ClipperToolComponent.uuid, this);
|
|
767
|
-
this.tool = clipperToolImpl(components);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/** Per-Components singleton accessor for the clipping tool. */
|
|
772
|
-
export const clipperTool = (components: OBC.Components): ClipperTool =>
|
|
773
|
-
components.get(ClipperToolComponent).tool;
|