@liquidcars/atlas 0.1.39 → 0.1.40
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 +729 -550
- package/dist/liquidcars-atlas.js.map +1 -1
- package/package.json +1 -1
- package/src/liquidcars-atlas.js +229 -1
package/package.json
CHANGED
package/src/liquidcars-atlas.js
CHANGED
|
@@ -491,8 +491,227 @@ function iconTexture(value, foreground = "#ffffff", background = "transparent")
|
|
|
491
491
|
return texture;
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
+
const SVG_PATH_COMMAND = /^[AaCcHhLlMmQqSsTtVvZz]$/;
|
|
495
|
+
const SVG_PATH_TOKEN = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?|[AaCcHhLlMmQqSsTtVvZz]/g;
|
|
496
|
+
|
|
497
|
+
function svgArcPoints(x1, y1, rxValue, ryValue, rotation, largeArc, sweep, x2, y2) {
|
|
498
|
+
let rx = Math.abs(Number(rxValue));
|
|
499
|
+
let ry = Math.abs(Number(ryValue));
|
|
500
|
+
if (!rx || !ry || (x1 === x2 && y1 === y2)) return [[x2, y2]];
|
|
501
|
+
const phi = Number(rotation || 0) * Math.PI / 180;
|
|
502
|
+
const cosPhi = Math.cos(phi);
|
|
503
|
+
const sinPhi = Math.sin(phi);
|
|
504
|
+
const dx = (x1 - x2) / 2;
|
|
505
|
+
const dy = (y1 - y2) / 2;
|
|
506
|
+
const xPrime = cosPhi * dx + sinPhi * dy;
|
|
507
|
+
const yPrime = -sinPhi * dx + cosPhi * dy;
|
|
508
|
+
const scale = Math.sqrt(Math.max(1, xPrime * xPrime / (rx * rx) + yPrime * yPrime / (ry * ry)));
|
|
509
|
+
rx *= scale;
|
|
510
|
+
ry *= scale;
|
|
511
|
+
const numerator = Math.max(0, rx * rx * ry * ry - rx * rx * yPrime * yPrime - ry * ry * xPrime * xPrime);
|
|
512
|
+
const denominator = Math.max(Number.EPSILON, rx * rx * yPrime * yPrime + ry * ry * xPrime * xPrime);
|
|
513
|
+
const sign = Boolean(largeArc) === Boolean(sweep) ? -1 : 1;
|
|
514
|
+
const coefficient = sign * Math.sqrt(numerator / denominator);
|
|
515
|
+
const centerPrimeX = coefficient * rx * yPrime / ry;
|
|
516
|
+
const centerPrimeY = coefficient * -ry * xPrime / rx;
|
|
517
|
+
const centerX = cosPhi * centerPrimeX - sinPhi * centerPrimeY + (x1 + x2) / 2;
|
|
518
|
+
const centerY = sinPhi * centerPrimeX + cosPhi * centerPrimeY + (y1 + y2) / 2;
|
|
519
|
+
const angleBetween = (ux, uy, vx, vy) => {
|
|
520
|
+
const divisor = Math.max(Number.EPSILON, Math.hypot(ux, uy) * Math.hypot(vx, vy));
|
|
521
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, (ux * vx + uy * vy) / divisor)));
|
|
522
|
+
return ux * vy - uy * vx < 0 ? -angle : angle;
|
|
523
|
+
};
|
|
524
|
+
const startX = (xPrime - centerPrimeX) / rx;
|
|
525
|
+
const startY = (yPrime - centerPrimeY) / ry;
|
|
526
|
+
const endX = (-xPrime - centerPrimeX) / rx;
|
|
527
|
+
const endY = (-yPrime - centerPrimeY) / ry;
|
|
528
|
+
const startAngle = angleBetween(1, 0, startX, startY);
|
|
529
|
+
let deltaAngle = angleBetween(startX, startY, endX, endY);
|
|
530
|
+
if (!sweep && deltaAngle > 0) deltaAngle -= Math.PI * 2;
|
|
531
|
+
if (sweep && deltaAngle < 0) deltaAngle += Math.PI * 2;
|
|
532
|
+
const steps = Math.max(6, Math.ceil(Math.abs(deltaAngle) / (Math.PI / 12)));
|
|
533
|
+
return Array.from({ length: steps }, (_, index) => {
|
|
534
|
+
const angle = startAngle + deltaAngle * (index + 1) / steps;
|
|
535
|
+
const cosAngle = Math.cos(angle);
|
|
536
|
+
const sinAngle = Math.sin(angle);
|
|
537
|
+
return [
|
|
538
|
+
centerX + cosPhi * rx * cosAngle - sinPhi * ry * sinAngle,
|
|
539
|
+
centerY + sinPhi * rx * cosAngle + cosPhi * ry * sinAngle
|
|
540
|
+
];
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function svgShapePath(pathData) {
|
|
545
|
+
const tokens = String(pathData || "").match(SVG_PATH_TOKEN) || [];
|
|
546
|
+
const shapePath = new THREE.ShapePath();
|
|
547
|
+
let index = 0;
|
|
548
|
+
let command = "";
|
|
549
|
+
let previousCommand = "";
|
|
550
|
+
let x = 0;
|
|
551
|
+
let y = 0;
|
|
552
|
+
let startX = 0;
|
|
553
|
+
let startY = 0;
|
|
554
|
+
let cubicControlX = 0;
|
|
555
|
+
let cubicControlY = 0;
|
|
556
|
+
let quadraticControlX = 0;
|
|
557
|
+
let quadraticControlY = 0;
|
|
558
|
+
const hasNumber = () => index < tokens.length && !SVG_PATH_COMMAND.test(tokens[index]);
|
|
559
|
+
const number = () => Number(tokens[index++]);
|
|
560
|
+
const flag = () => {
|
|
561
|
+
const token = tokens[index];
|
|
562
|
+
// SVG permits arc flags to touch one another and the following coordinate
|
|
563
|
+
// (for example `0 003.839 0` means flags 0/0 and x=3.839).
|
|
564
|
+
if (/^[01].+/.test(token)) {
|
|
565
|
+
tokens[index] = token.slice(1);
|
|
566
|
+
return token[0] === "1";
|
|
567
|
+
}
|
|
568
|
+
return number() !== 0;
|
|
569
|
+
};
|
|
570
|
+
const point = (nextX, nextY, relative) => [relative ? x + nextX : nextX, relative ? y + nextY : nextY];
|
|
571
|
+
|
|
572
|
+
while (index < tokens.length) {
|
|
573
|
+
if (SVG_PATH_COMMAND.test(tokens[index])) command = tokens[index++];
|
|
574
|
+
if (!command) break;
|
|
575
|
+
const lower = command.toLowerCase();
|
|
576
|
+
const relative = command === lower;
|
|
577
|
+
|
|
578
|
+
if (lower === "z") {
|
|
579
|
+
shapePath.currentPath?.closePath();
|
|
580
|
+
x = startX;
|
|
581
|
+
y = startY;
|
|
582
|
+
previousCommand = command;
|
|
583
|
+
command = "";
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (!hasNumber()) break;
|
|
588
|
+
if (lower === "m") {
|
|
589
|
+
let first = true;
|
|
590
|
+
while (hasNumber()) {
|
|
591
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
592
|
+
x = nextX;
|
|
593
|
+
y = nextY;
|
|
594
|
+
if (first) {
|
|
595
|
+
shapePath.moveTo(x, -y);
|
|
596
|
+
startX = x;
|
|
597
|
+
startY = y;
|
|
598
|
+
first = false;
|
|
599
|
+
} else shapePath.lineTo(x, -y);
|
|
600
|
+
}
|
|
601
|
+
} else if (lower === "l") {
|
|
602
|
+
while (hasNumber()) {
|
|
603
|
+
[x, y] = point(number(), number(), relative);
|
|
604
|
+
shapePath.lineTo(x, -y);
|
|
605
|
+
}
|
|
606
|
+
} else if (lower === "h") {
|
|
607
|
+
while (hasNumber()) {
|
|
608
|
+
const value = number();
|
|
609
|
+
x = relative ? x + value : value;
|
|
610
|
+
shapePath.lineTo(x, -y);
|
|
611
|
+
}
|
|
612
|
+
} else if (lower === "v") {
|
|
613
|
+
while (hasNumber()) {
|
|
614
|
+
const value = number();
|
|
615
|
+
y = relative ? y + value : value;
|
|
616
|
+
shapePath.lineTo(x, -y);
|
|
617
|
+
}
|
|
618
|
+
} else if (lower === "c") {
|
|
619
|
+
while (hasNumber()) {
|
|
620
|
+
const [control1X, control1Y] = point(number(), number(), relative);
|
|
621
|
+
const [control2X, control2Y] = point(number(), number(), relative);
|
|
622
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
623
|
+
shapePath.currentPath?.bezierCurveTo(control1X, -control1Y, control2X, -control2Y, nextX, -nextY);
|
|
624
|
+
x = nextX;
|
|
625
|
+
y = nextY;
|
|
626
|
+
cubicControlX = control2X;
|
|
627
|
+
cubicControlY = control2Y;
|
|
628
|
+
previousCommand = command;
|
|
629
|
+
}
|
|
630
|
+
} else if (lower === "s") {
|
|
631
|
+
while (hasNumber()) {
|
|
632
|
+
const reflectedX = ["c", "s"].includes(previousCommand.toLowerCase()) ? x * 2 - cubicControlX : x;
|
|
633
|
+
const reflectedY = ["c", "s"].includes(previousCommand.toLowerCase()) ? y * 2 - cubicControlY : y;
|
|
634
|
+
const [control2X, control2Y] = point(number(), number(), relative);
|
|
635
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
636
|
+
shapePath.currentPath?.bezierCurveTo(reflectedX, -reflectedY, control2X, -control2Y, nextX, -nextY);
|
|
637
|
+
x = nextX;
|
|
638
|
+
y = nextY;
|
|
639
|
+
cubicControlX = control2X;
|
|
640
|
+
cubicControlY = control2Y;
|
|
641
|
+
previousCommand = command;
|
|
642
|
+
}
|
|
643
|
+
} else if (lower === "q") {
|
|
644
|
+
while (hasNumber()) {
|
|
645
|
+
const [controlX, controlY] = point(number(), number(), relative);
|
|
646
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
647
|
+
shapePath.currentPath?.quadraticCurveTo(controlX, -controlY, nextX, -nextY);
|
|
648
|
+
x = nextX;
|
|
649
|
+
y = nextY;
|
|
650
|
+
quadraticControlX = controlX;
|
|
651
|
+
quadraticControlY = controlY;
|
|
652
|
+
previousCommand = command;
|
|
653
|
+
}
|
|
654
|
+
} else if (lower === "t") {
|
|
655
|
+
while (hasNumber()) {
|
|
656
|
+
const controlX = ["q", "t"].includes(previousCommand.toLowerCase()) ? x * 2 - quadraticControlX : x;
|
|
657
|
+
const controlY = ["q", "t"].includes(previousCommand.toLowerCase()) ? y * 2 - quadraticControlY : y;
|
|
658
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
659
|
+
shapePath.currentPath?.quadraticCurveTo(controlX, -controlY, nextX, -nextY);
|
|
660
|
+
x = nextX;
|
|
661
|
+
y = nextY;
|
|
662
|
+
quadraticControlX = controlX;
|
|
663
|
+
quadraticControlY = controlY;
|
|
664
|
+
previousCommand = command;
|
|
665
|
+
}
|
|
666
|
+
} else if (lower === "a") {
|
|
667
|
+
while (hasNumber()) {
|
|
668
|
+
const rx = number();
|
|
669
|
+
const ry = number();
|
|
670
|
+
const rotation = number();
|
|
671
|
+
const largeArc = flag();
|
|
672
|
+
const sweep = flag();
|
|
673
|
+
const [nextX, nextY] = point(number(), number(), relative);
|
|
674
|
+
for (const [arcX, arcY] of svgArcPoints(x, y, rx, ry, rotation, largeArc, sweep, nextX, nextY)) {
|
|
675
|
+
shapePath.lineTo(arcX, -arcY);
|
|
676
|
+
}
|
|
677
|
+
x = nextX;
|
|
678
|
+
y = nextY;
|
|
679
|
+
previousCommand = command;
|
|
680
|
+
}
|
|
681
|
+
} else break;
|
|
682
|
+
|
|
683
|
+
previousCommand = command;
|
|
684
|
+
}
|
|
685
|
+
return shapePath;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function createSvgExtrudeGeometry(source, options = {}) {
|
|
689
|
+
const pathTags = String(source || "").match(/<path\b[^>]*>/gi) || [];
|
|
690
|
+
const shapes = [];
|
|
691
|
+
for (const tag of pathTags) {
|
|
692
|
+
const match = tag.match(/\bd\s*=\s*(["'])([\s\S]*?)\1/i);
|
|
693
|
+
if (!match) continue;
|
|
694
|
+
const shapePath = svgShapePath(match[2]);
|
|
695
|
+
shapes.push(...shapePath.toShapes(true, false));
|
|
696
|
+
}
|
|
697
|
+
if (!shapes.length) return new THREE.BoxGeometry(1, 1, .12);
|
|
698
|
+
const bevel = Math.max(0, Math.min(.2, Number(options.bevel) || .055));
|
|
699
|
+
const geometry = new THREE.ExtrudeGeometry(shapes, {
|
|
700
|
+
depth: 1,
|
|
701
|
+
steps: 1,
|
|
702
|
+
curveSegments: Math.max(2, Math.round(Number(options.curveSegments) || 8)),
|
|
703
|
+
bevelEnabled: bevel > 0,
|
|
704
|
+
bevelSegments: Math.max(1, Math.round(Number(options.bevelSegments) || 2)),
|
|
705
|
+
bevelSize: bevel,
|
|
706
|
+
bevelThickness: bevel
|
|
707
|
+
});
|
|
708
|
+
geometry.center();
|
|
709
|
+
return geometry;
|
|
710
|
+
}
|
|
711
|
+
|
|
494
712
|
function partGeometry(part) {
|
|
495
713
|
if (part.shape === "plane") return new THREE.PlaneGeometry(1, 1);
|
|
714
|
+
if (part.shape === "svg-extrude") return createSvgExtrudeGeometry(part.svg, part);
|
|
496
715
|
return makeGeometry(part.shape || part.geometry || "box", part.radius, part.segments, part.size);
|
|
497
716
|
}
|
|
498
717
|
|
|
@@ -518,8 +737,14 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
518
737
|
normalizeGeometry(geometry);
|
|
519
738
|
const colorValue = part.color || "component";
|
|
520
739
|
const color = resolveGeometryColor(colorValue, localPalette, modelPalette.component || DEFAULT_PALETTE.component);
|
|
740
|
+
const emissiveValue = part.emissive || "#000000";
|
|
741
|
+
const emissive = resolveGeometryColor(emissiveValue, localPalette, "#000000");
|
|
521
742
|
const material = new THREE.MeshStandardMaterial({
|
|
522
743
|
color,
|
|
744
|
+
emissive,
|
|
745
|
+
emissiveIntensity: Number.isFinite(Number(part.emissiveIntensity))
|
|
746
|
+
? Math.max(0, Number(part.emissiveIntensity))
|
|
747
|
+
: 0,
|
|
523
748
|
roughness: Number.isFinite(Number(part.roughness)) ? Number(part.roughness) : .72,
|
|
524
749
|
metalness: Number.isFinite(Number(part.metalness)) ? Number(part.metalness) : .06,
|
|
525
750
|
transparent: part.transparent === true || Number(part.opacity) < 1,
|
|
@@ -646,7 +871,7 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
646
871
|
if (Array.isArray(part.rotation)) mesh.rotation.set(...part.rotation.map(Number));
|
|
647
872
|
root.add(mesh);
|
|
648
873
|
meshes.push(mesh);
|
|
649
|
-
colorBindings.push({ material, value: colorValue, palette: localPalette });
|
|
874
|
+
colorBindings.push({ material, value: colorValue, emissiveValue, palette: localPalette });
|
|
650
875
|
if (part.edges !== false && part.shape !== "plane") {
|
|
651
876
|
const edgeMaterial = new THREE.LineBasicMaterial({ color, transparent: true, opacity: Number.isFinite(Number(part.edgeOpacity)) ? Number(part.edgeOpacity) : .55 });
|
|
652
877
|
const edge = new THREE.LineSegments(new THREE.EdgesGeometry(geometry, 22), edgeMaterial);
|
|
@@ -1880,6 +2105,9 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
1880
2105
|
for (const binding of node.colorBindings || []) {
|
|
1881
2106
|
const palette = { ...this.modelData.palette, ...(node.geometryPalette || {}), ...(node.geometryPaletteOverrides || {}) };
|
|
1882
2107
|
binding.material.color.set(resolveGeometryColor(binding.value, palette, color));
|
|
2108
|
+
if (binding.material.emissive && binding.emissiveValue) {
|
|
2109
|
+
binding.material.emissive.set(resolveGeometryColor(binding.emissiveValue, palette, "#000000"));
|
|
2110
|
+
}
|
|
1883
2111
|
}
|
|
1884
2112
|
}
|
|
1885
2113
|
}
|