abstract-3d 3.1.2 → 4.0.0-beta.10
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 +53 -1
- package/lib/abstract-3d.d.ts +27 -1
- package/lib/abstract-3d.d.ts.map +1 -1
- package/lib/abstract-3d.js +26 -2
- package/lib/abstract-3d.js.map +1 -1
- package/lib/renderers/dxf/dxf-encoding/dxf-build.d.ts.map +1 -1
- package/lib/renderers/dxf/dxf-encoding/dxf-build.js +1 -2
- package/lib/renderers/dxf/dxf-encoding/dxf-build.js.map +1 -1
- package/lib/renderers/ifc/ifc.test.d.ts +2 -0
- package/lib/renderers/ifc/ifc.test.d.ts.map +1 -0
- package/lib/renderers/ifc/ifc.test.js +380 -0
- package/lib/renderers/ifc/ifc.test.js.map +1 -0
- package/lib/renderers/index.d.ts +1 -0
- package/lib/renderers/index.d.ts.map +1 -1
- package/lib/renderers/index.js +1 -0
- package/lib/renderers/index.js.map +1 -1
- package/lib/renderers/react/index.d.ts +2 -5
- package/lib/renderers/react/index.d.ts.map +1 -1
- package/lib/renderers/react/index.js +1 -2
- package/lib/renderers/react/index.js.map +1 -1
- package/lib/renderers/react/react.d.ts +16 -42
- package/lib/renderers/react/react.d.ts.map +1 -1
- package/lib/renderers/react/react.js +56 -32
- package/lib/renderers/react/react.js.map +1 -1
- package/lib/renderers/three/appearance/appearance.d.ts +46 -0
- package/lib/renderers/three/appearance/appearance.d.ts.map +1 -0
- package/lib/renderers/three/appearance/appearance.js +102 -0
- package/lib/renderers/three/appearance/appearance.js.map +1 -0
- package/lib/renderers/three/appearance/appearance.test.d.ts +2 -0
- package/lib/renderers/three/appearance/appearance.test.d.ts.map +1 -0
- package/lib/renderers/three/appearance/appearance.test.js +97 -0
- package/lib/renderers/three/appearance/appearance.test.js.map +1 -0
- package/lib/renderers/three/appearance.d.ts +41 -0
- package/lib/renderers/three/appearance.d.ts.map +1 -0
- package/lib/renderers/three/appearance.js +100 -0
- package/lib/renderers/three/appearance.js.map +1 -0
- package/lib/renderers/three/build/build.d.ts +53 -0
- package/lib/renderers/three/build/build.d.ts.map +1 -0
- package/lib/renderers/three/build/build.js +181 -0
- package/lib/renderers/three/build/build.js.map +1 -0
- package/lib/renderers/three/build/build.test.d.ts +2 -0
- package/lib/renderers/three/build/build.test.d.ts.map +1 -0
- package/lib/renderers/three/build/build.test.js +120 -0
- package/lib/renderers/three/build/build.test.js.map +1 -0
- package/lib/renderers/three/build/materials.d.ts +20 -0
- package/lib/renderers/three/build/materials.d.ts.map +1 -0
- package/lib/renderers/three/build/materials.js +168 -0
- package/lib/renderers/three/build/materials.js.map +1 -0
- package/lib/renderers/three/build/parts.d.ts +65 -0
- package/lib/renderers/three/build/parts.d.ts.map +1 -0
- package/lib/renderers/three/build/parts.js +203 -0
- package/lib/renderers/three/build/parts.js.map +1 -0
- package/lib/renderers/three/build.d.ts +50 -0
- package/lib/renderers/three/build.d.ts.map +1 -0
- package/lib/renderers/three/build.js +210 -0
- package/lib/renderers/three/build.js.map +1 -0
- package/lib/renderers/three/camera/camera.d.ts +40 -0
- package/lib/renderers/three/camera/camera.d.ts.map +1 -0
- package/lib/renderers/three/camera/camera.js +51 -0
- package/lib/renderers/three/camera/camera.js.map +1 -0
- package/lib/renderers/three/camera/camera.test.d.ts +2 -0
- package/lib/renderers/three/camera/camera.test.d.ts.map +1 -0
- package/lib/renderers/three/camera/camera.test.js +46 -0
- package/lib/renderers/three/camera/camera.test.js.map +1 -0
- package/lib/renderers/three/camera/controls.d.ts +24 -0
- package/lib/renderers/three/camera/controls.d.ts.map +1 -0
- package/lib/renderers/three/camera/controls.js +39 -0
- package/lib/renderers/three/camera/controls.js.map +1 -0
- package/lib/renderers/three/camera/gizmo.d.ts +44 -0
- package/lib/renderers/three/camera/gizmo.d.ts.map +1 -0
- package/lib/renderers/three/camera/gizmo.js +43 -0
- package/lib/renderers/three/camera/gizmo.js.map +1 -0
- package/lib/renderers/three/camera.d.ts +63 -0
- package/lib/renderers/three/camera.d.ts.map +1 -0
- package/lib/renderers/three/camera.js +85 -0
- package/lib/renderers/three/camera.js.map +1 -0
- package/lib/renderers/three/compile/compile.d.ts +59 -0
- package/lib/renderers/three/compile/compile.d.ts.map +1 -0
- package/lib/renderers/three/compile/compile.js +88 -0
- package/lib/renderers/three/compile/compile.js.map +1 -0
- package/lib/renderers/three/compile/compile.test.d.ts +2 -0
- package/lib/renderers/three/compile/compile.test.d.ts.map +1 -0
- package/lib/renderers/three/compile/compile.test.js +93 -0
- package/lib/renderers/three/compile/compile.test.js.map +1 -0
- package/lib/renderers/three/compile.d.ts +58 -0
- package/lib/renderers/three/compile.d.ts.map +1 -0
- package/lib/renderers/three/compile.js +89 -0
- package/lib/renderers/three/compile.js.map +1 -0
- package/lib/renderers/three/controls.d.ts +27 -0
- package/lib/renderers/three/controls.d.ts.map +1 -0
- package/lib/renderers/three/controls.js +115 -0
- package/lib/renderers/three/controls.js.map +1 -0
- package/lib/renderers/three/environment/studio.d.ts +9 -0
- package/lib/renderers/three/environment/studio.d.ts.map +1 -0
- package/lib/renderers/three/environment/studio.js +15 -0
- package/lib/renderers/three/environment/studio.js.map +1 -0
- package/lib/renderers/three/geometry/geometry.d.ts +21 -0
- package/lib/renderers/three/geometry/geometry.d.ts.map +1 -0
- package/lib/renderers/three/geometry/geometry.js +141 -0
- package/lib/renderers/three/geometry/geometry.js.map +1 -0
- package/lib/renderers/three/geometry/geometry.test.d.ts +2 -0
- package/lib/renderers/three/geometry/geometry.test.d.ts.map +1 -0
- package/lib/renderers/three/geometry/geometry.test.js +80 -0
- package/lib/renderers/three/geometry/geometry.test.js.map +1 -0
- package/lib/renderers/three/geometry.d.ts +20 -0
- package/lib/renderers/three/geometry.d.ts.map +1 -0
- package/lib/renderers/three/geometry.js +164 -0
- package/lib/renderers/three/geometry.js.map +1 -0
- package/lib/renderers/three/gizmo.d.ts +49 -0
- package/lib/renderers/three/gizmo.d.ts.map +1 -0
- package/lib/renderers/three/gizmo.js +172 -0
- package/lib/renderers/three/gizmo.js.map +1 -0
- package/lib/renderers/three/index.d.ts +16 -0
- package/lib/renderers/three/index.d.ts.map +1 -0
- package/lib/renderers/three/index.js +6 -0
- package/lib/renderers/three/index.js.map +1 -0
- package/lib/renderers/three/input.d.ts +25 -0
- package/lib/renderers/three/input.d.ts.map +1 -0
- package/lib/renderers/three/input.js +71 -0
- package/lib/renderers/three/input.js.map +1 -0
- package/lib/renderers/three/lines.d.ts +16 -0
- package/lib/renderers/three/lines.d.ts.map +1 -0
- package/lib/renderers/three/lines.js +63 -0
- package/lib/renderers/three/lines.js.map +1 -0
- package/lib/renderers/three/materials.d.ts +15 -0
- package/lib/renderers/three/materials.d.ts.map +1 -0
- package/lib/renderers/three/materials.js +70 -0
- package/lib/renderers/three/materials.js.map +1 -0
- package/lib/renderers/three/options.d.ts +68 -0
- package/lib/renderers/three/options.d.ts.map +1 -0
- package/lib/renderers/three/options.js +64 -0
- package/lib/renderers/three/options.js.map +1 -0
- package/lib/renderers/three/overlays.d.ts +25 -0
- package/lib/renderers/three/overlays.d.ts.map +1 -0
- package/lib/renderers/three/overlays.js +57 -0
- package/lib/renderers/three/overlays.js.map +1 -0
- package/lib/renderers/three/quality/quality.d.ts +18 -0
- package/lib/renderers/three/quality/quality.d.ts.map +1 -0
- package/lib/renderers/three/quality/quality.js +28 -0
- package/lib/renderers/three/quality/quality.js.map +1 -0
- package/lib/renderers/three/quality/quality.test.d.ts +2 -0
- package/lib/renderers/three/quality/quality.test.d.ts.map +1 -0
- package/lib/renderers/three/quality/quality.test.js +23 -0
- package/lib/renderers/three/quality/quality.test.js.map +1 -0
- package/lib/renderers/three/quality.d.ts +24 -0
- package/lib/renderers/three/quality.d.ts.map +1 -0
- package/lib/renderers/three/quality.js +67 -0
- package/lib/renderers/three/quality.js.map +1 -0
- package/lib/renderers/three/shadow/key-light.d.ts +6 -0
- package/lib/renderers/three/shadow/key-light.d.ts.map +1 -0
- package/lib/renderers/three/shadow/key-light.js +39 -0
- package/lib/renderers/three/shadow/key-light.js.map +1 -0
- package/lib/renderers/three/shadow/shadow.d.ts +21 -0
- package/lib/renderers/three/shadow/shadow.d.ts.map +1 -0
- package/lib/renderers/three/shadow/shadow.js +120 -0
- package/lib/renderers/three/shadow/shadow.js.map +1 -0
- package/lib/renderers/three/text/text.d.ts +55 -0
- package/lib/renderers/three/text/text.d.ts.map +1 -0
- package/lib/renderers/three/text/text.js +90 -0
- package/lib/renderers/three/text/text.js.map +1 -0
- package/lib/renderers/three/text/text.test.d.ts +2 -0
- package/lib/renderers/three/text/text.test.d.ts.map +1 -0
- package/lib/renderers/three/text/text.test.js +70 -0
- package/lib/renderers/three/text/text.test.js.map +1 -0
- package/lib/renderers/three/text.d.ts +53 -0
- package/lib/renderers/three/text.d.ts.map +1 -0
- package/lib/renderers/three/text.js +151 -0
- package/lib/renderers/three/text.js.map +1 -0
- package/lib/renderers/three/viewer/options.d.ts +82 -0
- package/lib/renderers/three/viewer/options.d.ts.map +1 -0
- package/lib/renderers/three/viewer/options.js +34 -0
- package/lib/renderers/three/viewer/options.js.map +1 -0
- package/lib/renderers/three/viewer/options.test.d.ts +2 -0
- package/lib/renderers/three/viewer/options.test.d.ts.map +1 -0
- package/lib/renderers/three/viewer/options.test.js +38 -0
- package/lib/renderers/three/viewer/options.test.js.map +1 -0
- package/lib/renderers/three/viewer/overlays.d.ts +31 -0
- package/lib/renderers/three/viewer/overlays.d.ts.map +1 -0
- package/lib/renderers/three/viewer/overlays.js +43 -0
- package/lib/renderers/three/viewer/overlays.js.map +1 -0
- package/lib/renderers/three/viewer/viewer.d.ts +102 -0
- package/lib/renderers/three/viewer/viewer.d.ts.map +1 -0
- package/lib/renderers/three/viewer/viewer.js +488 -0
- package/lib/renderers/three/viewer/viewer.js.map +1 -0
- package/lib/renderers/three/viewer.d.ts +38 -0
- package/lib/renderers/three/viewer.d.ts.map +1 -0
- package/lib/renderers/three/viewer.js +435 -0
- package/lib/renderers/three/viewer.js.map +1 -0
- package/package.json +34 -16
- package/src/abstract-3d.ts +60 -2
- package/src/renderers/dxf/dxf-encoding/dxf-build.ts +1 -2
- package/src/renderers/ifc/ifc.test.ts +7 -5
- package/src/renderers/index.ts +1 -0
- package/src/renderers/react/index.ts +2 -5
- package/src/renderers/react/react.tsx +107 -181
- package/src/renderers/three/appearance/appearance.test.ts +153 -0
- package/src/renderers/three/appearance/appearance.ts +150 -0
- package/src/renderers/three/build/build.test.ts +149 -0
- package/src/renderers/three/build/build.ts +278 -0
- package/src/renderers/three/build/materials.ts +222 -0
- package/src/renderers/three/build/parts.ts +277 -0
- package/src/renderers/three/camera/camera.test.ts +53 -0
- package/src/renderers/three/camera/camera.ts +82 -0
- package/src/renderers/three/camera/controls.ts +65 -0
- package/src/renderers/three/camera/gizmo.ts +94 -0
- package/src/renderers/three/compile/compile.test.ts +126 -0
- package/src/renderers/three/compile/compile.ts +151 -0
- package/src/renderers/three/environment/studio.ts +18 -0
- package/src/renderers/three/geometry/geometry.test.ts +121 -0
- package/src/renderers/three/geometry/geometry.ts +189 -0
- package/src/renderers/three/index.ts +15 -0
- package/src/renderers/three/quality/quality.test.ts +37 -0
- package/src/renderers/three/quality/quality.ts +36 -0
- package/src/renderers/three/shadow/key-light.ts +43 -0
- package/src/renderers/three/shadow/shadow.ts +170 -0
- package/src/renderers/three/text/text.test.ts +79 -0
- package/src/renderers/three/text/text.ts +128 -0
- package/src/renderers/three/viewer/options.ts +112 -0
- package/src/renderers/three/viewer/overlays.ts +71 -0
- package/src/renderers/three/viewer/viewer.ts +596 -0
- package/lib/renderers/react/react-hotspot.d.ts +0 -27
- package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
- package/lib/renderers/react/react-hotspot.js +0 -37
- package/lib/renderers/react/react-hotspot.js.map +0 -1
- package/src/custom_typings/react-three-fiber.d.ts +0 -9
- package/src/custom_typings/react-three-postprocessing.d.ts +0 -1
- package/src/renderers/react/react-camera.tsx +0 -391
- package/src/renderers/react/react-dimension.tsx +0 -122
- package/src/renderers/react/react-group.test.ts +0 -56
- package/src/renderers/react/react-group.tsx +0 -202
- package/src/renderers/react/react-image-material.tsx +0 -139
- package/src/renderers/react/react-material.tsx +0 -91
- package/src/renderers/react/react-mesh.tsx +0 -512
- package/src/renderers/react/react-relation.tsx +0 -86
- package/src/renderers/react/react-scene.tsx +0 -111
- package/src/renderers/react/react-types.ts +0 -9
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { GlyphBitmap } from "./text.js";
|
|
3
|
+
import { createGlyphAtlas, getGlyph, layoutText, RASTER_SIZE, SDF_BUFFER } from "./text.js";
|
|
4
|
+
|
|
5
|
+
/** Glyphs of 20 px ink, 30 px of it above the baseline, a 24 px advance, filled with their char code */
|
|
6
|
+
const rasterize = (char: string): GlyphBitmap => {
|
|
7
|
+
const [glyphWidth, glyphHeight] = char === " " ? [0, 0] : [20, 40];
|
|
8
|
+
const [width, height] = [glyphWidth + 2 * SDF_BUFFER, glyphHeight + 2 * SDF_BUFFER];
|
|
9
|
+
return {
|
|
10
|
+
data: new Uint8ClampedArray(width * height).fill(char.charCodeAt(0)),
|
|
11
|
+
width,
|
|
12
|
+
height,
|
|
13
|
+
glyphWidth,
|
|
14
|
+
glyphHeight,
|
|
15
|
+
glyphLeft: 2,
|
|
16
|
+
glyphTop: 30,
|
|
17
|
+
glyphAdvance: 24,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const em = (px: number): number => px / RASTER_SIZE;
|
|
21
|
+
const round = (v: number): number => Math.round(v * 1e4) / 1e4;
|
|
22
|
+
|
|
23
|
+
describe("getGlyph", () => {
|
|
24
|
+
it("packs each glyph once, left to right, and writes its bitmap", () => {
|
|
25
|
+
const atlas = createGlyphAtlas(rasterize, 64);
|
|
26
|
+
const a = getGlyph(atlas, "A");
|
|
27
|
+
expect(getGlyph(atlas, "A")).toBe(a);
|
|
28
|
+
const b = getGlyph(atlas, "B");
|
|
29
|
+
const data = atlas.texture.image.data as Uint8Array;
|
|
30
|
+
expect([data[0], data[28]]).toEqual([65, 66]);
|
|
31
|
+
expect(a.rect.map(round)).toEqual([0, round(48 / 64), round(28 / 64), round(-48 / 64)]);
|
|
32
|
+
expect(b.rect[0]).toBe(28 / 64);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("starts a new row when a row is full", () => {
|
|
36
|
+
const atlas = createGlyphAtlas(rasterize, 128);
|
|
37
|
+
["A", "B", "C", "D"].forEach((char) => getGlyph(atlas, char));
|
|
38
|
+
expect(getGlyph(atlas, "E").rect.slice(0, 2)).toEqual([0, 96 / 128]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("gives glyphs their bitmap's position and size in em, from the pen at the baseline", () => {
|
|
42
|
+
expect(getGlyph(createGlyphAtlas(rasterize, 64), "A")).toMatchObject({
|
|
43
|
+
left: em(2 - SDF_BUFFER),
|
|
44
|
+
top: em(30 + SDF_BUFFER),
|
|
45
|
+
width: em(28),
|
|
46
|
+
height: em(48),
|
|
47
|
+
advance: em(24),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("leaves glyphs without ink out of the atlas", () => {
|
|
52
|
+
const atlas = createGlyphAtlas(rasterize, 64);
|
|
53
|
+
expect(getGlyph(atlas, " ").width).toBe(0);
|
|
54
|
+
expect(getGlyph(atlas, "A").rect[0]).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("layoutText", () => {
|
|
59
|
+
const atlas = createGlyphAtlas(rasterize, 256);
|
|
60
|
+
|
|
61
|
+
it("centers a line on the origin", () => {
|
|
62
|
+
const advance = 24 + 0.001 * 40;
|
|
63
|
+
const quads = layoutText(atlas, "AA", 40);
|
|
64
|
+
expect(quads.map((q) => round(q.x))).toEqual([round(-advance + 2 - SDF_BUFFER + 14), round(2 - SDF_BUFFER + 14)]);
|
|
65
|
+
expect(quads.map((q) => [round(q.width), round(q.height)])).toEqual([
|
|
66
|
+
[28, 48],
|
|
67
|
+
[28, 48],
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("stacks lines 1.5 font sizes apart, centered vertically as a block", () => {
|
|
72
|
+
// The block is 120 high; each baseline is half the 20 of leading and an ascent of 32 below its line's top, and the glyphs' centers 10 above it
|
|
73
|
+
expect(layoutText(atlas, "A\nA", 40).map((q) => round(q.y))).toEqual([28, -32]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("skips glyphs without ink but keeps their advance", () => {
|
|
77
|
+
expect(layoutText(atlas, "A A", 40)).toHaveLength(2);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import TinySDF from "@mapbox/tiny-sdf";
|
|
2
|
+
import { DataTexture, LinearFilter, RedFormat, UnsignedByteType } from "three";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Text as signed distance field glyphs from the system font, packed into one atlas texture, so all text draws in one call. The glyph atlas is Mapbox
|
|
6
|
+
* GL JS's approach (its @mapbox/tiny-sdf rasterizes), and the distance field text Valve's (Green, "Improved Alpha-Tested Magnification for Vector
|
|
7
|
+
* Textures and Special Effects", SIGGRAPH 2007)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const RASTER_SIZE = 40;
|
|
11
|
+
export const SDF_BUFFER = 4;
|
|
12
|
+
/** The distance field's value at the glyph's edge, from tiny-sdf's default cutoff of 0.25 */
|
|
13
|
+
export const SDF_EDGE = 0.75;
|
|
14
|
+
const LINE_HEIGHT = 1.5;
|
|
15
|
+
const LETTER_SPACING = 0.001;
|
|
16
|
+
/** Of the font size, above the baseline, to center a line's glyphs in it */
|
|
17
|
+
const ASCENT = 0.8;
|
|
18
|
+
|
|
19
|
+
export type GlyphBitmap = ReturnType<TinySDF["draw"]>;
|
|
20
|
+
export type Rasterize = (char: string) => GlyphBitmap;
|
|
21
|
+
|
|
22
|
+
export type Glyph = {
|
|
23
|
+
/** The glyph's place in the atlas as a uv offset and scale, for a quad with uv (0, 0) at its bottom left */
|
|
24
|
+
readonly rect: readonly [number, number, number, number];
|
|
25
|
+
/** The bitmap's left and top edges and size, in em, from the pen at the baseline; no width for glyphs without ink */
|
|
26
|
+
readonly left: number;
|
|
27
|
+
readonly top: number;
|
|
28
|
+
readonly width: number;
|
|
29
|
+
readonly height: number;
|
|
30
|
+
readonly advance: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type GlyphAtlas = {
|
|
34
|
+
readonly texture: DataTexture;
|
|
35
|
+
readonly size: number;
|
|
36
|
+
readonly glyphs: Map<string, Glyph>;
|
|
37
|
+
/** Where the next glyph goes: the current row's top and next x, and its height */
|
|
38
|
+
readonly shelf: { x: number; y: number; height: number };
|
|
39
|
+
readonly rasterize: Rasterize | undefined;
|
|
40
|
+
readonly sdf: { current?: TinySDF };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** A quad of a laid out text, with its center and size in the text's units, centered on the origin */
|
|
44
|
+
export type GlyphQuad = {
|
|
45
|
+
readonly glyph: Glyph;
|
|
46
|
+
readonly x: number;
|
|
47
|
+
readonly y: number;
|
|
48
|
+
readonly width: number;
|
|
49
|
+
readonly height: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Without `rasterize`, glyphs are rasterized with the OS's built-in Helvetica or Arial, on a canvas created at first use */
|
|
53
|
+
export function createGlyphAtlas(rasterize?: Rasterize, size = 1024): GlyphAtlas {
|
|
54
|
+
const texture = new DataTexture(new Uint8Array(size * size), size, size, RedFormat, UnsignedByteType);
|
|
55
|
+
Object.assign(texture, { minFilter: LinearFilter, magFilter: LinearFilter, generateMipmaps: false });
|
|
56
|
+
return { texture, size, glyphs: new Map(), shelf: { x: 0, y: 0, height: 0 }, rasterize, sdf: {} };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getGlyph(atlas: GlyphAtlas, char: string): Glyph {
|
|
60
|
+
const cached = atlas.glyphs.get(char);
|
|
61
|
+
if (cached) {
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
64
|
+
atlas.sdf.current ??= atlas.rasterize
|
|
65
|
+
? undefined
|
|
66
|
+
: new TinySDF({ fontSize: RASTER_SIZE, buffer: SDF_BUFFER, radius: 10, fontFamily: "Helvetica, Arial, sans-serif" });
|
|
67
|
+
const bitmap = atlas.rasterize?.(char) ?? atlas.sdf.current!.draw(char);
|
|
68
|
+
const rect = bitmap.width > 2 * SDF_BUFFER && bitmap.height > 2 * SDF_BUFFER ? packGlyph(atlas, bitmap) : undefined;
|
|
69
|
+
const glyph: Glyph = {
|
|
70
|
+
rect: rect ?? [0, 0, 0, 0],
|
|
71
|
+
left: (bitmap.glyphLeft - SDF_BUFFER) / RASTER_SIZE,
|
|
72
|
+
top: (bitmap.glyphTop + SDF_BUFFER) / RASTER_SIZE,
|
|
73
|
+
width: rect ? bitmap.width / RASTER_SIZE : 0,
|
|
74
|
+
height: rect ? bitmap.height / RASTER_SIZE : 0,
|
|
75
|
+
advance: bitmap.glyphAdvance / RASTER_SIZE,
|
|
76
|
+
};
|
|
77
|
+
atlas.glyphs.set(char, glyph);
|
|
78
|
+
return glyph;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Copies the bitmap to the atlas, left to right in rows, or leaves it out when the atlas is full; shelf packing, as @mapbox/shelf-pack does */
|
|
82
|
+
function packGlyph({ texture, size, shelf }: GlyphAtlas, bitmap: GlyphBitmap): Glyph["rect"] | undefined {
|
|
83
|
+
if (shelf.x + bitmap.width > size) {
|
|
84
|
+
Object.assign(shelf, { x: 0, y: shelf.y + shelf.height, height: 0 });
|
|
85
|
+
}
|
|
86
|
+
if (shelf.y + bitmap.height > size) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const { x, y } = shelf;
|
|
90
|
+
const data = texture.image.data as Uint8Array;
|
|
91
|
+
for (let row = 0; row < bitmap.height; row++) {
|
|
92
|
+
data.set(bitmap.data.subarray(row * bitmap.width, (row + 1) * bitmap.width), (y + row) * size + x);
|
|
93
|
+
}
|
|
94
|
+
Object.assign(shelf, { x: x + bitmap.width, height: Math.max(shelf.height, bitmap.height) });
|
|
95
|
+
texture.needsUpdate = true;
|
|
96
|
+
// Bitmap rows run top down from the texture's v = 0, while the quad's uv runs bottom up
|
|
97
|
+
return [x / size, (y + bitmap.height) / size, bitmap.width / size, -bitmap.height / size];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Lays out the text centered on the origin, each line centered, as drei's Text (troika-three-text) did with the r3f renderer's line height and
|
|
102
|
+
* spacing
|
|
103
|
+
*/
|
|
104
|
+
export function layoutText(atlas: GlyphAtlas, text: string, fontSize: number): ReadonlyArray<GlyphQuad> {
|
|
105
|
+
const lineHeight = LINE_HEIGHT * fontSize;
|
|
106
|
+
const lines = text.split("\n");
|
|
107
|
+
return lines.flatMap((line, i) => {
|
|
108
|
+
const glyphs = Array.from(line, (char) => getGlyph(atlas, char));
|
|
109
|
+
const advances = glyphs.map((g) => (g.advance + LETTER_SPACING) * fontSize);
|
|
110
|
+
const baseline = (lines.length * lineHeight) / 2 - i * lineHeight - (lineHeight - fontSize) / 2 - ASCENT * fontSize;
|
|
111
|
+
const pen = { x: -advances.reduce((sum, a) => sum + a, 0) / 2 };
|
|
112
|
+
return glyphs.flatMap((glyph, j) => {
|
|
113
|
+
const x = pen.x;
|
|
114
|
+
pen.x += advances[j]!;
|
|
115
|
+
return glyph.width === 0
|
|
116
|
+
? []
|
|
117
|
+
: [
|
|
118
|
+
{
|
|
119
|
+
glyph,
|
|
120
|
+
x: x + (glyph.left + glyph.width / 2) * fontSize,
|
|
121
|
+
y: baseline + (glyph.top - glyph.height / 2) * fontSize,
|
|
122
|
+
width: glyph.width * fontSize,
|
|
123
|
+
height: glyph.height * fontSize,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Object3D } from "three";
|
|
2
|
+
import type { Group, Relation, Scene, Vec3, View } from "../../../abstract-3d.js";
|
|
3
|
+
import type { MaterialState } from "../appearance/appearance.js";
|
|
4
|
+
import type { BufferZones, CameraOptions } from "../camera/camera.js";
|
|
5
|
+
import type { ControlsOptions } from "../camera/controls.js";
|
|
6
|
+
import type { ControlsHelper } from "../camera/gizmo.js";
|
|
7
|
+
import type { Quality } from "../quality/quality.js";
|
|
8
|
+
import type { MountOverlays } from "./overlays.js";
|
|
9
|
+
|
|
10
|
+
/** The viewer's options, and what passing each of them in an update redoes; telling which actually changed is the caller's, e.g. React's dependencies */
|
|
11
|
+
|
|
12
|
+
export type Popover<Content> = { readonly id: string; readonly pos: Vec3; readonly content: Content };
|
|
13
|
+
|
|
14
|
+
export type SceneEvent = {
|
|
15
|
+
readonly nativeEvent: MouseEvent;
|
|
16
|
+
/** Where the pointer hit, in world space */
|
|
17
|
+
readonly point: Vec3;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A group's data: of the top-level group, and of the group the hit mesh is in */
|
|
21
|
+
type Data = Record<string, string> | undefined;
|
|
22
|
+
|
|
23
|
+
export type ViewerOptions<Content> = {
|
|
24
|
+
/** Undefined shows the fallback */
|
|
25
|
+
readonly scene?: Scene;
|
|
26
|
+
/** The id of a top-level group, which its hover, selection, material state and events are by; groups without an id can't be clicked */
|
|
27
|
+
readonly createGroupId?: (g: Group) => string;
|
|
28
|
+
readonly selectedIds?: Record<string, boolean>;
|
|
29
|
+
readonly hoveredIds?: ReadonlyArray<string>;
|
|
30
|
+
readonly activeComponents?: Record<string, MaterialState>;
|
|
31
|
+
/**
|
|
32
|
+
* The ids of the relations whose shape is drawn to be clicked, e.g. where a component can be inserted. While defined, even when empty, only the
|
|
33
|
+
* groups whose material state is Accept can be clicked
|
|
34
|
+
*/
|
|
35
|
+
readonly activeRelations?: ReadonlyArray<string>;
|
|
36
|
+
readonly showDimensions?: boolean;
|
|
37
|
+
/** View, buffer zones and padding are read when the scene is fitted: first, and for a new camera or canvas aspect */
|
|
38
|
+
readonly camera?: CameraOptions;
|
|
39
|
+
readonly view?: View;
|
|
40
|
+
readonly bufferZones?: BufferZones;
|
|
41
|
+
readonly fitPadding?: number;
|
|
42
|
+
readonly controls?: ControlsOptions;
|
|
43
|
+
readonly gizmo?: ControlsHelper;
|
|
44
|
+
/** Lights added to the photographed studio and key light that light the scene; none by default */
|
|
45
|
+
readonly lights?: ReadonlyArray<Object3D> | ((scene: Scene) => ReadonlyArray<Object3D>);
|
|
46
|
+
/** Of the studio's light, or without the studio of its plain light, 0.6 by default, the key light giving the rest */
|
|
47
|
+
readonly environmentIntensity?: number;
|
|
48
|
+
/**
|
|
49
|
+
* The photographed studio's light and reflections, on by default. Off, three-point lighting emulates it, a weak sky and floor light, the key light,
|
|
50
|
+
* a fill and a rim light, without reflections, so metals are satin at most; a first visit then shows the scene about 400 ms sooner, as its shaders
|
|
51
|
+
* compile without the reflection code. Changing it rebuilds the scene
|
|
52
|
+
*/
|
|
53
|
+
readonly studio?: boolean;
|
|
54
|
+
/** A soft shadow on the ground under the scene, rendered once per build; on by default */
|
|
55
|
+
readonly groundShadow?: boolean;
|
|
56
|
+
/** The solids' shadows on themselves, e.g. under a roof, cast by the key light from the upper left front; on by default */
|
|
57
|
+
readonly shadows?: boolean;
|
|
58
|
+
/** The resolution: `low` 1 and `medium` up to 1.5 pixels per CSS pixel, `high`, the default, the screen's; `auto` high until moving gets slow */
|
|
59
|
+
readonly quality?: Quality;
|
|
60
|
+
readonly useAlphaTest?: boolean;
|
|
61
|
+
readonly materialStateImages?: Record<string, string>;
|
|
62
|
+
readonly popovers?: ReadonlyArray<Popover<Content>>;
|
|
63
|
+
readonly getTooltips?: (id: string) => ReadonlyArray<Popover<Content>>;
|
|
64
|
+
/** Mounts popover and tooltip content, e.g. as React portals; without it, strings and DOM nodes are mounted directly */
|
|
65
|
+
readonly onOverlays?: MountOverlays<Content>;
|
|
66
|
+
/** A built scene's shaders are compiled, so its first frame renders without a stall, e.g. to hide a fallback */
|
|
67
|
+
readonly onLoad?: () => void;
|
|
68
|
+
readonly onClickGroup?: (id: string, rootData: Data, data: Data, event: SceneEvent) => void;
|
|
69
|
+
/** Called with no id when the pointer leaves the group */
|
|
70
|
+
readonly onHoverGroup?: (id: string | undefined, rootData: Data, data: Data, event: SceneEvent) => void;
|
|
71
|
+
readonly onContextMenuGroup?: (id: string, rootData: Data, data: Data, left: number, top: number, event: SceneEvent) => void;
|
|
72
|
+
readonly onClickRelation?: (relation: Relation, event: SceneEvent) => void;
|
|
73
|
+
/** A click, not a drag, on nothing clickable, e.g. the background; r3f's Canvas event of the same name */
|
|
74
|
+
readonly onPointerMissed?: (event: MouseEvent) => void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** What an update has to redo, in the order it's redone */
|
|
78
|
+
export type Change = "compile" | "build" | "shadow" | "camera" | "controls" | "gizmo" | "lights" | "quality" | "paint" | "dimensions" | "overlays";
|
|
79
|
+
|
|
80
|
+
/** What passing each option redoes; callbacks and what's read later need nothing */
|
|
81
|
+
export const effects: { readonly [K in keyof ViewerOptions<unknown>]-?: ReadonlyArray<Change> } = {
|
|
82
|
+
scene: ["compile", "build", "lights", "overlays"],
|
|
83
|
+
createGroupId: ["compile", "build"],
|
|
84
|
+
activeComponents: ["build"],
|
|
85
|
+
activeRelations: ["build"],
|
|
86
|
+
useAlphaTest: ["build"],
|
|
87
|
+
materialStateImages: ["build"],
|
|
88
|
+
selectedIds: ["paint"],
|
|
89
|
+
hoveredIds: ["paint"],
|
|
90
|
+
showDimensions: ["dimensions"],
|
|
91
|
+
camera: ["camera"],
|
|
92
|
+
view: [],
|
|
93
|
+
bufferZones: [],
|
|
94
|
+
fitPadding: [],
|
|
95
|
+
controls: ["controls"],
|
|
96
|
+
gizmo: ["gizmo"],
|
|
97
|
+
lights: ["lights"],
|
|
98
|
+
environmentIntensity: ["lights"],
|
|
99
|
+
studio: ["build", "lights"],
|
|
100
|
+
groundShadow: ["shadow"],
|
|
101
|
+
shadows: ["shadow"],
|
|
102
|
+
quality: ["quality"],
|
|
103
|
+
popovers: ["overlays"],
|
|
104
|
+
getTooltips: ["overlays"],
|
|
105
|
+
onOverlays: [],
|
|
106
|
+
onLoad: [],
|
|
107
|
+
onClickGroup: ["paint"],
|
|
108
|
+
onHoverGroup: [],
|
|
109
|
+
onContextMenuGroup: [],
|
|
110
|
+
onClickRelation: [],
|
|
111
|
+
onPointerMissed: [],
|
|
112
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Scene, type Vector3 } from "three";
|
|
2
|
+
import { CSS2DObject, CSS2DRenderer } from "three/addons/renderers/CSS2DRenderer.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTML elements over the canvas at 3D anchors, e.g. popovers and tooltips, placed by three.js's CSS2DRenderer; their content is mounted by the app,
|
|
6
|
+
* or directly
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Overlay<Content> = {
|
|
10
|
+
readonly key: string;
|
|
11
|
+
/** In world space */
|
|
12
|
+
readonly position: Vector3;
|
|
13
|
+
readonly content: Content;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type MountedOverlay<Content> = { readonly key: string; readonly element: HTMLElement; readonly content: Content };
|
|
17
|
+
|
|
18
|
+
/** Mounts the overlays' content into their elements, called when the set of overlays or their content changes */
|
|
19
|
+
export type MountOverlays<Content> = (overlays: ReadonlyArray<MountedOverlay<Content>>) => void;
|
|
20
|
+
|
|
21
|
+
export type OverlayLayer<Content> = {
|
|
22
|
+
readonly renderer: CSS2DRenderer;
|
|
23
|
+
readonly scene: Scene;
|
|
24
|
+
readonly mounted: Map<string, { readonly object: CSS2DObject; overlay: Overlay<Content> }>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function createOverlayLayer<Content>(container: HTMLElement): OverlayLayer<Content> {
|
|
28
|
+
const renderer = new CSS2DRenderer();
|
|
29
|
+
renderer.domElement.style.cssText = "position:absolute;inset:0;overflow:hidden;pointer-events:none";
|
|
30
|
+
container.append(renderer.domElement);
|
|
31
|
+
return { renderer, scene: new Scene(), mounted: new Map() };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Without `mount`, strings and DOM nodes are mounted as they are */
|
|
35
|
+
export function setOverlays<Content>(
|
|
36
|
+
layer: OverlayLayer<Content>,
|
|
37
|
+
overlays: ReadonlyArray<Overlay<Content>>,
|
|
38
|
+
mount: MountOverlays<Content> | undefined
|
|
39
|
+
): void {
|
|
40
|
+
const keys = new Set<string>();
|
|
41
|
+
const changed = { current: false };
|
|
42
|
+
overlays.forEach((overlay) => {
|
|
43
|
+
keys.add(overlay.key);
|
|
44
|
+
const existing = layer.mounted.get(overlay.key);
|
|
45
|
+
changed.current ||= existing?.overlay.content !== overlay.content;
|
|
46
|
+
const object = existing?.object ?? new CSS2DObject(document.createElement("div"));
|
|
47
|
+
object.position.copy(overlay.position);
|
|
48
|
+
if (existing) {
|
|
49
|
+
existing.overlay = overlay;
|
|
50
|
+
} else {
|
|
51
|
+
object.element.style.pointerEvents = "none";
|
|
52
|
+
layer.scene.add(object);
|
|
53
|
+
layer.mounted.set(overlay.key, { object, overlay });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
layer.mounted.forEach(({ object }, key) => {
|
|
57
|
+
if (!keys.has(key)) {
|
|
58
|
+
// Its element is removed with it
|
|
59
|
+
layer.scene.remove(object);
|
|
60
|
+
layer.mounted.delete(key);
|
|
61
|
+
changed.current = true;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (changed.current && mount) {
|
|
65
|
+
mount(Array.from(layer.mounted, ([key, { object, overlay }]) => ({ key, element: object.element, content: overlay.content })));
|
|
66
|
+
} else if (changed.current) {
|
|
67
|
+
layer.mounted.forEach(({ object, overlay: { content } }) =>
|
|
68
|
+
object.element.replaceChildren(content instanceof Node ? content : String(content ?? ""))
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|