@liquidcars/atlas 0.1.39 → 0.1.41
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 +32 -3
- package/dist/liquidcars-atlas.js +993 -739
- package/dist/liquidcars-atlas.js.map +1 -1
- package/package.json +1 -1
- package/src/liquidcars-atlas.js +318 -2
package/package.json
CHANGED
package/src/liquidcars-atlas.js
CHANGED
|
@@ -76,6 +76,48 @@ function normalizeGeometryResource(resource, index, sourceIds) {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function normalizeGeometryShowcase(pack, namespace) {
|
|
80
|
+
if (!pack.showcase || typeof pack.showcase !== "object") return null;
|
|
81
|
+
const value = cloneGeometryMetadata(pack.showcase);
|
|
82
|
+
const groups = Array.isArray(value.groups) ? value.groups : [];
|
|
83
|
+
const groupIds = new Set();
|
|
84
|
+
const itemIds = new Set();
|
|
85
|
+
const normalizedGroups = groups.map((source, groupIndex) => {
|
|
86
|
+
const group = source && typeof source === "object" ? source : {};
|
|
87
|
+
const id = String(group.id || `group-${groupIndex + 1}`).trim();
|
|
88
|
+
if (!id || groupIds.has(id)) throw new TypeError(`Geometry pack '${namespace}' showcase contains a duplicate or empty group id.`);
|
|
89
|
+
groupIds.add(id);
|
|
90
|
+
const items = Array.isArray(group.items) ? group.items : Array.isArray(group.children) ? group.children : [];
|
|
91
|
+
return {
|
|
92
|
+
...group,
|
|
93
|
+
id,
|
|
94
|
+
name: String(group.name || group.title || id),
|
|
95
|
+
items: items.map((sourceItem, itemIndex) => {
|
|
96
|
+
const item = sourceItem && typeof sourceItem === "object" ? sourceItem : {};
|
|
97
|
+
const geometry = String(item.geometry || item.kind || "").trim();
|
|
98
|
+
if (!geometry) throw new TypeError(`Geometry pack '${namespace}' showcase item ${itemIndex + 1} in '${id}' requires a geometry.`);
|
|
99
|
+
const itemId = String(item.id || `${id}-${itemIndex + 1}`).trim();
|
|
100
|
+
if (!itemId || itemIds.has(itemId)) throw new TypeError(`Geometry pack '${namespace}' showcase contains a duplicate or empty item id.`);
|
|
101
|
+
itemIds.add(itemId);
|
|
102
|
+
return {
|
|
103
|
+
...item,
|
|
104
|
+
id: itemId,
|
|
105
|
+
geometry: geometry.includes(".") ? geometry : `${namespace}.${geometry}`,
|
|
106
|
+
geometryOptions: cloneGeometryMetadata(item.geometryOptions || item.options || item.parameters || {}),
|
|
107
|
+
geometryPalette: cloneGeometryMetadata(item.geometryPalette || item.palette || {})
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
...value,
|
|
114
|
+
title: String(value.title || pack.name || namespace),
|
|
115
|
+
description: String(value.description || ""),
|
|
116
|
+
groups: normalizedGroups,
|
|
117
|
+
relations: cloneGeometryMetadata(Array.isArray(value.relations) ? value.relations : [])
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
79
121
|
function normalizeGeometryPackMetadata(pack, namespace) {
|
|
80
122
|
const sources = (Array.isArray(pack.sources) ? pack.sources : []).map(normalizeGeometrySource);
|
|
81
123
|
const sourceIds = new Set(sources.map(source => source.id));
|
|
@@ -96,6 +138,7 @@ function normalizeGeometryPackMetadata(pack, namespace) {
|
|
|
96
138
|
license: normalizeGeometryLicense(pack.license),
|
|
97
139
|
attribution: String(pack.attribution || ""),
|
|
98
140
|
collections: cloneGeometryMetadata(Array.isArray(pack.collections) ? pack.collections : []),
|
|
141
|
+
showcase: normalizeGeometryShowcase(pack, namespace),
|
|
99
142
|
sources,
|
|
100
143
|
resources,
|
|
101
144
|
geometries: []
|
|
@@ -491,8 +534,227 @@ function iconTexture(value, foreground = "#ffffff", background = "transparent")
|
|
|
491
534
|
return texture;
|
|
492
535
|
}
|
|
493
536
|
|
|
537
|
+
const SVG_PATH_COMMAND = /^[AaCcHhLlMmQqSsTtVvZz]$/;
|
|
538
|
+
const SVG_PATH_TOKEN = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?|[AaCcHhLlMmQqSsTtVvZz]/g;
|
|
539
|
+
|
|
540
|
+
function svgArcPoints(x1, y1, rxValue, ryValue, rotation, largeArc, sweep, x2, y2) {
|
|
541
|
+
let rx = Math.abs(Number(rxValue));
|
|
542
|
+
let ry = Math.abs(Number(ryValue));
|
|
543
|
+
if (!rx || !ry || (x1 === x2 && y1 === y2)) return [[x2, y2]];
|
|
544
|
+
const phi = Number(rotation || 0) * Math.PI / 180;
|
|
545
|
+
const cosPhi = Math.cos(phi);
|
|
546
|
+
const sinPhi = Math.sin(phi);
|
|
547
|
+
const dx = (x1 - x2) / 2;
|
|
548
|
+
const dy = (y1 - y2) / 2;
|
|
549
|
+
const xPrime = cosPhi * dx + sinPhi * dy;
|
|
550
|
+
const yPrime = -sinPhi * dx + cosPhi * dy;
|
|
551
|
+
const scale = Math.sqrt(Math.max(1, xPrime * xPrime / (rx * rx) + yPrime * yPrime / (ry * ry)));
|
|
552
|
+
rx *= scale;
|
|
553
|
+
ry *= scale;
|
|
554
|
+
const numerator = Math.max(0, rx * rx * ry * ry - rx * rx * yPrime * yPrime - ry * ry * xPrime * xPrime);
|
|
555
|
+
const denominator = Math.max(Number.EPSILON, rx * rx * yPrime * yPrime + ry * ry * xPrime * xPrime);
|
|
556
|
+
const sign = Boolean(largeArc) === Boolean(sweep) ? -1 : 1;
|
|
557
|
+
const coefficient = sign * Math.sqrt(numerator / denominator);
|
|
558
|
+
const centerPrimeX = coefficient * rx * yPrime / ry;
|
|
559
|
+
const centerPrimeY = coefficient * -ry * xPrime / rx;
|
|
560
|
+
const centerX = cosPhi * centerPrimeX - sinPhi * centerPrimeY + (x1 + x2) / 2;
|
|
561
|
+
const centerY = sinPhi * centerPrimeX + cosPhi * centerPrimeY + (y1 + y2) / 2;
|
|
562
|
+
const angleBetween = (ux, uy, vx, vy) => {
|
|
563
|
+
const divisor = Math.max(Number.EPSILON, Math.hypot(ux, uy) * Math.hypot(vx, vy));
|
|
564
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, (ux * vx + uy * vy) / divisor)));
|
|
565
|
+
return ux * vy - uy * vx < 0 ? -angle : angle;
|
|
566
|
+
};
|
|
567
|
+
const startX = (xPrime - centerPrimeX) / rx;
|
|
568
|
+
const startY = (yPrime - centerPrimeY) / ry;
|
|
569
|
+
const endX = (-xPrime - centerPrimeX) / rx;
|
|
570
|
+
const endY = (-yPrime - centerPrimeY) / ry;
|
|
571
|
+
const startAngle = angleBetween(1, 0, startX, startY);
|
|
572
|
+
let deltaAngle = angleBetween(startX, startY, endX, endY);
|
|
573
|
+
if (!sweep && deltaAngle > 0) deltaAngle -= Math.PI * 2;
|
|
574
|
+
if (sweep && deltaAngle < 0) deltaAngle += Math.PI * 2;
|
|
575
|
+
const steps = Math.max(6, Math.ceil(Math.abs(deltaAngle) / (Math.PI / 12)));
|
|
576
|
+
return Array.from({ length: steps }, (_, index) => {
|
|
577
|
+
const angle = startAngle + deltaAngle * (index + 1) / steps;
|
|
578
|
+
const cosAngle = Math.cos(angle);
|
|
579
|
+
const sinAngle = Math.sin(angle);
|
|
580
|
+
return [
|
|
581
|
+
centerX + cosPhi * rx * cosAngle - sinPhi * ry * sinAngle,
|
|
582
|
+
centerY + sinPhi * rx * cosAngle + cosPhi * ry * sinAngle
|
|
583
|
+
];
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function svgShapePath(pathData) {
|
|
588
|
+
const tokens = String(pathData || "").match(SVG_PATH_TOKEN) || [];
|
|
589
|
+
const shapePath = new THREE.ShapePath();
|
|
590
|
+
let index = 0;
|
|
591
|
+
let command = "";
|
|
592
|
+
let previousCommand = "";
|
|
593
|
+
let x = 0;
|
|
594
|
+
let y = 0;
|
|
595
|
+
let startX = 0;
|
|
596
|
+
let startY = 0;
|
|
597
|
+
let cubicControlX = 0;
|
|
598
|
+
let cubicControlY = 0;
|
|
599
|
+
let quadraticControlX = 0;
|
|
600
|
+
let quadraticControlY = 0;
|
|
601
|
+
const hasNumber = () => index < tokens.length && !SVG_PATH_COMMAND.test(tokens[index]);
|
|
602
|
+
const number = () => Number(tokens[index++]);
|
|
603
|
+
const flag = () => {
|
|
604
|
+
const token = tokens[index];
|
|
605
|
+
// SVG permits arc flags to touch one another and the following coordinate
|
|
606
|
+
// (for example `0 003.839 0` means flags 0/0 and x=3.839).
|
|
607
|
+
if (/^[01].+/.test(token)) {
|
|
608
|
+
tokens[index] = token.slice(1);
|
|
609
|
+
return token[0] === "1";
|
|
610
|
+
}
|
|
611
|
+
return number() !== 0;
|
|
612
|
+
};
|
|
613
|
+
const point = (nextX, nextY, relative) => [relative ? x + nextX : nextX, relative ? y + nextY : nextY];
|
|
614
|
+
|
|
615
|
+
while (index < tokens.length) {
|
|
616
|
+
if (SVG_PATH_COMMAND.test(tokens[index])) command = tokens[index++];
|
|
617
|
+
if (!command) break;
|
|
618
|
+
const lower = command.toLowerCase();
|
|
619
|
+
const relative = command === lower;
|
|
620
|
+
|
|
621
|
+
if (lower === "z") {
|
|
622
|
+
shapePath.currentPath?.closePath();
|
|
623
|
+
x = startX;
|
|
624
|
+
y = startY;
|
|
625
|
+
previousCommand = command;
|
|
626
|
+
command = "";
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (!hasNumber()) break;
|
|
631
|
+
if (lower === "m") {
|
|
632
|
+
let first = true;
|
|
633
|
+
while (hasNumber()) {
|
|
634
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
635
|
+
x = nextX;
|
|
636
|
+
y = nextY;
|
|
637
|
+
if (first) {
|
|
638
|
+
shapePath.moveTo(x, -y);
|
|
639
|
+
startX = x;
|
|
640
|
+
startY = y;
|
|
641
|
+
first = false;
|
|
642
|
+
} else shapePath.lineTo(x, -y);
|
|
643
|
+
}
|
|
644
|
+
} else if (lower === "l") {
|
|
645
|
+
while (hasNumber()) {
|
|
646
|
+
[x, y] = point(number(), number(), relative);
|
|
647
|
+
shapePath.lineTo(x, -y);
|
|
648
|
+
}
|
|
649
|
+
} else if (lower === "h") {
|
|
650
|
+
while (hasNumber()) {
|
|
651
|
+
const value = number();
|
|
652
|
+
x = relative ? x + value : value;
|
|
653
|
+
shapePath.lineTo(x, -y);
|
|
654
|
+
}
|
|
655
|
+
} else if (lower === "v") {
|
|
656
|
+
while (hasNumber()) {
|
|
657
|
+
const value = number();
|
|
658
|
+
y = relative ? y + value : value;
|
|
659
|
+
shapePath.lineTo(x, -y);
|
|
660
|
+
}
|
|
661
|
+
} else if (lower === "c") {
|
|
662
|
+
while (hasNumber()) {
|
|
663
|
+
const [control1X, control1Y] = point(number(), number(), relative);
|
|
664
|
+
const [control2X, control2Y] = point(number(), number(), relative);
|
|
665
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
666
|
+
shapePath.currentPath?.bezierCurveTo(control1X, -control1Y, control2X, -control2Y, nextX, -nextY);
|
|
667
|
+
x = nextX;
|
|
668
|
+
y = nextY;
|
|
669
|
+
cubicControlX = control2X;
|
|
670
|
+
cubicControlY = control2Y;
|
|
671
|
+
previousCommand = command;
|
|
672
|
+
}
|
|
673
|
+
} else if (lower === "s") {
|
|
674
|
+
while (hasNumber()) {
|
|
675
|
+
const reflectedX = ["c", "s"].includes(previousCommand.toLowerCase()) ? x * 2 - cubicControlX : x;
|
|
676
|
+
const reflectedY = ["c", "s"].includes(previousCommand.toLowerCase()) ? y * 2 - cubicControlY : y;
|
|
677
|
+
const [control2X, control2Y] = point(number(), number(), relative);
|
|
678
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
679
|
+
shapePath.currentPath?.bezierCurveTo(reflectedX, -reflectedY, control2X, -control2Y, nextX, -nextY);
|
|
680
|
+
x = nextX;
|
|
681
|
+
y = nextY;
|
|
682
|
+
cubicControlX = control2X;
|
|
683
|
+
cubicControlY = control2Y;
|
|
684
|
+
previousCommand = command;
|
|
685
|
+
}
|
|
686
|
+
} else if (lower === "q") {
|
|
687
|
+
while (hasNumber()) {
|
|
688
|
+
const [controlX, controlY] = point(number(), number(), relative);
|
|
689
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
690
|
+
shapePath.currentPath?.quadraticCurveTo(controlX, -controlY, nextX, -nextY);
|
|
691
|
+
x = nextX;
|
|
692
|
+
y = nextY;
|
|
693
|
+
quadraticControlX = controlX;
|
|
694
|
+
quadraticControlY = controlY;
|
|
695
|
+
previousCommand = command;
|
|
696
|
+
}
|
|
697
|
+
} else if (lower === "t") {
|
|
698
|
+
while (hasNumber()) {
|
|
699
|
+
const controlX = ["q", "t"].includes(previousCommand.toLowerCase()) ? x * 2 - quadraticControlX : x;
|
|
700
|
+
const controlY = ["q", "t"].includes(previousCommand.toLowerCase()) ? y * 2 - quadraticControlY : y;
|
|
701
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
702
|
+
shapePath.currentPath?.quadraticCurveTo(controlX, -controlY, nextX, -nextY);
|
|
703
|
+
x = nextX;
|
|
704
|
+
y = nextY;
|
|
705
|
+
quadraticControlX = controlX;
|
|
706
|
+
quadraticControlY = controlY;
|
|
707
|
+
previousCommand = command;
|
|
708
|
+
}
|
|
709
|
+
} else if (lower === "a") {
|
|
710
|
+
while (hasNumber()) {
|
|
711
|
+
const rx = number();
|
|
712
|
+
const ry = number();
|
|
713
|
+
const rotation = number();
|
|
714
|
+
const largeArc = flag();
|
|
715
|
+
const sweep = flag();
|
|
716
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
717
|
+
for (const [arcX, arcY] of svgArcPoints(x, y, rx, ry, rotation, largeArc, sweep, nextX, nextY)) {
|
|
718
|
+
shapePath.lineTo(arcX, -arcY);
|
|
719
|
+
}
|
|
720
|
+
x = nextX;
|
|
721
|
+
y = nextY;
|
|
722
|
+
previousCommand = command;
|
|
723
|
+
}
|
|
724
|
+
} else break;
|
|
725
|
+
|
|
726
|
+
previousCommand = command;
|
|
727
|
+
}
|
|
728
|
+
return shapePath;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function createSvgExtrudeGeometry(source, options = {}) {
|
|
732
|
+
const pathTags = String(source || "").match(/<path\b[^>]*>/gi) || [];
|
|
733
|
+
const shapes = [];
|
|
734
|
+
for (const tag of pathTags) {
|
|
735
|
+
const match = tag.match(/\bd\s*=\s*(["'])([\s\S]*?)\1/i);
|
|
736
|
+
if (!match) continue;
|
|
737
|
+
const shapePath = svgShapePath(match[2]);
|
|
738
|
+
shapes.push(...shapePath.toShapes(true, false));
|
|
739
|
+
}
|
|
740
|
+
if (!shapes.length) return new THREE.BoxGeometry(1, 1, .12);
|
|
741
|
+
const bevel = Math.max(0, Math.min(.2, Number(options.bevel) || .055));
|
|
742
|
+
const geometry = new THREE.ExtrudeGeometry(shapes, {
|
|
743
|
+
depth: 1,
|
|
744
|
+
steps: 1,
|
|
745
|
+
curveSegments: Math.max(2, Math.round(Number(options.curveSegments) || 8)),
|
|
746
|
+
bevelEnabled: bevel > 0,
|
|
747
|
+
bevelSegments: Math.max(1, Math.round(Number(options.bevelSegments) || 2)),
|
|
748
|
+
bevelSize: bevel,
|
|
749
|
+
bevelThickness: bevel
|
|
750
|
+
});
|
|
751
|
+
geometry.center();
|
|
752
|
+
return geometry;
|
|
753
|
+
}
|
|
754
|
+
|
|
494
755
|
function partGeometry(part) {
|
|
495
756
|
if (part.shape === "plane") return new THREE.PlaneGeometry(1, 1);
|
|
757
|
+
if (part.shape === "svg-extrude") return createSvgExtrudeGeometry(part.svg, part);
|
|
496
758
|
return makeGeometry(part.shape || part.geometry || "box", part.radius, part.segments, part.size);
|
|
497
759
|
}
|
|
498
760
|
|
|
@@ -518,8 +780,14 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
518
780
|
normalizeGeometry(geometry);
|
|
519
781
|
const colorValue = part.color || "component";
|
|
520
782
|
const color = resolveGeometryColor(colorValue, localPalette, modelPalette.component || DEFAULT_PALETTE.component);
|
|
783
|
+
const emissiveValue = part.emissive || "#000000";
|
|
784
|
+
const emissive = resolveGeometryColor(emissiveValue, localPalette, "#000000");
|
|
521
785
|
const material = new THREE.MeshStandardMaterial({
|
|
522
786
|
color,
|
|
787
|
+
emissive,
|
|
788
|
+
emissiveIntensity: Number.isFinite(Number(part.emissiveIntensity))
|
|
789
|
+
? Math.max(0, Number(part.emissiveIntensity))
|
|
790
|
+
: 0,
|
|
523
791
|
roughness: Number.isFinite(Number(part.roughness)) ? Number(part.roughness) : .72,
|
|
524
792
|
metalness: Number.isFinite(Number(part.metalness)) ? Number(part.metalness) : .06,
|
|
525
793
|
transparent: part.transparent === true || Number(part.opacity) < 1,
|
|
@@ -646,7 +914,7 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
646
914
|
if (Array.isArray(part.rotation)) mesh.rotation.set(...part.rotation.map(Number));
|
|
647
915
|
root.add(mesh);
|
|
648
916
|
meshes.push(mesh);
|
|
649
|
-
colorBindings.push({ material, value: colorValue, palette: localPalette });
|
|
917
|
+
colorBindings.push({ material, value: colorValue, emissiveValue, palette: localPalette });
|
|
650
918
|
if (part.edges !== false && part.shape !== "plane") {
|
|
651
919
|
const edgeMaterial = new THREE.LineBasicMaterial({ color, transparent: true, opacity: Number.isFinite(Number(part.edgeOpacity)) ? Number(part.edgeOpacity) : .55 });
|
|
652
920
|
const edge = new THREE.LineSegments(new THREE.EdgesGeometry(geometry, 22), edgeMaterial);
|
|
@@ -666,6 +934,27 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
666
934
|
return { root, meshes, colorBindings, parameters, descriptor };
|
|
667
935
|
}
|
|
668
936
|
|
|
937
|
+
const LABEL_ANCHORS = Object.freeze(["auto", "top", "bottom", "left", "right"]);
|
|
938
|
+
|
|
939
|
+
export function normalizeLabelAnchor(value) {
|
|
940
|
+
const anchor = String(value || "auto").trim().toLowerCase();
|
|
941
|
+
return LABEL_ANCHORS.includes(anchor) ? anchor : "auto";
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export function labelPlacementForAnchor(value, dimensions, labelWidth, labelHeight, gap = .025) {
|
|
945
|
+
const anchor = normalizeLabelAnchor(value);
|
|
946
|
+
if (anchor === "auto") return null;
|
|
947
|
+
const width = Math.max(0, Number(dimensions?.x) || 0);
|
|
948
|
+
const height = Math.max(0, Number(dimensions?.y) || 0);
|
|
949
|
+
const textWidth = Math.max(0, Number(labelWidth) || 0);
|
|
950
|
+
const textHeight = Math.max(0, Number(labelHeight) || 0);
|
|
951
|
+
const spacing = Math.max(0, Number(gap) || 0);
|
|
952
|
+
if (anchor === "top") return { x: 0, y: height / 2 + spacing + textHeight / 2, frontRotation: 0, backRotation: 0 };
|
|
953
|
+
if (anchor === "bottom") return { x: 0, y: -height / 2 - spacing - textHeight / 2, frontRotation: 0, backRotation: 0 };
|
|
954
|
+
if (anchor === "left") return { x: -width / 2 - spacing - textHeight / 2, y: 0, frontRotation: Math.PI / 2, backRotation: -Math.PI / 2 };
|
|
955
|
+
return { x: width / 2 + spacing + textHeight / 2, y: 0, frontRotation: -Math.PI / 2, backRotation: Math.PI / 2 };
|
|
956
|
+
}
|
|
957
|
+
|
|
669
958
|
function makeLabel(text, color, dimensions, type = "", outlined = true) {
|
|
670
959
|
const bitmap = document.createElement("canvas");
|
|
671
960
|
const context = bitmap.getContext("2d");
|
|
@@ -1880,6 +2169,9 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
1880
2169
|
for (const binding of node.colorBindings || []) {
|
|
1881
2170
|
const palette = { ...this.modelData.palette, ...(node.geometryPalette || {}), ...(node.geometryPaletteOverrides || {}) };
|
|
1882
2171
|
binding.material.color.set(resolveGeometryColor(binding.value, palette, color));
|
|
2172
|
+
if (binding.material.emissive && binding.emissiveValue) {
|
|
2173
|
+
binding.material.emissive.set(resolveGeometryColor(binding.emissiveValue, palette, "#000000"));
|
|
2174
|
+
}
|
|
1883
2175
|
}
|
|
1884
2176
|
}
|
|
1885
2177
|
}
|
|
@@ -1969,6 +2261,7 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
1969
2261
|
node.group.add(node.edge);
|
|
1970
2262
|
}
|
|
1971
2263
|
node.label = makeLabel(entity.name, color, node.labelSize, entity.type, this.showShellLabels);
|
|
2264
|
+
node.labelAnchor = normalizeLabelAnchor(entity.style?.label?.anchor ?? entity.labelAnchor ?? entity.label_anchor);
|
|
1972
2265
|
node.label.userData.node = node;
|
|
1973
2266
|
node.label.traverse(labelFace => { labelFace.userData.node = node; });
|
|
1974
2267
|
node.group.add(node.label);
|
|
@@ -2299,6 +2592,7 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
2299
2592
|
const textOffsetX = node.label.userData.textOffsetX || 0;
|
|
2300
2593
|
const textOffsetY = node.label.userData.textOffsetY || 0;
|
|
2301
2594
|
const labelSize = node.labelSize || new THREE.Vector3().fromArray(node.s);
|
|
2595
|
+
const floatingLabel = labelPlacementForAnchor(node.labelAnchor, labelSize, labelWidth, labelHeight);
|
|
2302
2596
|
node.label.visible = visible;
|
|
2303
2597
|
// Only paint the face that is oriented towards the camera. Both faces
|
|
2304
2598
|
// used to be depth-test disabled, so the back label could be drawn over
|
|
@@ -2308,7 +2602,23 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
2308
2602
|
? true
|
|
2309
2603
|
: this.camera.position.z >= center.z;
|
|
2310
2604
|
const cameraAbove = this.camera.position.y > center.y + .25;
|
|
2311
|
-
if (
|
|
2605
|
+
if (floatingLabel) {
|
|
2606
|
+
front.visible = cameraOnFront;
|
|
2607
|
+
back.visible = !cameraOnFront;
|
|
2608
|
+
top.visible = false;
|
|
2609
|
+
front.scale.setScalar(1);
|
|
2610
|
+
back.scale.setScalar(1);
|
|
2611
|
+
front.position.set(floatingLabel.x, floatingLabel.y, labelSize.z / 2 + .025);
|
|
2612
|
+
back.position.set(floatingLabel.x, floatingLabel.y, -labelSize.z / 2 - .025);
|
|
2613
|
+
front.rotation.z = floatingLabel.frontRotation;
|
|
2614
|
+
back.rotation.z = floatingLabel.backRotation;
|
|
2615
|
+
front.material.color.set(0xf2fafc);
|
|
2616
|
+
back.material.color.set(0xf2fafc);
|
|
2617
|
+
front.material.opacity = focusAlpha;
|
|
2618
|
+
back.material.opacity = focusAlpha;
|
|
2619
|
+
} else if (isContainerType(node.type)) {
|
|
2620
|
+
front.rotation.z = 0;
|
|
2621
|
+
back.rotation.z = 0;
|
|
2312
2622
|
front.visible = cameraOnFront && (!transparentLabel || displayTransparentLabel);
|
|
2313
2623
|
back.visible = !cameraOnFront && (!transparentLabel || displayTransparentLabel);
|
|
2314
2624
|
// The upper label is useful only when the camera actually sees the
|
|
@@ -2342,12 +2652,18 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
2342
2652
|
top.material.color.set(displayTransparentLabel ? 0xa4b0bb : 0xf2fafc);
|
|
2343
2653
|
top.material.opacity = (displayTransparentLabel ? .82 : 1) * focusAlpha;
|
|
2344
2654
|
} else {
|
|
2655
|
+
front.rotation.z = 0;
|
|
2656
|
+
back.rotation.z = 0;
|
|
2345
2657
|
front.visible = cameraOnFront && !opened;
|
|
2346
2658
|
back.visible = !cameraOnFront && !opened;
|
|
2347
2659
|
top.visible = false;
|
|
2348
2660
|
front.scale.setScalar(1);
|
|
2349
2661
|
front.position.x = 0;
|
|
2350
2662
|
front.position.y = labelSize.y / 2 - .09 - labelHeight / 2;
|
|
2663
|
+
front.position.z = labelSize.z / 2 + .012;
|
|
2664
|
+
back.position.x = 0;
|
|
2665
|
+
back.position.y = labelSize.y / 2 - .09 - labelHeight / 2;
|
|
2666
|
+
back.position.z = -labelSize.z / 2 - .012;
|
|
2351
2667
|
front.material.color.set(0xf2fafc);
|
|
2352
2668
|
front.material.opacity = focusAlpha;
|
|
2353
2669
|
back.material.opacity = focusAlpha;
|