@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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
  }